Bạn biết điều gì buồn cười không?
Những sinh viên mới ra trường bootcamp viết code quá đơn giản.
Sáu tháng sau, khi phát hiện ra các design pattern, họ viết code cần bằng tiến sĩ mới hiểu nổi.
Hành trình của một developer về cơ bản là:
“Chờ đã, mình có thể dùng classes?” →“MỌI THỨ PHẢI LÀ FACTORY STRATEGY OBSERVER SINGLETON.”
Để tôi kể cho bạn nghe về lần tôi kế thừa một codebase mà có người đã “thiết kế kiến trúc” cho việc hiển thị tên đầy đủ của người dùng.
Nội dung chính
Mục Lục
- Tội Ác Chiến Tranh
- Cờ Đỏ #1:
Ngụy Biện “Chống Lỗi Thời” - Cờ Đỏ #2:
Interface Chỉ Có Một Triển Khai - Cờ Đỏ #3:
Giải Pháp Tổng Quát Không Ai Đòi Hỏi - Cờ Đỏ #4:
Trừu Tượng Hóa Code Ổn Định, Ghép Nối Code Bất Ổn - Cờ Đỏ #5:
Tư Duy “Doanh Nghiệp” - Cờ Đỏ #6:
Sự Trừu Tượng Hóa Non - Khi Nào Trừu Tượng Hóa Thực Sự Có Ý Nghĩa
- Danh Sách Kiểm Tra:
Bạn Có Nên Trừu Tượng Hóa Cái Này? - Phục Hồi:
Xóa Các Trừu Tượng Hóa Tồi - Sự Thật Về Code “Có Thể Mở Rộng”
- Triết Lý
- Kết Luận
Tội Ác Chiến Tranh
// user-name-display-strategy.interface.tsexportinterfaceIUserNameDisplayStrategy{formatName(context:UserNameContext):string;supports(type:DisplayType):boolean;}// user-name-context.interface.tsexportinterfaceUserNameContext{firstName:string;lastName:string;locale:string;preferences:UserDisplayPreferences;culturalNamingConvention:CulturalNamingConvention;titlePrefix?:string;suffixes?:string[];}// user-name-display-strategy.factory.ts@Injectable()exportclassUserNameDisplayStrategyFactory{constructor(@Inject("DISPLAY_STRATEGIES")privatereadonlystrategies:IUserNameDisplayStrategy[]){}create(type:DisplayType):IUserNameDisplayStrategy{conststrategy=this.strategies.find((s)=>s.supports(type));if(!strategy){thrownewUnsupportedDisplayTypeException(type);}returnstrategy;}}// standard-user-name-display.strategy.ts@Injectable()exportclassStandardUserNameDisplayStrategyimplementsIUserNameDisplayStrategy{supports(type:DisplayType):boolean{returntype===DisplayType.STANDARD;}formatName(context:UserNameContext):string{return`${context.firstName}${context.lastName}`;}}// Mô-đun kết nối kiến trúc tuyệt đẹp này lại với nhau@Module({providers:[UserNameDisplayStrategyFactory,StandardUserNameDisplayStrategy,FormalUserNameDisplayStrategy,InformalUserNameDisplayStrategy,{provide:"DISPLAY_STRATEGIES",useFactory:(...strategies)=>strategies,inject:[StandardUserNameDisplayStrategy,FormalUserNameDisplayStrategy,InformalUserNameDisplayStrategy,],},],exports:[UserNameDisplayStrategyFactory],})exportclassUserNameDisplayModule{}// Cách sử dụng (hít một hơi thật sâu):constcontext:UserNameContext={firstName:user.firstName,lastName:user.lastName,locale:"en-US",preferences:userPreferences,culturalNamingConvention:CulturalNamingConvention.WESTERN,};conststrategy=this.strategyFactory.create(DisplayType.STANDARD);constdisplayName=strategy.formatName(context);
Điều này thực sự làm gì:
`${user.firstName}${user.lastName}`;
Tôi thậm chí không đùa đâu.
200+ dòng “kiến trúc” chỉ để nối hai chuỗi với một khoảng trắng.
Người phát triển viết cái này chắc đã xăm “Design Patterns” của Gang of Four lên lưng dưới của họ.
Cờ đỏ #1:
Ngụy biện “Chuẩn bị cho tương lai”
Để tôi nói cho bạn một bí mật:Bạn không thể dự đoán tương lai, và bạn rất tệ trong việc đó.
// "Chúng ta có thể cần nhiều nhà cung cấp thanh toán một ngày nào đó!"exportinterfaceIPaymentGateway{processPayment(request:PaymentRequest):Promise<PaymentResult>;refund(transactionId:string):Promise<RefundResult>;validateCard(card:CardDetails):Promise<boolean>;}exportinterfaceIPaymentGatewayFactory{create(provider:PaymentProvider):IPaymentGateway;}@Injectable()exportclassStripePaymentGatewayimplementsIPaymentGateway{// Triển khai duy nhất trong 3 năm qua// Có lẽ sẽ là duy nhất trong 3 năm tới// Nhưng này, chúng ta đã "sẵn sàng" cho PayPal!}@Injectable()exportclassPaymentGatewayFactoryimplementsIPaymentGatewayFactory{create(provider:PaymentProvider):IPaymentGateway{switch(provider){casePaymentProvider.STRIPE:returnnewStripePaymentGateway();default:thrownewError("Nhà cung cấp thanh toán không được hỗ trợ");}}}
Ba năm sau, khi bạn cuối cùng thêm PayPal:
- Yêu cầu của bạn đã hoàn toàn thay đổi
- API của Stripe đã phát triển
- Sự trừu tượng không phù hợp với trường hợp sử dụng mới
- Bạn vẫn phải tái cấu trúc mọi thứ
Điều bạn nên viết:
@Injectable()exportclassPaymentService{constructor(privatestripe:Stripe){}asynccharge(amount:number,token:string):Promise<string>{constcharge=awaitthis.stripe.charges.create({amount,currency:"usd",source:token,});returncharge.id;}}
Xong.
Khi PayPal xuất hiện (NẾU nó xuất hiện), bạn sẽ tái cấu trúc với các yêu cầu thực tế.
Không phải những yêu cầu giả định bạn mơ lúc 2 giờ sáng.
Cảnh báo đỏ #2:
Interface với một triển khai
Đây là cái tôi thích nhất.
Nó giống như mang ô vào sa mạc “phòng khi cần”.
exportinterfaceIUserService{findById(id:string):Promise<User>;create(dto:CreateUserDto):Promise<User>;update(id:string,dto:UpdateUserDto):Promise<User>;}@Injectable()exportclassUserServiceimplementsIUserService{// Triển khai duy nhất// Sẽ là triển khai duy nhất cho đến khi vũ trụ kết thúcasyncfindById(id:string):Promise<User>{returnthis.userRepository.findOne({where:{id}});}}
Chúc mừng, bạn đã đạt được:
- ✅ Khiến IDE của bạn mất hai cú click thay vì một để nhảy đến định nghĩa
- ✅ Thêm hậu tố “Impl” vào tên lớp như năm 2005
- ✅ Tạo ra sự nhầm lẫn:
“Chờ đã, tại sao lại có interface?” - ✅ Khiến việc tái cấu trúc sau này khó khăn hơn (giờ bạn phải cập nhật hai thứ)
- ✅ Không có lợi ích thực tế nào
Chỉ cần viết cái service đó đi:
@Injectable()exportclassUserService{constructor(privateuserRepository:UserRepository){}asyncfindById(id:string):Promise<User>{returnthis.userRepository.findOne({where:{id}});}}
“Nhưng còn kiểm thử thì sao?” Ê, TypeScript cójest.mock().
Bạn không cần interface để mock mọi thứ.
Khi interface THỰC SỰ hữu ích:
// "Điều này sẽ tiết kiệm RẤT NHIỀU thời gian!"exportabstractclassBaseService<T,ID=string>{constructor(protectedrepository:Repository<T>){}asyncfindById(id:ID):Promise<T>{constentity=awaitthis.repository.findOne({where:{id}});if(!entity){thrownewNotFoundException(`${this.getEntityName()}không tìm thấy`);}returnentity;}asyncfindAll(query?:QueryParams):Promise<T[]>{returnthis.repository.find(this.buildQuery(query));}asynccreate(dto:DeepPartial<T>):Promise<T>{this<s
pan>.validate(dto);returnthis.repository.save(dto);}asyncupdate(id:ID,dto:DeepPartial<T>):Promise<T>{constentity=awaitthis.findById(id);this.validate(dto);returnthis.repository.save({…entity,…dto});}asyncdelete(id:ID):Promise<void>{awaitthis.repository.delete(id);}protectedabstractgetEntityName():string;protectedabstractvalidate(dto:DeepPartial<T>):void;protectedbuildQuery(query?:QueryParams):any{// 50 dòng logic xây dựng truy vấn “tái sử dụng”}}@Injectable</span>()xuấtlớpDịchVụNgườiDùngkế thừaDịchVụCơSở<NgườiDùng>{hàm khởi tạo(khoNgườiDùng:KhoNgườiDùng){gốc(khoNgườiDùng);}được bảo vệlấyTênThựcThể():chuỗi{trả về“NgườiDùng“;}được bảo vệxác thực(đối tượng truyền dữ liệu:Một phần sâu<NgườiDùng>):rỗng{// Chờ đã, người dùng cần xác thực đặc biệtnếu(!đối tượng truyền dữ liệu.email?.bao gồm(“@“)){némmớiNgoại lệ yêu cầu không hợp lệ(“Email không hợp lệ“);}// Và mã hóa mật khẩu// Và xác minh email// Và…
cái này không còn phù hợp với mẫu nữa}// Giờ bạn cần ghi đè một nửa các phương thức cơ sởkhông đồng bộtạo(đối tượng truyền dữ liệu:Đối tượng truyền dữ liệu tạo người dùng):Lời hứa<NgườiDùng>{// Không thể dùng gốc.tạo() vì người dùng đặc biệt// Nên bạn viết lại nó ở đây// Làm mất đi toàn bộ mục đích của lớp cơ sở}}
Bước ngoặt:Mọi thực thể cuối cùng đều trở nên “đặc biệt” và bạn ghi đè mọi thứ.
Lớp cơ sở trở thành một tượng đài 500 dòng của thời gian lãng phí.
Điều bạn nên làm:
@Injectable()exportclassUserService{constructor(privateuserRepository:UserRepository,privatepasswordService:PasswordService){}asynccreate(dto:CreateUserDto):Promise<User>{if(awaitthis.emailExists(dto.email)){thrownewConflictException("Email đã tồn tại");}consthashedPassword=awaitthis.passwordService.hash(dto.password);returnthis.userRepository.save({...dto,password:hashedPassword,});}// Chỉ những phương thức người dùng thực sự cần}
Nhàm chán?
Có.
Dễ đọc?
Cũng có.
Dễ bảo trì?
Cực kỳ có.
Cờ đỏ #4:
Trừu tượng hóa mã ổn định, Ghép nối mã không ổn định
Đây là lỗi yêu thích của tôi vì nó ngược đời một cách kỳ lạ.
// Lập trình viên:
"Để tôi trừu tượng hóa phép tính này!"exportinterfaceIDiscountCalculator{calculate(context:DiscountContext):number;}@Injectable()exportclassPercentageDiscountCalculatorimplementsIDiscountCalculator{calculate(context:DiscountContext):number{returncontext.price*(context.percentage/100);}}@Injectable()exportclassFixedDiscountCalculatorimplementsIDiscountCalculator{calculate(context:DiscountContext):number{returncontext.price-context.fixedAmount;}}// Factory, strategy pattern, đủ cả bộ sưu tập// Cho...
toán học cơ bản chưa thay đổi từ thời Babylon cổ đại
Trong khi đó, cùng trong codebase:
@Injectable()exportclassOrderService{asyncprocessPayment(order:Order):Promise<void>{// Gọi API Stripe cứngconstcharge=awaitfetch("https://api.stripe.com/v1/charges",{method:"POST",headers:{Authorization:`Bearer${process.env.STRIPE_KEY}`,},body:JSON.stringify({amount:order.total,currency:"usd",source:order.paymentToken,}),});// Phân tích định dạng phản hồi đặc thù của Stripeconstresult=awaitcharge.json();order.stripeChargeId=result.id;}}
Để tôi nói thẳng:
- Số học cơ bản (không bao giờ thay đổi):
Trừu tượng hóa nặng nề ✅ - Gọi API bên ngoài (thay đổi liên tục):
Ghép nối chặt chẽ ✅ - Lựa chọn nghề nghiệp:
Đáng nghi ngờ ✅
Hãy làm ngược lại:
// Toán học là toán học, giữ nó đơn giảnexportclassDiscountCalculator{calculatePercentage(price:number,percent:number):number{returnprice*(percent/100);}calculateFixed(price:number,amount:number):number{returnMath.max(0,price-amount);}}// Các phụ thuộc bên ngoài cần trừu tượng hóaexportinterfacePaymentProcessor{charge(amount:number,token:string):Promise<PaymentResult>;}@Injectable()exportclassStripeProcessorimplementsPaymentProcessor{asynccharge(amount:number,token:string):Promise<PaymentResult>{// Các phần cụ thể của Stripe được cô lập ở đây}}
Nguyên tắc:Trừu tượng hóa những gì thay đổi.
Đừng trừu tượng hóa những gì ổn định.
Cảnh báo #5:
Tư duy “Doanh nghiệp”
Tôi từng thấy đoạn code yêu cầumười một tệpđể lưu tùy chọn của người dùng.
Không phải tùy chọn phức tạp.
Chỉ là bật/tắt chế độ tối.
// preference-persistence-strategy.interface.tsexportinterfaceIPreferencePersistenceStrategy{persist(context:PreferencePersistenceContext):Promise<void>;}// preference-persistence-context-builder.interface.tsexportinterfaceIPreferencePersistenceContextBuilder{build(params:PreferencePersistenceParameters):PreferencePersistenceContext;}// preference-persistence-orchestrator.service.ts@Injectable()exportclassPreferencePersistenceOrchestrator{constructor(privatecontextBuilder:IPreferencePersistenceContextBuilder,privatestrategyFactory:IPreferencePersistenceStrategyFactory,privatevalidator:IPreferencePersistenceValidator){}asyncorchestrate(params:PreferencePersistenceParameters):Promise<void>{constcontext=awaitthis.contextBuilder.build(params);constvalidationResult=awaitthis.validator.validate(context);if(!validationResult.isValid){thrownewValidationException(validationResult.errors);}conststrategy=this.strategyFactory.create(context.persistenceType);awaitstrategy.persist(context);}}
Điều này làm gì:
awaitthis.userRepository.update(userId,{darkMode:true});
Tôi tin chắc người viết code này được trả lương theo số dòng.
Căn bệnh:Đọc quá nhiều sách về “kiến trúc doanh nghiệp” và nghĩ rằng nhiều tệp hơn = code tốt hơn.
Cách chữa:Tự hỏi bản thân, “Tôi đang giải quyết vấn đề thực sự hay đang chơi trò đóng vai Kỹ sư Phần mềm?”
Cảnh báo #6:
Trừu tượng hóa sớm
Quy tắc ba lần(mà mọi người đều phớt lờ):
- Viết nó
- Viết lại lần nữa
- Thấy mẫu?
BÂY GIỜ hãy trừu tượng hóa nó
Điều thực sự xảy ra:
- Viết một lần
- “TÔI CÓ THỂ cần cái này lần nữa, để tôi trừu tượng hóa!”
- Tạo một framework
- Trường hợp sử dụng thứ hai hoàn toàn khác
- Vật lộn với sự trừu tượng trong 6 tháng
- Viết lại mọi thứ
// First API endpoint@Controller("users")exportclassUserController{@Get(":id")asyncgetUser(@Param("id")id:string){returnthis.userService.findById(id);}}// Developer brain:
"I should make a base controller for all resources!"@Controller()exportabstractclassBaseResourceController<T,CreateDto,UpdateDto>{constructor(protectedservice:BaseService<T>){}@Get(":id")asyncget(@Param("id")id:string):Promise<T>{returnthis.service.findById(id);}@Post()asynccreate(@Body()dto:CreateDto):Promise<T>{returnthis.service.create(dto);}@Put(":id")asyncupdate(@Param("id")id:string,@Body()dto:UpdateDto):Promise<T>{returnthis.service.update(id,dto);}@Delete(":id")asyncdelete(@Param("id")id:string):Promise<void>{returnthis.service.delete(id);}}// Now every controller that doesn't fit this pattern is a special case// Users need password reset endpoint// Products need image upload// Orders need status transitions// Everything is fighting the abstraction
Cách làm thông minh:
// Write the first one@Controller("users")exportclassUserController{// Full implementation}// Write the second one@Controller("products")exportclassProductController{// Copy-paste, modify as needed}// On the third one, IF there's a clear pattern:// Extract only the truly common parts
Lời khuyên:Lặp lại rẻ hơn là tạo abstraction sai.
Bạn luôn có thể DRY sau.
Abstraction sớm giống như tối ưu hóa sớm
– nó là gốc rễ của mọi vấn đề, nhưng ít vui hơn để đùa.
Khi Nào Abstraction Thực Sự Có Ý Nghĩa
Nghe này, tôi không chống lại abstraction.
Tôi chống lại abstraction ngu ngốc.
Đây là khi nó thực sự thông minh:
1. API Bên Ngoài SẼ Thay Đổi
// You're literally switching from Stripe to PayPal next quarterexportinterfacePaymentProvider{charge(amount:number):Promise<string>;}// This abstraction will save your ass
2. Nhiều Triển Khai THỰC SỰ
// You have all of these in production RIGHT NOWexportinterfaceStorageProvider{upload(file:Buffer):Promise<string>;}@Injectable()exportclassS3StorageimplementsStorageProvider{// Used for production files}@Injectable()exportclassLocalStorageimplementsStorageProvider{// Used in development}@Injectable()exportclassCloudinaryStorageimplementsStorageProvider{// Used for images}
3. Điểm Kiểm Thử
// Makes mocking way easierexportinterfaceTimeProvider{now():Date;}// Test with frozen time, run in prod with real time
4. Hệ Thống Plugin
// Được thiết kế cho các tiện ích mở rộng bên thứ baexportinterfaceWebhookHandler{handle(payload:unknown):Promise<void>;supports(event:string):boolean;}// Nhà phát triển có thể thêm Slack, Discord, các trình xử lý tùy chỉnh
Danh sách kiểm tra:
Bạn có nên trừu tượng hóa điều này?
Trước khi tạo một lớp trừu tượng, hãy tự hỏi:
🚨 DỪNG LẠI nếu bạn trả lời “không” cho những câu sau:
- Tôi có 2+ TRƯỜNG HỢP SỬ DỤNG THỰC TẾ ngay bây giờ?
- Điều này có cô lập thứ gì đó thường xuyên thay đổi không?
- Một nhà phát triển mới có hiểu tại sao cái này tồn tại không?
- Đây có phải đang giải quyết một vấn đề THỰC SỰ mà tôi gặp phải HIỆN TẠI?
🛑 CHẮC CHẮN DỪNG LẠI nếu những điều này đúng:
- “Chúng ta có thể cần cái này một ngày nào đó”
- “Nó chuyên nghiệp hơn”
- “Tôi đọc về mẫu thiết kế này”
- “Nó có khả năng mở rộng hơn”
- “Ứng dụng doanh nghiệp làm theo cách này”
✅ ĐÈN XANH nếu:
- Có nhiều triển khai tồn tại NGAY BÂY GIỜ
- Phụ thuộc bên ngoài thực sự đang thay đổi
- Giúp việc kiểm thử dễ dàng hơn đáng kể
- Loại bỏ sự trùng lặp đáng kể
Phục hồi:
Xóa các lớp trừu tượng tồi
Điều dũng cảm nhất bạn có thể làm là xóa mã.
Đặc biệt là “kiến trúc.”
Trước:
// 6 files, 300 linesexportinterfaceIUserValidator{}exportclassUserValidationStrategy{}exportclassUserValidationFactory{}exportclassUserValidationOrchestrator{}// ...
Sau:
// 1 file, 20 lines@Injectable()exportclassUserService{asynccreate(dto:CreateUserDto):Promise<User>{if(!dto.email.includes("@")){thrownewBadRequestException("Invalid email");}returnthis.userRepository.save(dto);}}
Đội của bạn:“Cái này tốt hơn nhiều!”
Cái tôi của bạn:“Nhưng…
kiến trúc của tôi…”
Bản thân tương lai của bạn:“Cảm ơn trời là tôi đã xóa cái đó.”
Sự thật về mã “Có thể mở rộng”
Đây là một bí mật:Mã đơn giản mở rộng tốt hơn mã “có thể mở rộng”.
Netflix không sử dụng mẫu BaseAbstractFactoryStrategyManagerProvider của bạn.
Họ sử dụng mã nhàm chán, trực tiếp giải quyết các vấn đề thực tế.
Mã “có thể mở rộng” nhất mà tôi từng thấy:
- Dễ đọc
- Có trách nhiệm rõ ràng
- Sử dụng lớp trừu tượng một cách tiết kiệm
- Có thể được hiểu bởi các nhà phát triển mới trong vài phút
Mã ít có khả năng mở rộng nhất:
- Yêu cầu bằng tiến sĩ để hiểu
- Có 47 cấp độ gián tiếp
- “Mẫu doanh nghiệp” ở khắp mọi nơi
- Khiến những thay đổi đơn giản mất hàng tuần
Triết lý
Người mới:Sao chép-dán mọi thứ
Trung cấp:Trừu tượng hóa mọi thứ
Chuyên gia:Biết khi nào không nên làm cả hai
Mục tiêu không phải là mã sạch hay kiến trúc có thể mở rộng.
Mục tiêu làgiải quyết vấn đề với độ phức tạp tối thiểu khả thi.
Công việc của bạn không phải là gây ấn tượng với các nhà phát triển khác bằng kiến thức về các mẫu thiết kế.
Đó là viết mã:
- Hoạt động
- Dễ hiểu
- Có thể thay đổi dễ dàng
- Không khiến mọi người muốn bỏ việc
Kết luận
Lần tới khi bạn sắp tạo một interface với một triển khai, hoặc xây dựng một factory cho hai trường hợp sử dụng, hoặc tạo một lớp cơ sở “phòng khi cần”, tôi muốn bạn dừng lại và hỏi:
“Tôi đang giải quyết vấn đề hay tạo ra vấn đề?”
Hầu hết các lớp trừu tượng được tạo ra vì:
- Chúng ta đọc về chúng trong sách
- Chúng có vẻ “chuyên nghiệp hơn”
- Chúng ta chán và muốn một thử thách
- Chúng ta sợ trông không tinh tế
Nhưng đây là điều:Mã tinh tế nhất là mã không tồn tại.
Hãy viết mã nhàm chán.
Sao chép-dán khi nó đơn giản hơn trừu tượng hóa.
Chờ đợi trường hợp sử dụng thứ ba.
Xóa các lớp trừu tượng thái quá.
Bản thân tương lai của bạn, đồng nghiệp của bạn, và bất kỳ ai phải bảo trì mã của bạn sẽ cảm ơn bạn.
Bây giờ hãy đi xóa một số interface.
P.S.Nếu bạn là người đã viết factory chiến lược hiển thị tên người dùng, tôi xin lỗi.
Nhưng cũng xin hãy tìm sự giúp đỡ.
Kiến trúc là món nợ.
Hãy chi tiêu một cách khôn ngoan.
Hầu hết các hệ thống không cần một khoản thế chấp.”







![[Tự học C++] Số dấu phẩy động(float, double,…) trong C++](https://cafedev.vn/wp-content/uploads/2019/12/cafedevn_c_develoment-100x70.jpg)

