DEV Community

OCP — Open Close Principle

Open-Closed Principle: thêm tính năng mà không sửa code đang chạy

Open-Closed Principle nói code nên mở để mở rộng, đóng để sửa đổi — thêm hành vi mới bằng cách thêm code, không bằng cách sửa code đã hoạt động và đã test. Lý do thực tế: mỗi lần sửa một khối logic trung tâm là một lần có nguy cơ làm hỏng các nhánh đang chạy. Một chuỗi if/else hay switch cứ phình ra mỗi khi có loại mới là vi phạm OCP điển hình — và là nơi bug hồi quy hay sinh ra. Strategy pattern và polymorphism là cách hiện thực OCP: mỗi loại mới là một class mới, code điều phối không đổi.

Cơ chế hoạt động

So sánh hai cách thêm một phương thức thanh toán. Cách vi phạm OCP — sửa hàm trung tâm mỗi lần:

function pay(method: string, amount: number) {
  if (method === 'card') { /* ... */ }
  else if (method === 'paypal') { /* ... */ }
  else if (method === 'crypto') { /* thêm loại mới = sửa hàm này, test lại tất cả */ }
}
Enter fullscreen mode Exit fullscreen mode

Cách theo OCP — thêm loại mới là thêm class, hàm điều phối đóng:

interface PaymentStrategy { pay(amount: number): Promise<Receipt> }

class CardPayment implements PaymentStrategy { async pay(a) { /* ... */ } }
class PaypalPayment implements PaymentStrategy { async pay(a) { /* ... */ } }
// thêm crypto = thêm một class mới, KHÔNG đụng processor

class PaymentProcessor {
  constructor(private strategies: Map<string, PaymentStrategy>) {}
  process(method: string, amount: number) {
    const s = this.strategies.get(method)
    if (!s) throw new UnsupportedPaymentError(method)
    return s.pay(amount)
  }
}
Enter fullscreen mode Exit fullscreen mode

PaymentProcessor không bao giờ phải sửa khi thêm phương thức — chỉ đăng ký thêm một strategy. Code đã test giữ nguyên.

Vấn đề gặp trong production

Failure mode: switch phình gây bug hồi quy. Mỗi lần thêm một nhánh vào một switch lớn, mọi nhánh cũ bị đặt vào rủi ro: vô tình rơi qua case (thiếu break), đụng biến dùng chung, hay logic mới phá thứ tự xử lý. Càng nhiều loại, hàm càng dài và càng dễ vỡ. Đây là lý do thực tế khiến OCP đáng giá: tách mỗi loại ra class riêng nghĩa là thêm loại mới không thể làm hỏng loại cũ, vì không đụng vào code của chúng.

Failure mode: vẫn còn một switch ở rìa. OCP đẩy sự phân nhánh ra rìa hệ thống — đâu đó vẫn phải ánh xạ một chuỗi/loại đầu vào sang strategy đúng (đăng ký vào Map, hay một factory). Điều quan trọng là điểm phân nhánh đó nhỏ và ổn định (chỉ ánh xạ tên → strategy), còn logic của từng loại thì đóng gói riêng. Nếu logic phân nhánh vẫn nằm rải trong hàm xử lý thì chưa đạt OCP, chỉ mới đổi chỗ.

Failure mode: plugin complexity — over-engineering. Áp OCP cho thứ không bao giờ thay đổi là phí. Tạo một hệ strategy/plugin đầy đủ cho hai trường hợp cố định, không có dấu hiệu sẽ thêm loại, chỉ thêm tầng trừu tượng và file mà không lợi ích. OCP đáng giá ở những điểm biến thiên đã biết — chỗ mà kinh nghiệm cho thấy loại mới sẽ liên tục được thêm (phương thức thanh toán, định dạng export, kênh thông báo). Chỗ ổn định thì một if đơn giản tốt hơn.

Cách debug và monitor

Dấu hiệu cần áp OCP: một switch/if-else cùng chủ đề xuất hiện ở nhiều nơi (xử lý, hiển thị, validate cùng một "loại" ở ba hàm khác nhau) — thêm loại mới phải sửa tất cả, dễ sót một chỗ. Lịch sử git cho thấy cùng một hàm bị sửa mỗi lần thêm "loại" là bằng chứng vi phạm. Sau khi refactor sang strategy, một loại mới chỉ nên là một file mới + một dòng đăng ký; nếu vẫn phải sửa nhiều nơi thì sự phân nhánh chưa được gom đúng. Ngược lại, một hệ plugin cho thứ chưa từng thêm loại nào trong nhiều tháng là dấu hiệu over-engineer.

Tradeoff

OCP làm hệ thống an toàn khi mở rộng — thêm hành vi không đụng code đã test, nên không gây hồi quy cho phần đang chạy, và mỗi loại cô lập dễ test riêng. Cái giá là nhiều abstraction và file hơn, và một lớp gián tiếp (interface + đăng ký) khiến đọc luồng phải nhảy qua vài chỗ. Quy tắc thực tế: áp OCP tại các điểm biến thiên đã biết sẽ tiếp tục mở rộng (loại thanh toán, format, kênh), nơi lợi ích "thêm không sửa" là thật; với logic ổn định không mọc thêm loại, giữ if/switch đơn giản và đừng tạo plugin. Trừu tượng đúng chỗ, không phải mọi chỗ.

Câu hỏi phỏng vấn

Cho một ví dụ về OCP, và làm sao biết khi nào nên áp dụng nó?

OCP: code mở để mở rộng, đóng để sửa đổi — thêm hành vi bằng cách thêm code mới chứ không sửa code đã chạy và đã test. Ví dụ điển hình là thanh toán: thay vì một hàm pay với switch trên loại (card/paypal/crypto) phải sửa mỗi lần thêm loại, định nghĩa interface PaymentStrategy và mỗi phương thức là một class implement nó; PaymentProcessor tra strategy theo tên và gọi — thêm crypto chỉ là thêm một class và một dòng đăng ký, không đụng processor. Biết khi nào áp dụng: ở các điểm biến thiên đã biết nơi loại mới sẽ liên tục được thêm và một switch đang phình ra ở nhiều nơi cùng lúc (xử lý, hiển thị, validate). Điểm ăn điểm: cảnh báo over-engineering — đừng dựng hệ plugin cho logic ổn định không mọc thêm loại; và lưu ý vẫn còn một điểm ánh xạ tên→strategy ở rìa, nó nên nhỏ và ổn định.

Hands-on

Lấy một hàm thật có switch/if-else trên một "loại" và đang phình (ví dụ xử lý thanh toán, hoặc export ra nhiều định dạng CSV/PDF/XLSX), rồi refactor sang strategy pattern: một interface, mỗi loại một class, một processor tra theo Map. Thêm một loại mới sau khi refactor và xác nhận chỉ phải thêm một file + một dòng đăng ký, không đụng processor đã test. Tìm xem cùng loại phân nhánh đó có lặp ở nơi khác không (validate/hiển thị) và gom lại. Cuối cùng, lấy một chỗ logic ổn định chỉ có hai trường hợp cố định và cố tình "OCP hóa" nó để cảm nhận over-engineering, rồi đối chiếu với việc giữ if đơn giản.

Top comments (0)