Chào mừng bạn đến với thế giới của những dòng code. Một trong những sự thật trớ trêu nhất trong hành trình phát triển phần mềm là cách các lập trình viên “lên tay” sau vài tháng làm quen với các khái niệm mới. Ban đầu, các lập trình viên mới ra trường thường viết code rất đơn giản, thậm chí là quá đơn giản. Nhưng chỉ sáu tháng sau, khi họ khám phá ra thế giới huyền ảo của “Design Patterns”, code của họ bắt đầu trở nên phức tạp đến mức đòi hỏi cả bằng tiến sĩ mới có thể hiểu được. Hành trình của một lập trình viên thường diễn ra như thế này: “Khoan đã, mình có thể dùng `class` ư?” → **”MỌI THỨ PHẢI LÀ MỘT `FACTORY STRATEGY OBSERVER SINGLETON`!”**
Thực tế đã chứng minh, việc phức tạp hóa quá mức (over-engineering) là một trong những sai lầm phổ biến nhất, gây tốn kém thời gian và nguồn lực không đáng có. Mục tiêu của bài viết này là làm rõ những dấu hiệu của over-engineering, lý do nên tránh nó, và quan trọng nhất, khi nào thì trừu tượng hóa thực sự mang lại giá trị.
Mục lục
Tội Ác Chiến Tranh Mã Nguồn: Một Ví Dụ Điển Hình Về Phức Tạp Hóa Vô Lý
Hãy để tôi kể cho bạn nghe về một lần tôi kế thừa một codebase, nơi ai đó đã “thiết kế kiến trúc” cho việc hiển thị tên đầy đủ của người dùng. Dưới đây là đoạn code tôi tìm thấy:
// user-name-display-strategy.interface.ts
export interface IUserNameDisplayStrategy {
formatName(context: UserNameContext): string;
supports(type: DisplayType): boolean;
}
// user-name-context.interface.ts
export interface UserNameContext {
firstName: string;
lastName: string;
locale: string;
preferences: UserDisplayPreferences;
culturalNamingConvention: CulturalNamingConvention;
titlePrefix?: string;
suffixes?: string[];
}
// user-name-display-strategy.factory.ts
@Injectable()
export class UserNameDisplayStrategyFactory {
constructor(
@Inject("DISPLAY_STRATEGIES")
private readonly strategies: IUserNameDisplayStrategy[]
) {}
create(type: DisplayType): IUserNameDisplayStrategy {
const strategy = this.strategies.find((s) => s.supports(type));
if (!strategy) {
throw new UnsupportedDisplayTypeException(type);
}
return strategy;
}
}
// standard-user-name-display.strategy.ts
@Injectable()
export class StandardUserNameDisplayStrategy
implements IUserNameDisplayStrategy
{
supports(type: DisplayType): boolean {
return type === DisplayType.STANDARD;
}
formatName(context: UserNameContext): string {
return `${context.firstName} ${context.lastName}`;
}
}
// The module that ties this beautiful architecture together
@Module({
providers: [
UserNameDisplayStrategyFactory,
StandardUserNameDisplayStrategy,
FormalUserNameDisplayStrategy,
InformalUserNameDisplayStrategy,
{
provide: "DISPLAY_STRATEGIES",
useFactory: (...strategies) => strategies,
inject: [
StandardUserNameDisplayStrategy,
FormalUserNameDisplayStrategy,
InformalUserNameDisplayStrategy,
],
},
],
exports: [UserNameDisplayStrategyFactory],
})
export class UserNameDisplayModule {}
// Usage (hãy hít thở sâu):
const context: UserNameContext = {
firstName: user.firstName,
lastName: user.lastName,
locale: "en-US",
preferences: userPreferences,
culturalNamingConvention: CulturalNamingConvention.WESTERN,
};
const strategy = this.strategyFactory.create(DisplayType.STANDARD);
const displayName = strategy.formatName(context);
Bạn biết đoạn code “kiến trúc” hơn 200 dòng này thực sự làm gì không? Nó chỉ đơn giản là:
`${user.firstName} ${user.lastName}`;
Vâng, bạn không nghe nhầm đâu. Hơn 200 dòng code chỉ để nối hai chuỗi với một dấu cách. Lập trình viên viết đoạn này có lẽ đã khắc “Design Patterns” của Gang of Four lên… tâm trí của mình. Đây là một ví dụ điển hình của việc over-engineering, nơi sự phức tạp được tạo ra mà không giải quyết bất kỳ vấn đề thực tế nào, chỉ làm tăng gánh nặng bảo trì và khó khăn trong việc hiểu code.
Cờ Đỏ Báo Động: Những Dấu Hiệu Của Việc “Over-Engineering” Quá Mức
Việc nhận diện over-engineering là bước đầu tiên để tránh xa nó. Dưới đây là những “lá cờ đỏ” thường thấy mà các lập trình viên giàu kinh nghiệm cảnh báo.
1. Ngụy Biện “Phòng Bị Tương Lai” (The “Future-Proofing” Fallacy)
Đây là một bí mật mà các lập trình viên cần biết: Bạn không thể dự đoán tương lai, và bạn rất tệ trong việc đó.
Nhiều lập trình viên tạo ra các trừu tượng hóa phức tạp với lý do “chúng ta có thể cần đến nó trong tương lai”. Ví dụ điển hình là việc tạo `interface` cho nhiều cổng thanh toán khi bạn chỉ đang sử dụng một cổng duy nhất.
// "Chúng ta có thể cần nhiều nhà cung cấp thanh toán vào một ngày nào đó!"
export interface IPaymentGateway {
processPayment(request: PaymentRequest): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
validateCard(card: CardDetails): Promise<boolean>;
}
export interface IPaymentGatewayFactory {
create(provider: PaymentProvider): IPaymentGateway;
}
@Injectable()
export class StripePaymentGateway implements IPaymentGateway {
// 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 dù sao, chúng ta đã "sẵn sàng" cho PayPal!
}
@Injectable()
export class PaymentGatewayFactory implements IPaymentGatewayFactory {
create(provider: PaymentProvider): IPaymentGateway {
switch (provider) {
case PaymentProvider.STRIPE:
return new StripePaymentGateway();
default:
throw new Error("Unsupported payment provider");
}
}
}
Ba năm sau, khi bạn cuối cùng cũng thêm PayPal:
- Yêu cầu của bạn đã thay đổi hoàn toàn.
- API của Stripe đã phát triển.
- Trừu tượng hóa cũ không còn phù hợp với trường hợp sử dụng mới.
- Bạn vẫn phải refactor (tái cấu trúc) mọi thứ.
Thay vào đó, bạn nên viết:
@Injectable()
export class PaymentService {
constructor(private stripe: Stripe) {}
async charge(amount: number, token: string): Promise<string> {
const charge = await this.stripe.charges.create({
amount,
currency: "usd",
source: token,
});
return charge.id;
}
}
Đơn giản. Khi PayPal xuất hiện (nếu nó xuất hiện), bạn sẽ refactor với các yêu cầu thực tế, không phải những giả định bạn mơ mộng lúc 2 giờ sáng. Theo nguyên tắc KISS (Keep It Simple, Stupid), hãy giải quyết vấn đề hiện tại thay vì cố gắng đoán trước mọi kịch bản tương lai.
2. Interface Chỉ Với Một Triển Khai Thực Tế (The Interface with One Implementation)
Đây là một trong những “lỗi” yêu thích của tôi. Nó giống như mang một chiếc ô ra sa mạc “phòng khi trời mưa”.
export interface IUserService {
findById(id: string): Promise<User>;
create(dto: CreateUserDto): Promise<User>;
update(id: string, dto: UpdateUserDto): Promise<User>;
}
@Injectable()
export class UserService implements IUserService {
// Triển khai duy nhất
// Sẽ là triển khai duy nhất cho đến khi vũ trụ diệt vong
async findById(id: string): Promise<User> {
return this.userRepository.findOne({ where: { id } });
}
}
Chúc mừng, bạn đã đạt được:
- ✅ Làm cho việc chuyển đến định nghĩa trong IDE mất hai lần nhấp chuột thay vì một.
- ✅ Thêm hậu tố “Impl” vào tên lớp của bạn như thể đang ở năm 2005.
- ✅ Gây nhầm lẫn: “Khoan đã, tại sao lại có một interface ở đây?”
- ✅ Làm cho việc refactor trong tương lai khó khăn hơn (bây giờ bạn có hai thứ để cập nhật).
- ✅ Zero lợi ích thực tế.
Chỉ cần viết service đó:
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
async findById(id: string): Promise<User> {
return this.userRepository.findOne({ where: { id } });
}
}
“Nhưng còn việc kiểm thử thì sao?” TypeScript có `jest.mock()`, `sinon.stub()`. Bạn không cần một `interface` để mock các đối tượng. Interface chỉ thực sự hữu ích khi có nhiều triển khai thực tế đang được sử dụng, không phải là “có thể” được sử dụng.
3. Giải Pháp Chung Chung Không Ai Yêu Cầu (The Generic Solution Nobody Asked For)
Tham vọng tạo ra một giải pháp siêu cấp, “linh hoạt” cho mọi thứ thường dẫn đến những cỗ máy phức tạp mà không thực sự giải quyết tốt vấn đề cụ thể nào.
// "Điều này sẽ tiết kiệm RẤT NHIỀU thời gian!"
export abstract class BaseService<T, ID = string> {
constructor(protected repository: Repository<T>) {}
async findById(id: ID): Promise<T> {
const entity = await this.repository.findOne({ where: { id } });
if (!entity) {
throw new NotFoundException(`${this.getEntityName()} not found`);
}
return entity;
}
async findAll(query?: QueryParams): Promise<T[]> {
return this.repository.find(this.buildQuery(query));
}
async create(dto: DeepPartial<T>): Promise<T> {
this.validate(dto);
return this.repository.save(dto);
}
// ... các phương thức CRUD chung khác
protected abstract getEntityName(): string;
protected abstract validate(dto: DeepPartial<T>): void;
protected buildQuery(query?: QueryParams): any {
// 50 dòng logic xây dựng truy vấn "có thể tái sử dụng"
}
}
@Injectable()
export class UserService extends BaseService<User> {
constructor(userRepository: UserRepository) {
super(userRepository);
}
protected getEntityName(): string {
return "User";
}
protected validate(dto: DeepPartial<User>): void {
// Khoan đã, người dùng cần xác thực đặc biệt
if (!dto.email?.includes("@")) {
throw new BadRequestException("Invalid email");
}
// Và băm mật khẩu
// Và xác minh email
// Và... điều này không còn phù hợp với mẫu nữa
}
// Bây giờ bạn cần ghi đè một nửa các phương thức cơ sở
async create(dto: CreateUserDto): Promise<User> {
// Không thể sử dụng super.create() vì người dùng là đặc biệt
// Vì vậy bạn viết lại nó ở đây
// Đánh bại toàn bộ mục đích của lớp cơ sở
}
}
Diễn biến bất ngờ: Mọi thực thể cuối cùng đều “đặc biệt” và bạn phải ghi đè gần như mọi thứ. Lớp cơ sở trở thành một tượng đài dài 500 dòng cho thời gian bị lãng phí. Theo Martin Fowler, việc cố gắng tạo ra một giải pháp “chung” cho mọi thứ thường dẫn đến sự phức tạp không cần thiết, đặc biệt khi các yêu cầu cụ thể của từng thực thể khác biệt đáng kể.
Điều bạn nên làm:
@Injectable()
export class UserService {
constructor(
private userRepository: UserRepository,
private passwordService: PasswordService
) {}
async create(dto: CreateUserDto): Promise<User> {
if (await this.emailExists(dto.email)) {
throw new ConflictException("Email already exists");
}
const hashedPassword = await this.passwordService.hash(dto.password);
return this.userRepository.save({
...dto,
password: hashedPassword,
});
}
// Chỉ những phương thức mà người dùng thực sự cần
}
Tẻ nhạt? Đúng vậy. Dễ đọc? Cũng đúng vậy. Dễ bảo trì? Cực kỳ đúng vậy.
4. Trừu Tượng Hóa Mã Ổn Định, Gắn Kết Mã Hay Thay Đổi (Abstracting Stable Code, Coupling Volatile Code)
Đây là một trong những sai lầm nghịch lý nhất. Lập trình viên dành nhiều công sức để trừu tượng hóa những thứ ổn định, ít thay đổi, trong khi lại gắn kết chặt chẽ với những phần dễ thay đổi nhất.
// Lập trình viên: "Hãy trừu tượng hóa phép tính này!"
export interface IDiscountCalculator {
calculate(context: DiscountContext): number;
}
@Injectable()
export class PercentageDiscountCalculator implements IDiscountCalculator {
calculate(context: DiscountContext): number {
return context.price * (context.percentage / 100);
}
}
@Injectable()
export class FixedDiscountCalculator implements IDiscountCalculator {
calculate(context: DiscountContext): number {
return context.price - context.fixedAmount;
}
}
// Factory, strategy pattern, mọi thứ phức tạp
// Dành cho... các phép toán cơ bản không thay đổi từ thời Babylon cổ đại
Trong khi đó, ở cùng codebase:
@Injectable()
export class OrderService {
async processPayment(order: Order): Promise<void> {
// Gọi API Stripe được hardcode
const charge = await fetch("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 riêng của Stripe
const result = await charge.json();
order.stripeChargeId = result.id;
}
}
Hãy xem xét điều này:
- 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): Gắn kết chặt chẽ ✅
- Lựa chọn nghề nghiệp: Đáng nghi vấn ✅
Hãy làm ngược lại:
// Toán học là toán học, giữ nó đơn giản
export class DiscountCalculator {
calculatePercentage(price: number, percent: number): number {
return price * (percent / 100);
}
calculateFixed(price: number, amount: number): number {
return Math.max(0, price - amount);
}
}
// Các phụ thuộc bên ngoài cần trừu tượng hóa
export interface PaymentProcessor {
charge(amount: number, token: string): Promise<PaymentResult>;
}
@Injectable()
export class StripeProcessor implements PaymentProcessor {
async charge(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. Đây là một nguyên tắc cơ bản của thiết kế phần mềm linh hoạt, giúp hệ thống dễ dàng thích nghi với các thay đổi bên ngoài.
5. Tư Duy “Doanh Nghiệp” Rập Khuôn (The “Enterprise” Mindset)
Tôi từng chứng kiến đoạn code yêu cầu mười một file để lưu trữ tùy chọn của người dùng. Không phải tùy chọn phức tạp, chỉ đơn giản là chế độ tối (dark mode) bật/tắt.
// preference-persistence-strategy.interface.ts
export interface IPreferencePersistenceStrategy {
persist(context: PreferencePersistenceContext): Promise<void>;
}
// preference-persistence-context-builder.interface.ts
export interface IPreferencePersistenceContextBuilder {
build(params: PreferencePersistenceParameters): PreferencePersistenceContext;
}
// preference-persistence-orchestrator.service.ts
@Injectable()
export class PreferencePersistenceOrchestrator {
constructor(
private contextBuilder: IPreferencePersistenceContextBuilder,
private strategyFactory: IPreferencePersistenceStrategyFactory,
private validator: IPreferencePersistenceValidator
) {}
async orchestrate(params: PreferencePersistenceParameters): Promise<void> {
const context = await this.contextBuilder.build(params);
const validationResult = await this.validator.validate(context);
if (!validationResult.isValid) {
throw new ValidationException(validationResult.errors);
}
const strategy = this.strategyFactory.create(context.persistenceType);
await strategy.persist(context);
}
}
Điều này thực sự làm gì:
await this.userRepository.update(userId, { darkMode: true });
Tôi tin chắc người viết đoạn này được trả lương theo số dòng code. Căn bệnh này bắt nguồn từ việc đọc quá nhiều sách “kiến trúc doanh nghiệp” và nghĩ rằng càng nhiều file thì code càng tốt. Cách chữa trị là hãy tự hỏi: “Mình đang giải quyết một vấn đề thực tế hay chỉ đang đóng vai Kỹ sư phần mềm?” Đơn giản hóa quá trình là chìa khóa.
6. Trừu Tượng Hóa Non Nớt (The Premature Abstraction)
Quy tắc Ba (Rule of Three) (mà ai cũng bỏ qua):
- Viết nó một lần.
- Viết nó lại lần nữa.
- Thấy một mẫu hình? BÂY GIỜ hãy trừu tượng hóa nó.
Điều thực sự xảy ra:
- Viết nó một lần.
- “Mình CÓ THỂ cần cái này lần nữa, hãy trừu tượng hóa!”
- Tạo ra một framework.
- Trường hợp sử dụng thứ hai hoàn toàn khác biệt.
- Chống chọi với trừu tượng hóa trong 6 tháng.
- Viết lại mọi thứ.
// Endpoint API đầu tiên
@Controller("users")
export class UserController {
@Get(":id")
async getUser(@Param("id") id: string) {
return this.userService.findById(id);
}
}
// Não lập trình viên: "Mình nên tạo một controller cơ sở cho tất cả các tài nguyên!"
@Controller()
export abstract class BaseResourceController<T, CreateDto, UpdateDto> {
constructor(protected service: BaseService<T>) {}
@Get(":id")
async get(@Param("id") id: string): Promise<T> {
return this.service.findById(id);
}
@Post()
async create(@Body() dto: CreateDto): Promise<T> {
return this.service.create(dto);
}
// ... các phương thức CRUD chung khác
}
// Bây giờ mỗi controller không phù hợp với mẫu này đều là một trường hợp đặc biệt
// Người dùng cần endpoint đặt lại mật khẩu
// Sản phẩm cần tải ảnh lên
// Đơn hàng cần chuyển đổi trạng thái
// Mọi thứ đều chống lại trừu tượng hóa
Động thái thông minh:
// Viết cái đầu tiên
@Controller("users")
export class UserController {
// Triển khai đầy đủ
}
// Viết cái thứ hai
@Controller("products")
export class ProductController {
// Sao chép-dán, sửa đổi khi cần
}
// Đến cái thứ ba, NẾU có một mẫu hình rõ ràng:
// Chỉ trích xuất những phần thực sự chung
Lời khuyên: Việc trùng lặp rẻ hơn so với trừu tượng hóa sai. Bạn luôn có thể DRY (Don’t Repeat Yourself) sau này. Trừu tượng hóa non nớt giống như tối ưu hóa non nớt – nó là gốc rễ của mọi cái ác, nhưng ít thú vị hơn để đùa cợt.
Khi Nào Trừu Tượng Hóa Thực Sự Có Ý Nghĩa?
Hãy hiểu rõ, tôi không chống lại trừu tượng hóa. Tôi chống lại trừu tượng hóa ngu ngốc. Dưới đây là những trường hợp trừu tượng hóa thực sự thông minh và mang lại giá trị:
1. API Bên Ngoài Có Thể Thay Đổi (External APIs That WILL Change)
Nếu bạn đang tích hợp với các dịch vụ bên thứ ba mà bạn biết chắc chắn sẽ thay đổi hoặc bạn có kế hoạch chuyển đổi giữa các nhà cung cấp, trừu tượng hóa là cần thiết.
// Bạn thực sự sẽ chuyển từ Stripe sang PayPal vào quý tới
export interface PaymentProvider {
charge(amount: number): Promise<string>;
}
// Trừu tượng hóa này sẽ cứu bạn
2. Nhiều Triển Khai THỰC TẾ Hiện Hữu (Multiple ACTUAL Implementations)
Khi bạn có nhiều hơn một triển khai cho một hành vi nào đó đang được sử dụng trong sản phẩm của bạn ngay bây giờ, trừu tượng hóa giúp quản lý sự đa dạng đó.
// Bạn có tất cả những điều này trong sản xuất NGAY BÂY GIỜ
export interface StorageProvider {
upload(file: Buffer): Promise<string>;
}
@Injectable()
export class S3Storage implements StorageProvider {
// Được sử dụng cho các tệp tin sản xuất
}
@Injectable()
export class LocalStorage implements StorageProvider {
// Được sử dụng trong môi trường phát triển
}
@Injectable()
export class CloudinaryStorage implements StorageProvider {
// Được sử dụng cho hình ảnh
}
3. Điểm Nối Để Kiểm Thử (Testing Seams)
Trừu tượng hóa giúp tạo ra các “điểm nối” (seams) trong code, cho phép bạn thay thế các thành phần phức tạp hoặc có phụ thuộc bên ngoài bằng các mock hoặc stub trong môi trường kiểm thử, làm cho việc kiểm thử dễ dàng hơn rất nhiều.
// Giúp việc mocking dễ dàng hơn nhiều
export interface TimeProvider {
now(): Date;
}
// Kiểm thử với thời gian cố định, chạy trong sản xuất với thời gian thực
4. Hệ Thống Plugin (Plugin Systems)
Khi bạn thiết kế một hệ thống cho phép bên thứ ba mở rộng chức năng của nó thông qua các plugin, interface là một công cụ mạnh mẽ để định nghĩa hợp đồng mà các plugin phải tuân theo.
// Được thiết kế cho các tiện ích mở rộng của bên thứ ba
export interface WebhookHandler {
handle(payload: unknown): Promise<void>;
supports(event: string): boolean;
}
// Các lập trình viên có thể thêm Slack, Discord, các trình xử lý tùy chỉnh
Danh Sách Kiểm Tra: Có Nên Trừu Tượng Hóa Không?
Trước khi tạo một trừu tượng hóa, hãy tự hỏi bản thân:
🚨 DỪNG LẠI ngay nếu bạn trả lời “không” cho những câu hỏi này:
- Bạn có 2+ trường hợp sử dụng THỰC TẾ ngay bây giờ không?
- Điều này có cô lập một thứ gì đó thường xuyên thay đổi không?
- Một lập trình viên mới có hiểu tại sao điều này tồn tại không?
- Bạn có đang giải quyết một vấn đề thực tế bạn đang gặp PHẢI HÔM NAY không?
🛑 CHẮC CHẮN DỪNG LẠI nếu những điều này là đúng:
- “Chúng ta có thể cần cái này vào một ngày nào đó.”
- “Nó chuyên nghiệp hơn.”
- “Mình đã đọc về mẫu này.”
- “Nó có khả năng mở rộng tốt hơn.”
- “Các ứng dụng doanh nghiệp làm theo cách này.”
✅ BẬT ĐÈN XANH nếu:
- Nhiều triển khai tồn tại NGAY BÂY GIỜ.
- Phụ thuộc bên ngoài mà thực sự đang thay đổi.
- Làm cho việc kiểm thử dễ dàng hơn đáng kể.
- Loại bỏ sự trùng lặp đáng kể.
Hồi Phục Sau Sai Lầm: Mạnh Dạn Xóa Bỏ Các Trừu Tượng Hóa Tệ
Điều dũng cảm nhất bạn có thể làm là xóa code. Đặc biệt là “kiến trúc”. Không có gì giải phóng code base bằng việc loại bỏ sự phức tạp không cần thiết.
Trước:
// 6 files, 300 dòng
export interface IUserValidator {}
export class UserValidationStrategy {}
export class UserValidationFactory {}
export class UserValidationOrchestrator {}
// ...
Sau:
// 1 file, 20 dòng
@Injectable()
export class UserService {
async create(dto: CreateUserDto): Promise<User> {
if (!dto.email.includes("@")) {
throw new BadRequestException("Invalid email");
}
return this.userRepository.save(dto);
}
}
Đội ngũ của bạn: “Tuyệt vời hơn rất nhiều!”
Cái tôi của bạn: “Nhưng… kiến trúc của mình…”
Bản thân bạn trong tương lai: “Tạ ơn trời tôi đã xóa nó.”
Việc xóa bỏ code không cần thiết không chỉ giúp giảm kích thước codebase mà còn cải thiện khả năng đọc, giảm thiểu lỗi và tăng tốc độ phát triển.
Sự Thật Về “Mã Nguồn Có Khả Năng Mở Rộng” (Scalable Code)
Đây là một bí mật: Mã đơn giản có khả năng mở rộng tốt hơn mã được gọi là “có khả năng mở rộng”.
Netflix không sử dụng mẫu `BaseAbstractFactoryStrategyManagerProvider` của bạn. Họ sử dụng code đơn giản, dễ hiểu, giải quyết các vấn đề thực tế. Các kỹ sư tại Netflix, Google hay Amazon thường ưu tiên sự đơn giản và tính trực quan để đảm bảo hàng nghìn kỹ sư có thể cùng đóng góp mà không bị sa lầy vào những cỗ máy phức tạp không cần thiết.
Mã “có khả năng mở rộng” nhất tôi từng thấy:
- Rất dễ đọc.
- Có trách nhiệm rõ ràng.
- Sử dụng trừu tượng hóa một cách tiết kiệm.
- Có thể được hiểu bởi các lập trình viên mới trong vài phút.
Mã ít có khả năng mở rộng nhất:
- Đòi hỏi bằng tiến sĩ để hiểu.
- Có 47 tầng gián tiếp.
- “Mẫu thiết kế doanh nghiệp” ở khắp mọi nơi.
- Khiến những thay đổi đơn giản mất vài tuần.
Triết Lý Về Sự Đơn Giản Trong Lập Trình
Để trở thành một lập trình viên giỏi, bạn cần hiểu rõ các giai đoạn phát triển:
- Người mới bắt đầu (Novices): Sao chép-dán mọi thứ.
- Người trung cấp (Intermediates): Trừu tượng hóa mọi thứ.
- Chuyên gia (Experts): Biết khi nào nên làm cả hai.
Mục tiêu không phải là code sạch hay kiến trúc có khả năng mở rộng. Mục tiêu là giải quyết vấn đề với mức độ 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 lập trình viên khác bằng kiến thức về design patterns. Đó là viết code:
- 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.
Triết lý “Less is More” (Ít hơn là nhiều hơn) hay “You Ain’t Gonna Need It” (YAGNI) là kim chỉ nam cho việc phát triển phần mềm hiệu quả.
Kết Luận
Lần tới khi bạn định tạo một `interface` chỉ với một `implementation`, 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:
“Mình đang giải quyết một vấn đề hay tạo ra một vấn đề?”
Hầu hết các trừu tượng hóa được tạo ra vì:
- Chúng ta đọc về chúng trong một cuốn sách.
- Chúng có vẻ “chuyên nghiệp” hơn.
- Chúng ta đang buồn chán và muốn thử thách.
- Chúng ta sợ trông kém tinh tế.
Nhưng đây là điều quan trọng: Code tinh tế nhất là code không tồn tại.
Hãy viết code nhàm chán. Hãy sao chép-dán khi nó đơn giản hơn việc trừu tượng hóa. Hãy đợi đến trường hợp sử dụng thứ ba. Hãy xóa bỏ những trừu tượng hóa quá mức.
Bản thân bạn trong tương lai, đồng nghiệp của bạn, và bất kỳ ai phải bảo trì code của bạn sẽ cảm ơn bạn.
Bây giờ, hãy đi xóa một vài `interface` đi.
—
P.S. Nếu bạn là người đã viết `user name display strategy factory` đó, tôi xin lỗi. Nhưng cũng xin hãy tìm sự trợ giúp.
Kiến trúc là một khoản nợ. Hãy chi tiêu nó 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.



