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