Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jayzz55/2cf292fccdb76cb71414 to your computer and use it in GitHub Desktop.
Save jayzz55/2cf292fccdb76cb71414 to your computer and use it in GitHub Desktop.

7: Sharing Role Behavior With Modules

Combining the qualities of two existing subclasses is something Ruby cannot do (multiple inheritance)

Because no design technique is free, creating the most cost-effective application requires making informed tradeoffs between the relative cost and likely benefits of alternatives

Writing Inheritable Code

  • Recognize the antipatterns
  • Insist on the abstraction
  • Honor the contract (Liskov Substitution Principle)
  • Use the template method pattern
  • Preemptively decouple classes (avoid super)
  • Create shallow hierarchies

...when a sending object checks the class of a received object to determine what message to send, you have overlooked a duck type. This is another maintenance nightmare; the code must change everytime you introduce a new class of receiver. In this situation all of the possible receiving objects play a common role. They should be codified as a duck type and receivers should implement the duck type's interface. Once they do, the original object can send one single message to every receiver, confident that because each receiver plays the role it will understand the common message.

Example:

Consider the problem of scheduling a trip. Trips occur at specific points in time and involve bicycles, mechanics, and motor vehicles. Bikes, mechanics, and vehicles are real things in the physical world that can’t be in two places at once. FastFeet needs a way to arrange all of these objects on a schedule so that it can determine, for any point in time, which objects are available and which are already committed.

To model this problem, first we have to discover the duck type Schedulable, which is a role shared by Bikes, Mechanics, and Vehicles. Through this role, objects can share a common behaviour and decouple the Preparer object from the responsibility of checking and managing the schedule.

With this, the Schedule object is also free from knowing and managing the lead time of Preparer object, the Schedule object only needs to know the date and the Preparer, and see if it has been scheduled without knowing about the perparer's lead time.

class Schedule
  def scheduled?(schedulable, start_date, end_date)
    puts "This #{schedulable.class} " +
         "is not scheduled\n" +
         "  between #{start_date} and #{end_date}"
    false
  end
end

module Schedulable
  attr_writer :schedule

  def schedule
    @schedule ||= ::Schedule.new
  end

  def schedulable?(start_date, end_date)
    !scheduled?(start_date - lead_days, end_date)
  end

  def scheduled?(start_date, end_date)
    schedule.scheduled?(self, start_date, end_date)
  end

  # includers may override
  def lead_days
    0
  end

end

class Bicycle
  include Schedulable

  def lead_days
    1
  end

  # ...
end

require 'date'
starting = Date.parse("2015/09/04")
ending   = Date.parse("2015/09/10")

b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
#    between 2015-09-03 and 2015-09-10
#  => true

class Vehicle
  include Schedulable

  def lead_days
    3
  end

  # ...
end

class Mechanic
  include Schedulable

  def lead_days
    4
  end

  # ...
end

v = Vehicle.new
v.schedulable?(starting, ending)
# This Vehicle is not scheduled
#   between 2015-09-01 and 2015-09-10
#  => true

m = Mechanic.new
m.schedulable?(starting, ending)
# This Mechanic is not scheduled
#   between 2015-08-31 and 2015-09-10
#  => true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment