OCP (Open/Closed Principle) là nguyên tắc thứ hai của SOLID, quy định: các thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi. Nguyên tắc này, được Bertrand Meyer xây dựng vào năm 1988, cho phép thêm chức năng mới mà không thay đổi mã hiện có. Theo cuốn sách của Robert Martin Clean Architecture (2017), nguyên tắc mở được triển khai thông qua trừu tượng hóa và đa hình, giảm thiểu rủi ro lỗi hồi quy.
Các Điểm Chính
OCP (Open/Closed Principle) — nguyên tắc mở để mở rộng và đóng để sửa đổi. Các lớp, module và hàm nên được thiết kế để có thể thêm hành vi mới mà không thay đổi mã nguồn của chúng. Việc mở rộng đạt được thông qua kế thừa, hợp thành hoặc thay thế các triển khai giao diện.
Bertrand Meyer trong cuốn sách Object-Oriented Software Construction (1988) lần đầu tiên mô tả OCP thông qua kế thừa: lớp cơ sở không thay đổi, trong khi các lớp con mở rộng hành vi của nó. Cách diễn giải hiện đại của OCP, do Robert Martin đề xuất, dựa trên đa hình và các giao diện: thay vì kế thừa, các hợp đồng trừu tượng được sử dụng.
Sự khác biệt giữa các cách tiếp cận là đáng kể. Kế thừa tạo ra sự liên kết chặt chẽ giữa lớp cơ sở và lớp dẫn xuất. Các giao diện và hợp thành mang lại sự linh hoạt: việc triển khai có thể được thay đổi mà không cần sửa đổi mã client. OCP hiện đại nói về trừu tượng hóa, không phải kế thừa.
OCP đa hình sử dụng các lớp trừu tượng hoặc giao diện để định nghĩa một hợp đồng. Mã client làm việc với trừu tượng mà không biết triển khai cụ thể. Chức năng mới được thêm bằng cách tạo một lớp mới triển khai cùng một giao diện — mà không thay đổi một dòng mã hiện có. Điều này làm cho hệ thống chống chịu với thay đổi và dễ dự đoán khi mở rộng.
Trong phát triển di động, cách tiếp cận này phổ biến: mẫu Strategy cho phép trao đổi các thuật toán (nén ảnh, lưu đệm, xác thực) thông qua một giao diện duy nhất. Thêm một chiến lược mới không yêu cầu thay đổi mã sử dụng nó.
Triển khai OCP bắt đầu bằng cách cô lập hành vi có thể thay đổi vào một trừu tượng. Nếu có cấu trúc switch hoặc chuỗi if-else kiểm tra kiểu của một đối tượng trong mã — đó là tín hiệu để áp dụng OCP. Mỗi nhánh điều kiện có khả năng yêu cầu thêm một nhánh mới khi mở rộng.
Quy trình tái cấu trúc theo OCP bao gồm ba bước: xác định khía cạnh có thể thay đổi (những gì có thể mở rộng), cô lập nó vào một giao diện hoặc lớp trừu tượng, viết lại mã client để làm việc với trừu tượng thay vì lớp cụ thể. Sau đó, chức năng mới được thêm mà không thay đổi client.
Một làm rõ quan trọng: đóng để sửa đổi không tuyệt đối. Nếu một thay đổi yêu cầu ảnh hưởng đến chính trừu tượng hoặc hợp đồng — thì sự thay đổi là không thể tránh khỏi. OCP bảo vệ chống lại các thay đổi trong triển khai, không phải trong hợp đồng. Thiết kế tốt giả định rằng các hợp đồng ổn định và các triển khai có thể thay đổi.
Khi đánh giá khả năng tương thích OCP của một kiến trúc, rất hữu ích khi xem xét các điểm mở rộng. Mỗi điểm mà nhà phát triển thêm if-else hoặc switch cho một kiểu mới là ứng viên cho trừu tượng hóa. Một hệ thống được thiết kế theo OCP có các điểm mở rộng có thể dự đoán: các giao diện với tài liệu hướng dẫn "triển khai giao diện này để thêm một kiểu mới". Trong Android, mẫu Factory kết hợp với ViewModelProvider.Factory là một ví dụ rõ ràng — thêm một kiểu ViewModel mới không yêu cầu thay đổi các factory hiện có.
Các mẫu hiệu quả nhất để tuân thủ OCP trong phát triển di động bao gồm Strategy, Template Method, Decorator và Factory. Mỗi mẫu giải quyết vấn đề mở rộng hành vi mà không sửa đổi mã hiện có thông qua các cơ chế thiết kế hướng đối tượng khác nhau.
Strategy cho phép trao đổi các thuật toán một cách linh hoạt thông qua một giao diện chung. Trong phát triển iOS, các chiến lược được sử dụng cho hoạt ảnh và xác thực biểu mẫu. Template Method định nghĩa khung xương của một thuật toán trong lớp cơ sở, và các lớp con ghi đè các bước — phù hợp cho các màn hình có cấu trúc chung nhưng nội dung khác nhau.
Decorator thêm hành vi động vào một đối tượng mà không thay đổi lớp của nó. Trong Android, Decorator được sử dụng để bọc một Repository bằng lớp lưu đệm hoặc ghi nhật ký. Factory Method tạo các đối tượng thông qua một giao diện, cho phép các lớp con quyết định lớp nào sẽ khởi tạo — nền tảng của việc tạo phụ thuộc tương thích OCP.
Việc chọn mẫu phụ thuộc vào độ ổn định của hành vi đang được mở rộng. Strategy là tối ưu khi các thuật toán được thay thế hoàn toàn. Template Method — khi cấu trúc cố định nhưng các bước thay đổi. Decorator — khi việc mở rộng nên minh bạch với client. Đối với hầu hết các kịch bản trong Android và iOS, Strategy + tiêm phụ thuộc là đủ.
Áp dụng các mẫu này mà không có OCP về mặt kỹ thuật là có thể nhưng mất đi ý nghĩa. Chính OCP biện minh cho việc tại sao chúng ta giới thiệu một mức trừu tượng bổ sung: để hệ thống có thể phát triển mà không cần viết lại mã hiện có.
Hãy xem xét một ví dụ Android với xử lý thanh toán. Không có OCP, mỗi hệ thống thanh toán mới yêu cầu thay đổi trong lớp xử lý. Với OCP, một triển khai giao diện mới được thêm mà không sửa đổi mã hiện có.
// Vi phạm OCP: switch yêu cầu sửa đổi khi thêm hệ thống mới
class BadPaymentProcessor {
fun process(type: String) {
when (type) {
"card" -> // xử lý thẻ
"paypal" -> // xử lý PayPal
}
}
}
// Thiết kế tương thích OCP
interface PaymentMethod {
fun pay(amount: Double)
}
class CardPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
class PayPalPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
// Hệ thống mới — lớp mới, không thay đổi mã hiện có
class ApplePayPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
Một ví dụ iOS với xác thực trường văn bản thể hiện cùng một logic thông qua các giao thức Swift:
// Xác thực tương thích OCP
protocol ValidationRule {
func validate(_ input: String) -> Bool
}
struct EmailRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.contains("@")
}
}
struct PhoneRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count == 11
}
}
// Thêm quy tắc mới không yêu cầu thay đổi mã trình xác thực
struct PasswordRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count >= 8
}
}
Lợi ích chính của OCP trong các ví dụ này: thêm ApplePay hoặc PasswordRule không yêu cầu sửa đổi các lớp hiện có. Mã mở rộng theo chiều ngang — thông qua các tệp mới, không phải bằng cách thay đổi các tệp cũ. Điều này giảm rủi ro hồi quy và tăng tốc triển khai chức năng mới.
Vi phạm phổ biến nhất là cấu trúc switch hoặc when dựa trên kiểu đối tượng. Mỗi lần thêm một kiểu mới, phải tìm tất cả các switch đó trong mã và thêm một nhánh mới. Một switch bị bỏ sót là lỗi thực thi khó phát hiện ở thời điểm biên dịch.
Trong phát triển di động, OCP bị vi phạm khi sử dụng các lớp enum khổng lồ với các phương thức phụ thuộc vào giá trị enum. Thêm một phần tử enum mới yêu cầu thay đổi mọi switch trong toàn bộ dự án. Giải pháp thay thế là đa hình thông qua một giao diện, nơi mỗi kiểu triển khai hành vi riêng của nó.
Một vi phạm điển hình khác là God Adapter: RecyclerView.Adapter (Android) hoặc UITableViewDataSource (iOS) xử lý các kiểu ô khác nhau thông qua if-else. Mỗi kiểu ô mới yêu cầu mở rộng adapter. Giải pháp là một ViewHolder đa hình với phương thức bind chung, nơi mỗi kiểu ô chịu trách nhiệm hiển thị riêng của nó.
Các biện pháp phòng ngừa bao gồm: tránh switch dựa trên kiểu để chuyển sang đa hình, tiêm phụ thuộc thông qua các giao diện và sử dụng mẫu Factory để tạo đối tượng theo cấu hình. Phân tích mã để tìm "công tắc theo kiểu" là một phần bắt buộc của việc đánh giá mã trong các nhóm định hướng OCP.
Tái cấu trúc vi phạm OCP hiện có được thực hiện thông qua Replace Conditional with Polymorphism: mỗi nhánh điều kiện trở thành một lớp riêng biệt triển khai một giao diện chung. Mã client được viết lại để làm việc với giao diện, và triển khai cụ thể được cung cấp thông qua một factory hoặc container DI.
Điều quan trọng là hiểu rằng OCP và đa hình không giải quyết tất cả các vấn đề mở rộng. Nếu kiến trúc được chọn không đúng, việc thêm chức năng mới sẽ yêu cầu thay đổi không chỉ các triển khai mà còn cả các hợp đồng. Một kiến trúc tốt dự đoán các hướng mở rộng và đặt các trừu tượng chính xác tại những điểm đó. Đầu tư vào OCP càng hiệu quả khi dự án tồn tại lâu hơn và các yêu cầu đối với các module cụ thể thay đổi thường xuyên hơn.
Các Câu Hỏi Thường Gặp
Không. OCP cấm thay đổi mã hiện có khi thêm chức năng mới liên quan đến cùng một trừu tượng. Thay đổi hợp đồng, sửa lỗi và tái cấu trúc không phải là vi phạm OCP — nguyên tắc bảo vệ chống lại các thay đổi dây chuyền trong quá trình mở rộng.
Strategy là một triển khai trực tiếp của OCP. Giao diện chiến lược định nghĩa hợp đồng, client phụ thuộc vào trừu tượng, và các chiến lược cụ thể triển khai hành vi có thể thay đổi. Thêm một chiến lược mới không yêu cầu thay đổi client — đó là tính mở để mở rộng với tính đóng để sửa đổi.
Có, thông qua kế thừa và Template Method: lớp cơ sở định nghĩa khung xương của thuật toán và các lớp con ghi đè các bước. Tuy nhiên, kế thừa tạo ra sự liên kết chặt chẽ và kém linh hoạt hơn các giao diện. Trong phát triển hiện đại, các giao diện và hợp thành được coi là cách ưa thích để triển khai OCP.
Mã tương thích OCP đơn giản hóa việc kiểm thử: mỗi triển khai giao diện được kiểm thử riêng biệt. Mã client được kiểm thử với một triển khai giả, cho phép xác minh logic mà không bị ràng buộc vào một hành vi cụ thể. Mở rộng hệ thống không yêu cầu viết lại các kiểm thử hiện có.
Không. OCP được biện minh khi việc mở rộng chức năng có thể dự đoán được. Đối với mã ổn định không có kế hoạch mở rộng, trừu tượng bổ sung là thừa. YAGNI (You Ain't Gonna Need It) là một đối trọng tốt với OCP: trừu tượng được giới thiệu khi một biến thể hành vi thứ hai xuất hiện, không phải trước thời hạn.
Tổng Kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm