Composition Over Inheritance

In class-based object-oriented systems, composition and inheritance are the two primary methods of reusing and assembling components. Composition Over Inheritance suggests that, when there isn’t a strong case for using inheritance, developers implement reuse and assembly using composition instead.

Thay vì sử dụng kế thừa, thì mình sẽ sử dụng Composition (các thành phần nhỏ).

Inheritance

In the inheritance model, we use an abstract base class called to implement common invitation-sending logic. We then use and MessageInviter subclasses to implement the delivery details.

# app/models/inviter.rb
class Inviter < AbstractController::Base
  include AbstractController::Rendering
  include Rails.application.routes.url_helpers
 
  self.view_paths = 'app/views'
  self.default_url_options = ActionMailer::Base.default_url_options
 
  private
  def render_message_body
    render template: 'invitations/message'
  end
end
 
# app/models/email_inviter.rb
class EmailInviter < Inviter
  def initialize(invitation)
    @invitation = invitation
  end
 
  def deliver
    Mailer.invitation_notification(@invitation, render_message_body).deliver
  end
end
 
# app/models/message_inviter.rb
class MessageInviter < Inviter
  def initialize(invitation, recipient)
    @invitation = invitation
    @recipient = recipient
  end
 
  def deliver
    Message.create!(
      recipient: @recipient, 
      sender: @invitation.sender, 
      body: render_message_body
    )
  end
end

Composition

In the composition model, we use a concrete InvitationMessage class to implement common invitation-sending logic. We then use that class from EmailInviter and MessageInviter to reuse the common behavior, and the inviter classes implement delivery details.

# app/models/invitation_message.rb
class InvitationMessage < AbstractController::Base
  include AbstractController::Rendering
  include Rails.application.routes.url_helpers
 
  self.view_paths = 'app/views'
  self.default_url_options = ActionMailer::Base.default_url_options
 
  def initialize(invitation)
    @invitation = invitation
  end
 
  def body
    render template: 'invitations/message'
  end
end
 
# app/models/email_inviter.rb
class EmailInviter
  def initialize(invitation)
    @invitation = invitation
    @body = InvitationMessage.new(@invitation).body
  end
 
  def deliver
    Mailer.invitation_notification(@invitation, @body).deliver
  end
end
 
# app/models/message_inviter.rb
class MessageInviter
  def initialize(invitation, recipient)
    @invitation = invitation
    @recipient = recipient
    @body = InvitationMessage.new(@invitation).body
  end
 
  def deliver Message.create!(
    recipient: @recipient, sender: @invitation.sender, body: @body
  )
  end
end

Dynamic vs Static

Although the two implementations are fairly similar, one difference between them is that, in the inheritance model, the components are assembled statically, whereas the composition model assembles the components dynamically.

Ruby is not a compiled language and everything is evaluated at runtime, so claiming that anything is assembled statically may sound like nonsense. However, there are several ways in which inheritance hierarchies are essentially written in stone, or static:

  • You can’t swap out a superclass once it’s assigned.
  • You can’t easily add and remove behaviors after an object is instantiated.
  • You can’t inject a superclass as a dependency.
  • You can’t easily access an abstract class’s methods directly.

Nhược điểm của việc dùng Inheritance là:

  • Một khi đã khởi tạo instance, ta không thể thay đổi class name
  • Không thể thay đổi behaviors (ví dụ muốn thay đổi cách gửi mail/ hoặc cách tính tiền theo type khác)
  • Không thể inject superclass as a dependency
  • Khó để access an abstract class’s methods directly.

Những hạn chế này có thể được cải thiện bởi Composition:

  • Ta có thể thay đổi composed instance sau khi khởi tạo.
  • Add/remove behaviors sử dụng decorators, strategies, observers, and other patterns.
  • Easily inject composed dependencies.
  • Composed objects aren’t abstract, so you can use their methods anywhere.

Dynamic Inheritance

There are very few rules in Ruby, so many of the restrictions that apply to inheritance in other languages can be worked around in Ruby. For example:

  • You can reopen and modify classes after they’re defined, even while an application is running.
  • You can extend objects with modules after they’re instantiated to add behaviors.
  • You can call private methods by using send.
  • You can create new classes at runtime by calling Class.new.

These features make it possible to overcome some of the rigidity of inheritance models. However, performing all of these operations is simpler with objects than it is with classes, and doing too much dynamic type definition will make the application harder to understand by diluting the type system. After all, if none of the classes are ever fully formed, what does a class represent?

The Trouble with Hierarchies

Using subclasses introduces a subtle problem into your domain model: it assumes that your models follow a hierarchy; that is, it assumes that your types fall into a tree-like structure.

Continuing with the above example, we have a root type, Inviter, and two subtypes, EmailInviter and MessageInviter. What if we want invitations sent by admins to behave differently than invitations sent by normal users? We can create an class, but what will its superclass be? How will we combine it with and MessageInviter? There’s no easy way to combine email, message, and admin functionality using inheritance, so you’ll end up with a proliferation of conditionals.

Composition, on the other hand, provides several ways out of this mess, such as using a decorator to add admin functionality to the inviter. Once you build objects with a reasonable interface, you can combine them endlessly with minimal modification to the existing class structure.

Mixins

TODO

Single Table Inheritance

Rails provides a way to persist an inheritance hierarchy, known as Single Table Inheritance, often abbreviated as STI. Using STI, a cluster of subclasses is persisted to the same table as the base class. The name of the subclass is also saved on the row, allowing Rails to instantiate the correct subclass when pulling records back out of the database.

Rails also provides a clean way to persist composed structures using polymorphic associations. Using a polymorphic association, Rails will store both the primary key and the class name of the associated object.

Because Rails provides a clean implementation for persisting both inheritance and composition, the fact that you’re using ActiveRecord should have little influence on your decision to design using inheritance versus composition.

Drawbacks

Although composed objects are largely easy to write and assemble, there are situations where they hurt more than inheritance trees.

  • Inheritances chỉ rõ cây kế thừa. Nếu bạn thực sự muốn có cây kế thừa cho object types, hãy sử dụng Inheritance.
  • Subclasses luôn biết superclass của nó là gì Dễ để khởi tạo. Nếu bạn sử dụng Composition, bạn sẽ phải khởi tạo ít nhất 2 objects: the composing object, và the composed object. (Ví dụ: @price_strategy@video)
  • Using composition is more abstract, which means you need a name for the composed object. In our earlier example, all three classes were “inviters” in the inheritance model, but the composition model introduced the “invitation message” concept. Excessive composition can lead to vocabulary overload.

Application

TODO