Ngừng Over-Engineering: Bạn Không Đang Xây Netflix Đâu

Chắc ai cũng từng thấy. Một pull request biến việc nối chuỗi đơn giản thành 200 dòng code Strategy/Factory pattern. Một interface chỉ có duy nhất một implementation "phòng khi cần." Một BaseService<T> mà mọi service đều extend nhưng override gần hết mọi method.
Bài viết này được lấy cảm hứng từ bài viết viral của Adam "You're Not Building Netflix: Stop Coding Like You Are", bài viết nắm bắt hoàn hảo một vấn đề phổ biến trong mọi team: over-engineering.
Bạn Sẽ Học Được Gì
✅ Tại sao lập trình viên hay over-engineer và những bẫy tâm lý đằng sau
✅ Sáu dấu hiệu cảnh báo cho thấy sự phức tạp không cần thiết
✅ Khi nào abstraction thực sự giúp ích và khi nào nó gây hại
✅ Framework thực tế để quyết định khi nào nên abstract
✅ Cách viết code đơn giản mà không hy sinh chất lượng
Hàm Format Tên 200 Dòng
Hãy bắt đầu với ví dụ đã truyền cảm hứng cho bài viết của Adam. Ai đó đã viết đoạn code này để format họ tên đầy đủ của người dùng:
// Cách tiếp cận "enterprise"
interface NameFormattingStrategy {
format(first: string, last: string): string;
}
class FirstLastStrategy implements NameFormattingStrategy {
format(first: string, last: string): string {
return `${first} ${last}`;
}
}
class LastFirstStrategy implements NameFormattingStrategy {
format(first: string, last: string): string {
return `${last}, ${first}`;
}
}
class NameFormatterFactory {
static create(type: 'firstLast' | 'lastFirst'): NameFormattingStrategy {
switch (type) {
case 'firstLast':
return new FirstLastStrategy();
case 'lastFirst':
return new LastFirstStrategy();
default:
throw new Error(`Unknown format: ${type}`);
}
}
}
// Sử dụng
const formatter = NameFormatterFactory.create('firstLast');
const fullName = formatter.format('John', 'Doe');Và đây là những gì đoạn code thực sự cần:
// Nên viết thế này
const fullName = `${firstName} ${lastName}`;Một dòng. Không pattern. Không factory. Không strategy. Chỉ làm đúng việc nó cần làm.
Đây không phải ví dụ bịa đặt — các biến thể của nó tồn tại trong các codebase production thực tế ở khắp mọi nơi.
Sáu Dấu Hiệu Cảnh Báo Của Over-Engineering
Dấu Hiệu #1: "Future-Proofing" Không Bao Giờ Đền Đáp
"Chúng ta nên abstract payment gateway ngay bây giờ, để sau này khi thêm PayPal..."
Đây là lý do phổ biến nhất để biện minh cho abstraction quá sớm. Vấn đề là: bạn không thể dự đoán tương lai.
Ba năm sau, khi yêu cầu PayPal thực sự đến:
- Yêu cầu của bạn đã thay đổi hoàn toàn
- Abstraction không phù hợp với use case mới
- Bạn đã duy trì sự phức tạp không dùng đến trong nhiều năm
- Developer viết nó đã nghỉ việc
// Thứ bạn xây "phòng khi cần"
interface PaymentGateway {
charge(amount: number, currency: string): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
getStatus(transactionId: string): Promise<TransactionStatus>;
}
class StripeGateway implements PaymentGateway {
// Implementation duy nhất trong 3 năm...
}
// Thứ bạn thực sự cần khi PayPal đến
// - Mô hình authentication khác
// - Trạng thái dựa trên webhook thay vì polling
// - Xử lý subscription (Stripe ban đầu không cần)
// - Yêu cầu chuyển đổi tiền tệ
// Abstraction không phù hợp. Bạn phải viết lại dù sao.Nguyên tắc YAGNI (You Aren't Gonna Need It — Bạn Sẽ Không Cần Nó Đâu) tồn tại chính vì lý do này. Xây dựng cho yêu cầu hôm nay. Refactor khi yêu cầu ngày mai thực sự đến.
Dấu Hiệu #2: Interface Chỉ Có Một Implementation
// Cái này không thêm giá trị gì
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
class UserRepositoryImpl implements UserRepository {
findById(id: string): Promise<User> { /* ... */ }
save(user: User): Promise<void> { /* ... */ }
}Bạn được gì?
- Khiến "jump to definition" trong IDE phải click hai lần thay vì một
- Thêm một file phải luôn đồng bộ với implementation
- Tạo ảo giác về tính linh hoạt mà thực tế không có
Nếu bạn chỉ có một implementation duy nhất, bạn không cần interface. Khi implementation thứ hai thực sự xuất hiện, lúc đó hãy extract interface. Mất năm phút — không phải năm tháng mang theo dead weight.
Dấu Hiệu #3: Base Class "Một Cỡ Cho Tất Cả"
abstract class BaseService<T> {
abstract findAll(): Promise<T[]>;
abstract findById(id: string): Promise<T>;
abstract create(data: Partial<T>): Promise<T>;
abstract update(id: string, data: Partial<T>): Promise<T>;
abstract delete(id: string): Promise<void>;
}
class UserService extends BaseService<User> {
// Override mọi thứ vì user có logic auth đặc biệt
async findAll() { /* hoàn toàn khác */ }
async create(data: Partial<User>) { /* cần xác minh email */ }
async delete(id: string) { /* soft delete, không phải hard delete */ }
}
class ProductService extends BaseService<Product> {
// Override mọi thứ vì product có logic inventory
async create(data: Partial<Product>) { /* cần kiểm tra tồn kho */ }
async update(id: string, data: Partial<Product>) { /* cần lịch sử giá */ }
}Khi mọi subclass override hầu hết các method của base class, base class không mang lại giá trị — nó mang lại cấu trúc giả. Bạn tốn nhiều thời gian làm việc xung quanh abstraction hơn là hưởng lợi từ nó.
Dấu Hiệu #4: Abstract Sai Thứ
Đây là sai lầm tinh vi nhưng nghiêm trọng: lập trình viên thường abstract code ổn định trong khi coupling chặt code hay thay đổi.
// ❌ Sai: Abstract phép tính ổn định (cái này không bao giờ thay đổi)
interface CalculationStrategy {
calculate(price: number, taxRate: number): number;
}
class StandardCalculation implements CalculationStrategy {
calculate(price: number, taxRate: number): number {
return price * (1 + taxRate); // Công thức này không thay đổi
}
}
// ❌ Sai: Coupling chặt external API hay thay đổi (cái này SẼ thay đổi)
class OrderService {
async createOrder(items: CartItem[]) {
const response = await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { 'Authorization': `Bearer ${STRIPE_KEY}` },
body: JSON.stringify({ /* payload riêng của Stripe */ })
});
// Xử lý response riêng của Stripe nhúng thẳng vào business logic
}
}Phép tính thuế sẽ không bao giờ thay đổi — price * (1 + taxRate) là toán học. Nhưng Stripe API? Nó thay đổi với mỗi lần nâng version, và nó đang nhúng sâu vào business logic.
Abstract thứ có khả năng thay đổi. Để code ổn định đơn giản.
Dấu Hiệu #5: Giải Quyết Vấn Đề Chưa Tồn Tại
// Xây plugin system cho tool nội bộ của team
interface Plugin {
name: string;
version: string;
initialize(): Promise<void>;
execute(context: PluginContext): Promise<PluginResult>;
teardown(): Promise<void>;
}
class PluginRegistry {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin) { /* ... */ }
unregister(name: string) { /* ... */ }
executeAll(context: PluginContext) { /* ... */ }
}
// Tổng số plugin từng được viết: 1
// Tổng số plugin dự kiến: "có thể một ngày nào đó"Nếu chỉ có một plugin, thì không có plugin system — chỉ có code với thêm vài bước phức tạp.
Dấu Hiệu #6: Dùng Pattern Như Trang Trí CV
Hãy thành thật: bạn đã bao giờ dùng design pattern vì nó cảm thấy ấn tượng hơn là vì bài toán đòi hỏi chưa?
- Dùng Observer pattern cho một event duy nhất giữa hai component
- Implement Command pattern cho các thao tác CRUD cơ bản
- Xây State Machine cho một boolean toggle
- Thêm Mediator cho hai service nói chuyện trực tiếp với nhau
Design pattern là giải pháp cho các vấn đề lặp đi lặp lại, không phải trang trí cho codebase. Nếu vấn đề không lặp lại, pattern chính là overhead.
Khi Nào Abstraction Thực Sự Có Ý Nghĩa
Đây không phải bài viết chống pattern. Abstraction là một trong những công cụ mạnh mẽ nhất trong software engineering — khi dùng đúng thời điểm.
1. External API Sẽ Thay Đổi
// ✅ Tốt: Wrap service bên thứ ba mà bạn có thể thay thế
class EmailService {
private provider: SendGrid; // Có thể thành SES, Mailgun, v.v.
async send(to: string, subject: string, body: string) {
return this.provider.send({ to, subject, body });
}
}Service bên thứ ba thay đổi giá, deprecate tính năng, hoặc ngừng hoạt động. Một wrapper mỏng tạo ra một seam hữu ích.
2. Nhiều Implementation Đang Chạy Production Hôm Nay
// ✅ Tốt: Bạn thực sự có nhiều kênh notification NGAY BÂY GIỜ
interface NotificationChannel {
send(user: User, message: string): Promise<void>;
}
class EmailNotification implements NotificationChannel { /* ... */ }
class SlackNotification implements NotificationChannel { /* ... */ }
class SMSNotification implements NotificationChannel { /* ... */ }Từ khóa là hôm nay. Không phải "một ngày nào đó." Không phải "quý 3." Hôm nay, trên production, với traffic thật.
3. Testing Seam
// ✅ Tốt: Abstraction giúp test được
class OrderProcessor {
constructor(private clock: () => Date = () => new Date()) {}
isOrderExpired(order: Order): boolean {
const now = this.clock();
return order.createdAt < new Date(now.getTime() - 24 * 60 * 60 * 1000);
}
}
// Trong test: inject clock cố định
const processor = new OrderProcessor(() => new Date('2026-01-15'));Nếu một abstraction giúp code của bạn test được trong khi trước đó không thể, đó là giá trị thực sự.
4. Pattern Lặp Lại Ở 3+ Nơi
Quy tắc Ba (Rule of Three): đừng abstract cho đến khi bạn thấy cùng một pattern ít nhất ba lần. Lúc đó, bạn có bằng chứng thực sự về sự trùng lặp — không phải giả thuyết.
Framework Quyết Định
Trước khi thêm abstraction, hãy tự hỏi:
Nếu bạn không thể trả lời "có" cho ít nhất một trong số này, có lẽ bạn chưa cần abstraction.
Chi Phí Thực Sự Của Over-Engineering
Over-engineering không chỉ là vấn đề thẩm mỹ. Nó có hậu quả thực sự:
| Chi phí | Tác động |
|---|---|
| Thời gian onboard | Developer mới mất nhiều ngày hiểu các pattern không cần thiết |
| Gánh nặng bảo trì | Nhiều file hơn, nhiều interface hơn, nhiều thứ phải đồng bộ hơn |
| Khó debug | Theo dõi một thao tác đơn giản qua 7 tầng abstraction |
| Tự tin giả | "Chúng ta có clean architecture" trong khi vấn đề thực sự không được giải quyết |
| Khó refactor | Càng nhiều abstraction, càng khó thay đổi hướng đi |
Mỗi dòng code là một gánh nặng. Mỗi tầng abstraction là một khoản thuế mà developer sau bạn phải trả. Hãy chắc chắn rằng giá trị nó mang lại xứng đáng với cái giá đó.
Code Đơn Giản Trông Như Thế Nào
Đây là sự thay đổi tư duy. Thay vì hỏi "mình nên dùng pattern nào?", hãy hỏi "cách đơn giản nhất mà hoạt động là gì?"
// ❌ Over-engineered
class UserNameBuilder {
private parts: string[] = [];
addFirstName(name: string): UserNameBuilder {
this.parts.push(name);
return this;
}
addLastName(name: string): UserNameBuilder {
this.parts.push(name);
return this;
}
build(): string {
return this.parts.join(' ');
}
}
const name = new UserNameBuilder()
.addFirstName('John')
.addLastName('Doe')
.build();// ✅ Đơn giản
const name = `${firstName} ${lastName}`;// ❌ Over-engineered
const config = ConfigurationFactory
.getInstance()
.getConfigurationProvider('database')
.getConfiguration();
// ✅ Đơn giản
const config = loadConfig('database');Ba dòng code rõ ràng luôn luôn tốt hơn một premature abstraction. Bạn có thể đọc, debug, và thay đổi nó mà không cần class diagram.
Triết Lý Thực Tế
Bài viết của Adam đưa ra một triết lý mà tôi hoàn toàn đồng ý:
Viết code giải quyết bài toán hôm nay. Khi bài toán ngày mai đến — nếu nó đến — hãy refactor với yêu cầu thực tế, không phải những yêu cầu giả thuyết bạn nghĩ ra lúc 2 giờ sáng.
Điều này không có nghĩa là "không bao giờ abstract." Nó có nghĩa là:
- Bắt đầu đơn giản. Bạn luôn có thể thêm phức tạp sau.
- Refactor khi nhu cầu thực sự. Implementation thứ hai là lúc bạn extract interface.
- Xóa abstraction không dùng. Nếu không ai dùng, nó không phải "future-proof" — nó là dead code.
- Coi trọng tính dễ đọc hơn sự thông minh. Developer tiếp theo đọc code của bạn sẽ cảm ơn bạn.
Code tốt nhất không phải là code kiến trúc phức tạp nhất. Nó là code giải quyết vấn đề rõ ràng, dễ thay đổi, và không làm khó người tiếp theo.
Tóm Tắt Và Bài Học Chính
✅ Strategy/Factory pattern để nối chuỗi là thứ thực sự tồn tại trong các codebase
✅ "Future-proofing" gần như không bao giờ hiệu quả — yêu cầu thay đổi khó lường
✅ Interface chỉ có một implementation thêm phức tạp mà không có giá trị
✅ Abstract thứ hay thay đổi (external API), giữ code ổn định đơn giản (toán học, business rule)
✅ Dùng Quy tắc Ba: đừng abstract cho đến khi thấy pattern 3+ lần
✅ Mỗi abstraction là một khoản thuế — hãy chắc chắn giá trị vượt chi phí
Bạn không đang xây Netflix. Bạn không đang xây Google. Và dù nếu bạn đang xây — họ cũng không bắt đầu với microservices và 47 design pattern. Họ bắt đầu đơn giản, và scale khi cần thiết.
Bắt đầu đơn giản. Giữ đơn giản. Chỉ thêm phức tạp khi thực tế đòi hỏi.
Tham Khảo
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.