Open/Closed Principle
Notes
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
The purpose of this principle is to make it possible to change or extend the behavior of an existing class without actually modifying the source code to that class.
Making classes extensible in this way has a number of benefits:
- Khi ta sửa 1 class, diều này có thể ảnh hưởng tới tất cả các class đang depend vào class này. Giảm bớt việc thay đổi trong class ⇒ Giảm bug có thể xuất hiện trong chính class đó.
- Việc thay đổi behavior or interface trong class này dẫn tới việc phải update bất cứ classes nào đang phụ thuộc vào behavior/ interface cũ ⇒ Có thể dẫn tới domino effect.
Strategies
It may sound nice to never need to change existing classes again, but achieving this is difficult in practice. Once you’ve identified an area that keeps changing, there are a few strategies you can use to make is possible to extend without modifications. Let’s go through an example with a few of those strategies.
EG:
Bây giờ, nếu ta bổ sung tính năng: Cho phép user unsubscribe notifications. Ta có 1 model Unsubscribe
để holds email addresses của những user đã unsubscribe.
Nếu thay đổi như thế này, ta sẽ vi phạm Open/Closed Principle, vì ta đã phải sửa đi code hiện tại.
Inheritance
Cách phổ biến nhất, đó là tạo ra 1 class mới, extend từ class cũ.
Tạo thế này code sẽ chạy okie, nhưng có một nhược điểm, đó là: Từ giờ, ta sẽ phải sử dụng UnsubscribeableInvitation
ở phần lớn các chỗ đang gọi Invitation:
This works alright for creation, but using the ActiveRecord pattern, we’ll end up with an instance of Invitation instead if we ever reload from the database. That means that inheritance is easiest to use when the class you’re extending doesn’t require persistence.
Decorators
Có một cách khác để extend existing class đó là viết decorator.
Using Ruby’s DelegateClass
method, we can quickly create decorators:
The implementation is extremely similar to the subclass, but it can now be applied at runtime to instances of Invitation
:
This makes it easier to combine with persistence. However, Ruby’s DelegateClass doesn’t combine well with ActionPack’s polymorphic URLs.
Dependency Injection
This method requires more forethought in the class you want to extend, but classes that follow Inversion of Control can inject dependencies to extend classes without modifying them.
We can modify our Invitation class slightly to allow client classes to inject a mailer:
Now we can write a mailer implementation that checks to see if a user is unsubscribed before sending them messages (đang đẩy việc check unsubscribe từ invitation
sang mailer
)
And we can use dependency injection to substitute it:
Everything is Open⭐⭐⭐
-
Kiểu gì ta cũng phải thay đổi ở 1 chỗ nào đó =)) Dù có viết thêm class thế nào đi nữa, ta vẫn phải thay đổi code “somewhere”.
-
Thay vì chú ý vào việc “đoán” xem tương lai sẽ mở rộng tính năng nào, chúng ta nên chú ý mỗi lần ta thay đổi existing code. Sau mỗi lần thay đổi, check lại xem có cách nào để refactor code, sao cho nếu có những thay đổi tương tự như thế này trong tương lai, ta sẽ không phải modify class hiện tại nữa.
-
Code tends to change in the same ways over and over, so by making each change easy to apply as you need to make it, you’re making the next change easier.
Monkey Patching
Có 1 cách để extend existing class mà không phải sửa code của nó, đó là dùng Monkey patch:
Cách này k thay đổi source code chính, nhưng nó lại modify existing class ⇒ Vẫn có risk sẽ breaking it, và các class đang phụ thuộc vào nó.
Một nhược điểm nữa là cách này dễ gây confusion, do developers phải nhìn vào nhiều chỗ để đọc full được definition của class.
⇒ Cách này k được suggest dùng.
Drawbacks
Although following this principle will make code easier to change, it may make it more difficult to understand. This is because the gained flexibility requires introducing indirection and abstraction. Although each of the three strategies outlined in this chapter are more flexible than the original change, directly modifying the class is the easiest to understand.
This principle is most useful when applied to classes with high reuse and potentially high churn. Applying it everywhere will result in extra work and more obscure code.