Skip to content

Instantly share code, notes, and snippets.

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

8: Combining Objects with Composition

Composition describes a has a relationship.

  • Composition: objects "inside" have no meaning outside that context

  • Aggregation: like composition except objects "inside" have meaning outside that context

Deciding Between Inheritance and Composition

Inheritance gives you message delegation for free at the cost of maintaining a class hierarchy. Composition allows objects to have structural independence at the cost of explicit message delegation.

Composition contains far few built-in dependencies than inheritance; it is very often the best choice.

With inheritance, a correctly modeled hierarchy will give you the benefit of propogating changes in the base class to all subclasses. This can also be a disadvantage when the hierarchy is modeled incorrectly, as a dramatic change to the base class due to a change in requirements will break sub-classes. Inheritance = built-in dependencies.

Enormous, broad-reaching changes of behavior can be achieved with very small changes in code. This is true, for better or for worse, whether you come to regret it or not.

Avoid writing frameworks that require users of your code to subclass your ibjects in order to gain your behavior. Their application's objects may already be arranged in a hierarchy; inheriting from your framework may not be possible.

Example:

The following code shows that Bicycle now uses composition. It shows Bicycle, Parts, and PartsFactory and the configuration arrays for road and mountain bikes. Bicycle has-a Parts, which in turn has-a collection of Part objects.

Parts and Part may exist as classes, but the objects in which they are contained think of them as roles. Parts is a class that plays the Parts role; it implements spares. The role of Part is played by an OpenStruct, which implements name, description and needs_spare.

Parts return a collection of objects that don't behave like Array. To make the :size and :each method available on parts & partially behaving like array, the Forwardable module is being used to delegate size and each method from the Enumerable package to Parts.

require 'ostruct'
module PartsFactory
  def self.build(config, parts_class = Parts)
    parts_class.new(
      config.collect {|part_config|
        create_part(part_config)})
  end

  def self.create_part(part_config)
    OpenStruct.new(
      name:        part_config[0],
      description: part_config[1],
      needs_spare: part_config.fetch(2, true))
  end
end

mountain_parts = PartsFactory.build(mountain_config)
# -> <Parts:0x000001009ad8b8 @parts=
#      [#<OpenStruct name="chain",
#                    description="10-speed",
#                    needs_spare=true>,
#       #<OpenStruct name="tire_size",
#                    description="2.1",
#                    etc ...

class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size       = args[:size]
    @parts      = args[:parts]
  end

  def spares
    parts.spares
  end
end

require 'forwardable'
class Parts
  extend Forwardable
  def_delegators :@parts, :size, :each
  include Enumerable

  def initialize(parts)
    @parts = parts
  end

  def spares
    select {|part| part.needs_spare}
  end
end

require 'ostruct'
module PartsFactory
  def self.build(config, parts_class = Parts)
    parts_class.new(
      config.collect {|part_config|
        create_part(part_config)})
  end

  def self.create_part(part_config)
    OpenStruct.new(
      name:        part_config[0],
      description: part_config[1],
      needs_spare: part_config.fetch(2, true))
  end
end

road_config =
  [['chain',        '10-speed'],
   ['tire_size',    '23'],
   ['tape_color',   'red']]

mountain_config =
  [['chain',        '10-speed'],
   ['tire_size',    '2.1'],
   ['front_shock',  'Manitou', false],
   ['rear_shock',   'Fox']]

road_bike =
  Bicycle.new(
    size: 'L',
    parts: PartsFactory.build(road_config))

road_bike.spares
# -> [#<OpenStruct name="chain", etc ...

mountain_bike =
  Bicycle.new(
    size: 'L',
    parts: PartsFactory.build(mountain_config))

mountain_bike.spares
# -> [#<OpenStruct name="chain", etc ...

recumbent_config =
  [['chain',        '9-speed'],
   ['tire_size',    '28'],
   ['flag',         'tall and orange']]

recumbent_bike =
  Bicycle.new(
    size: 'L',
    parts: PartsFactory.build(recumbent_config))

recumbent_bike.spares
# -> [#<OpenStruct
#       name="chain",
#       description="9-speed",
#       needs_spare=true>,
#     #<OpenStruct
#       name="tire_size",
#       description="28",
#       needs_spare=true>,
#     #<OpenStruct
#       name="flag",
#       description="tall and orange",
#       needs_spare=true>]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment