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:

# app/models/invitation.rb
def deliver
  body = InvitationMessage.new(self).body
  Mailer.invitation_notification(self, body).deliver
end

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.

# app/models/invitation.rb
def deliver
	unless unsubscribed?
		body = InvitationMessage.new(self).body
		Mailer.invitation_notification(self, body).deliver 
	end
end

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ũ.

# app/models/unsubscribeable_invitation.rb
class UnsubscribeableInvitation < Invitation
  def deliver
    unless unsubscribed? 
	    super
    end
  end
 
  private
  def unsubscribed?
    Unsubscribe.where(email: recipient_email).exists?
  end
end

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:

# app/models/survey_inviter.rb
def create_invitations
  Invitation.transaction do
    recipients.map do |recipient_email|
      UnsubscribeableInvitation.create!(
        survey: survey,
        sender: sender,
        recipient_email: recipient_email,
        status: 'pending',
        message: @message
      )
    end
  end
end

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:

# app/models/unsubscribeable_invitation.rb
class UnsubscribeableInvitation < DelegateClass(Invitation)
  def deliver
    unless unsubscribed?
      super
    end
  end
 
  private
 
  def unsubscribed?
    Unsubscribe.where(email: recipient_email).exists?
  end
end
 

The implementation is extremely similar to the subclass, but it can now be applied at runtime to instances of Invitation:

# app/models/survey_inviter.rb
def deliver_invitations 
	create_invitations.each do |invitation|
		UnsubscribeableInvitation.new(invitation).deliver 
	end
end

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:

# app/models/invitation.rb
def deliver(mailer)
	body = InvitationMessage.new(self).body 
	mailer.invitation_notification(self, body).deliver
end

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)

# app/mailers/unsubscribeable_mailer.rb
class UnsubscribeableMailer
  def self.invitation_notification(invitation, body)
    if unsubscribed?(invitation)
      NullMessage.new
    else
      Mailer.invitation_notification(invitation, body)
    end
  end
 
  private
  def self.unsubscribed?(invitation)
    Unsubscribe.where(email: invitation.recipient_email).exists?
  end
 
  class NullMessage
    def deliver
    end
  end
end

And we can use dependency injection to substitute it:

# app/models/survey_inviter.rb
def deliver_invitations 
	create_invitations.each do |invitation|
		invitation.deliver(UnsubscribeableMailer) 
	end
end

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:

# app/monkey_patches/invitation_with_unsubscribing.rb
Invitation.class_eval do
  alias_method :deliver_unconditionally, :deliver
 
  def deliver
    unless unsubscribed?
      deliver_unconditionally
    end
  end
 
  private
  def unsubscribed?
    Unsubscribe.where(email: recipient_email).exists?
  end
end

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.

Application