Chapter 8. Combining Objects with Composition

Composition is the act of combining distinct parts into a complex whole such that the whole becomes more than the sum of its parts. Music, for example, is composed.

You may not think of your software as music but the analogy is apt. The musical score of Beethoven’s Fifth Symphony is a long list of distinct and independent notes. You need hear them only once to understand that while it contains the notes, it is not the notes. It is something more.

You can create software this same way, by using object-oriented composition to combine simple, independent objects into larger, more complex wholes. In composition, the larger object is connected to its parts via a has-a relationship. A bicycle has parts. Bicycle is the containing object, the parts are contained within a bicycle. Inherent in the definition of composition is the idea that, not only does a bicycle have parts, but it communicates with them via an interface. Part is a role and bicycles are happy to collaborate with any object that plays the role.

This chapter teaches the techniques of OO composition. It starts with an example, moves on to a discussion of the relative strengths and weakness of composition and inheritance, and then concludes with recommendations about how to choose between alternative design techniques.

Composing a Bicycle of Parts

This section begins where the Bicycle example in Chapter 6, Acquiring Behavior Through Inheritance, ended. If that code is no longer in the forefront of your mind, it’s worth flipping back to the end of Chapter 6 and refreshing your memory. This section takes that example and moves it through several refactorings, gradually replacing inheritance with composition.

Updating the Bicycle Class

The Bicycle class is currently an abstract superclass in an inheritance hierarchy and you’d like to convert it to use composition. The first step is to ignore the existing code and think about how a bicycle should be composed.

The Bicycle class is responsible for responding to the spares message. This spares message should return a list of spare parts. Bicycles have parts, the bicycle–parts relationship quite naturally feels like composition. If you created an object to hold all of a bicycle’s parts, you could delegate the spares message to that new object.

It’s reasonable to name this new class Parts. The Parts object can be responsible for holding a list of the bike’s parts and for knowing which of those parts needs spares. Notice that this object represents a collection of parts, not a single part.

The sequence diagram in Figure 8.1 illustrates this idea. Here, a Bicycle sends the spares message to its Parts object.

Image

Figure 8.1. A Bicycle asks Parts for spares.

Every Bicycle needs a Parts object; part of what it means to be a Bicycle is to have-a Parts. The class diagram in Figure 8.2 illustrates this relationship.

Image

Figure 8.2. A Bicycle has-a Parts.

This diagram shows the Bicycle and Parts classes connected by a line. The line attaches to Bicycle with a black diamond; this black diamond indicates composition, it means that a Bicycle is composed of Parts. The Parts side of the line has the number “1.” This means there’s just one Parts object per Bicycle.

It’s easy to convert the existing Bicycle class to this new design. Remove most of its code, add a parts variable to hold the Parts object, and delegate spares to parts. Here’s the new Bicycle class.


 1  class Bicycle
 2    attr_reader :size, :parts
 3 
 4    def initialize(args={})
 5      @size       = args[:size]
 6      @parts      = args[:parts]
 7    end
 8 
 9    def spares
10      parts.spares
11    end
12  end


Bicycle is now responsible for three things: knowing its size, holding onto its Parts, and answering its spares.

Creating a Parts Hierarchy

That was easy, but only because there wasn’t much bicycle related behavior in the Bicycle class to begin with; most of the code in Bicycle dealt with parts. You still need the parts behavior that you just removed from Bicycle, and the simplest way to get this code working again is to simply fling that code into a new hierarchy of Parts, as shown below.


 1  class Parts
 2    attr_reader :chain, :tire_size
 3 
 4    def initialize(args={})
 5      @chain      = args[:chain]     || default_chain
 6      @tire_size  = args[:tire_size] || default_tire_size
 7      post_initialize(args)
 8    end
 9 
10    def spares
11      { tire_size: tire_size,
12        chain:     chain}.merge(local_spares)
13    end
14 
15    def default_tire_size
16      raise NotImplementedError
17    end
18 
19    # subclasses may override
20    def post_initialize(args)
21      nil
22    end
23 
24    def local_spares
25      {}
26    end
27 
28    def default_chain
29      '10-speed'
30    end
31  end
32 
33  class RoadBikeParts < Parts
34    attr_reader :tape_color
35 
36    def post_initialize(args)
37      @tape_color = args[:tape_color]
38    end
39 
40    def local_spares
41      {tape_color: tape_color}
42    end
43 
44    def default_tire_size
45      '23'
46    end
47  end
48 
49  class MountainBikeParts < Parts
50    attr_reader :front_shock, :rear_shock
51 
52    def post_initialize(args)
53      @front_shock = args[:front_shock]
54      @rear_shock =  args[:rear_shock]
55    end
56 
57    def local_spares
58      {rear_shock:  rear_shock}
59    end
60 
61    def default_tire_size
62      '2.1'
63    end
64  end


This code is a near exact copy of the Bicycle hierarchy from Chapter 6; the differences are that the classes have been renamed and the size variable has been removed.

The class diagram in Figure 8.3 illustrates this transition. There is now an abstract Parts class. Bicycle is composed of Parts. Parts has two subclasses, RoadBikeParts and MountainBikeParts.

Image

Figure 8.3. A hierarchy of Parts.

After this refactoring, everything still works. As you can see below, regardless of whether it has RoadBikeParts or MountainBikeParts, a bicycle can still correctly answer its size and spares.


 1  road_bike =
 2    Bicycle.new(
 3      size:  'L',
 4      parts: RoadBikeParts.new(tape_color: 'red'))
 5 
 6  road_bike.size    # -> 'L'
 7 
 8  road_bike.spares
 9  # -> {:tire_size=>"23",
10  #     :chain=>"10-speed",
11  #     :tape_color=>"red"}
12 
13  mountain_bike =
14    Bicycle.new(
15      size:  'L',
16      parts: MountainBikeParts.new(rear_shock: 'Fox'))
17 
18  mountain_bike.size   # -> 'L'
19 
20  mountain_bike.spares
21  # -> {:tire_size=>"2.1",
22  #     :chain=>"10-speed",
23  #     :rear_shock=>"Fox"}


This wasn’t a big change and it isn’t much of an improvement. However, this refactoring did reveal one useful thing; it made it blindingly obvious just how little Bicycle specific code there was to begin with. Most of the code above deals with individual parts; the Parts hierarchy now cries out for another refactoring.

Composing the Parts Object

By definition a parts list contains a list of individual parts. It’s time to add a class to represent a single part. The class name for an individual part clearly ought to be Part but introducing a Part class when you already have a Parts class makes conversation a challenge. It is confusing to use the word “parts” to refer to a collection of Part objects, when that same word already refers to a single Parts object. However, the previous phrase illustrates a technique that side steps the communication problem; when discussing Part and Parts, you can follow the class name with the word “object” and pluralize “object” as necessary.

You can also avoid the communication problem from the beginning by choosing different class names, but other names might not be as expressive and may well introduce communication problems of their own. This Parts/Part situation is common enough that it’s worth dealing with head-on. Choosing these class names requires a precision of communication that’s a worthy goal in itself.

Thus, there’s a Parts object, and it may contain many Part objects—simple as that.

Creating a Part

Figure 8.4 shows a new sequence diagram that illustrates the conversation between Bicycle and its Parts object, and between a Parts object and its Part objects. Bicycle sends spares to Parts and then the Parts object sends needs_spare to each Part.

Image

Figure 8.4. Bicycle sends spares to Parts, Parts sends needs_spare to each Part.

Changing the design in this way requires creating a new Part object. The Parts object is now composed of Part objects, as illustrated by the class diagram in Figure 8.5. The “1..*” on the line near Part indicates that a Parts will have one or more Part objects.

Image

Figure 8.5. Bicycle holds one Parts object, which in turn holds many Part objects.

Introducing this new Part class simplifies the existing Parts class, which now becomes a simple wrapper around an array of Part objects. Parts can filter its list of Part objects and return the ones that need spares. The code below shows three classes: the existing Bicycle class, the updated Parts class, and the newly introduced Part class.


 1  class Bicycle
 2    attr_reader :size, :parts
 3 
 4    def initialize(args={})
 5      @size       = args[:size]
 6      @parts      = args[:parts]
 7    end
 8 
 9    def spares
10      parts.spares
11    end
12  end
13 
14  class Parts
15    attr_reader :parts
16 
17    def initialize(parts)
18      @parts = parts
19    end
20 
21    def spares
22      parts.select {|part| part.needs_spare}
23    end
24  end
25 
26  class Part
27    attr_reader :name, :description, :needs_spare
28 
29    def initialize(args)
30      @name         = args[:name]
31      @description  = args[:description]
32      @needs_spare  = args.fetch(:needs_spare, true)
33    end
34  end


Now that these three classes exist you can create individual Part objects. The following code creates a number of different parts and saves each in an instance variable.


 1  chain =
 2    Part.new(name: 'chain', description: '10-speed')
 3 
 4  road_tire =
 5    Part.new(name: 'tire_size',  description: '23')
 6 
 7  tape =
 8    Part.new(name: 'tape_color', description: 'red')
 9 
10  mountain_tire =
11    Part.new(name: 'tire_size',  description: '2.1')
12 
13  rear_shock =
14    Part.new(name: 'rear_shock', description: 'Fox')
15 
16  front_shock =
17    Part.new(
18      name: 'front_shock',
19      description: 'Manitou',
20      needs_spare: false)


Individual Part objects can be grouped together into a Parts. The code below combines the road bike Part objects into a road bike suitable Parts.


 1  road_bike_parts =
 2    Parts.new([chain, road_tire, tape])


Of course, you can skip this intermediate step and simply construct the Parts object on the fly when creating a Bicycle, as shown in lines 4–6 and 22–25 below.


 1  road_bike =
 2    Bicycle.new(
 3      size:  'L',
 4      parts: Parts.new([chain,
 5                        road_tire,
 6                        tape]))
 7 
 8  road_bike.size    # -> 'L'
 9 
10  road_bike.spares
11  # -> [#<Part:0x00000101036770
12  #         @name="chain",
13  #         @description="10-speed",
14  #         @needs_spare=true>,
15  #  #<Part:0x0000010102dc60
16  #         @name="tire_size",
17  #         etc ...
18 
19  mountain_bike =
20    Bicycle.new(
21      size:  'L',
22      parts: Parts.new([chain,
23                        mountain_tire,
24                        front_shock,
25                        rear_shock]))
26 
27  mountain_bike.size    # -> 'L'
28 
29  mountain_bike.spares
30  # -> [#<Part:0x00000101036770
31  #         @name="chain",
32  #         @description="10-speed",
33  #         @needs_spare=true>,
34  #     #<Part:0x0000010101b678
35  #         @name="tire_size",
36  #         etc ...


As you can see from lines 8–17, and 27–34 above, this new code arrangement works just fine, and it behaves almost exactly like the old Bicycle hierarchy. There is one difference: Bicycle’s old spares method returned a hash, but this new spares method returns an array of Part objects.

While it may be tempting to think of these objects as instances of Part, composition tells you to think of them as objects that play the Part role. They don’t have to be a kind-of the Part class, they just have to act like one; that is, they must respond to name, description, and needs_spare.

Making the Parts Object More Like an Array

This code works but there’s definitely room for improvement. Step back for a minute and think about the parts and spares methods of Bicycle. These messages feel like they ought to return the same sort of thing, yet the objects that come back don’t behave in the same way. Look at what happens when you ask each for its size.

In line 1 below, spares is happy to report that its size is 3. However, asking this same question of parts doesn’t turn out so well, as you can see from lines 2–4.


 1  mountain_bike.spares.size # -> 3
 2  mountain_bike.parts.size
 3  # -> NoMethodError:
 4  #      undefined method 'size' for #<Parts:...>


Line 1 works because spares returns an array (of Part objects) and Array understands size. Line 2 fails because parts returns instance of Parts, which does not.

Failures like this will chase you around for as long as you own this code. These two things both seem like arrays. You will inevitably treat them as if they are, despite the fact that exactly one half of the time, the result will be like stepping on the proverbial rake in the yard. The Parts object does not behave like an array and all attempts to treat it as one will fail.

You can fix the proximate problem by adding a size method to Parts. This is a simple matter of implementing a method to delegate size to the actual array, as shown here:


 1    def size
 2      parts.size
 3    end


However, this change starts the Parts class down a slippery slope. Do this, and it won’t be long before you’ll want Parts to respond to each, and then sort, and then everything else in Array. This never ends; the more array-like you make Parts, the more like an array you’ll expect it to be.

Perhaps Parts is an Array, albeit one with a bit of extra behavior. You could make it one; the next example shows a new version of the Parts class, now as a subclass of Array.


 1  class Parts < Array
 2    def spares
 3      select {|part| part.needs_spare}
 4    end
 5  end


The above code is a very straightforward expression of the idea that Parts is a specialization of Array; in a perfect object-oriented language this solution would be exactly correct. Unfortunately, the Ruby language has not quite achieved perfection and this design contains a hidden flaw.

This next example illustrates the problem. When Parts subclasses Array, it inherits all of Array’s behavior. This behavior includes methods like +, which adds two arrays together and returns a third. Lines 3 and 4 below show + combining two existing instances of Parts and saving the result into the combo_parts variable.

This appears to work; combo_parts now contains the correct number of parts (line 7). However, something is clearly not right. As line 12 shows, combo_parts cannot answer its spares.

The root cause of the problem is revealed by lines 15–17. Although the objects that got +’d together were instances of Parts, the object that + returned was an instance of Array, and Array does not understand spares.


 1  #  Parts inherits '+' from Array, so you can
 2  #    add two Parts together.
 3  combo_parts =
 4    (mountain_bike.parts + road_bike.parts)
 5 
 6  # '+' definitely combines the Parts
 7  combo_parts.size            # -> 7
 8 
 9  # but the object that '+' returns
10  #   does not understand 'spares'
11  combo_parts.spares
12  # -> NoMethodError: undefined method 'spares'
13  #      for #<Array:...>
14 
15  mountain_bike.parts.class   # -> Parts
16  road_bike.parts.class       # -> Parts
17  combo_parts.class           # -> Array !!!


It turns out that there are many methods in Array that return new arrays, and unfortunately these methods return new instances of the Array class, not new instances of your subclass. The Parts class is still misleading and you have just swapped one problem for another. Where once you were disappointed to find that Parts did not implement size, now you might be surprised to find that adding two Parts together returns a result that does not understand spares.

You’ve seen three different implementations of Parts. The first answers only the spares and parts messages; it does not act like an array, it merely contains one. The second Parts implementation adds size, a minor improvement that just returns the size of its internal array. The most recent Parts implementation subclasses Array and therefore gives the appearance of fully behaving like an array, but as the example above shows, an instance of Parts still displays unexpected behavior.

It has become clear that there is no perfect solution; it’s therefore time to make a difficult decision. Even though it cannot respond to size, the original Parts implementation may be good enough; if so, you can accept its lack of array-like behavior and revert to that version. If you need size and size alone, it may be best to add just this one method and so settle for the second implementation. If you can tolerate the possibility of confusing errors or you know with absolute certainty that you’ll never encounter them, it might make sense to subclass Array and walk quietly away.

Somewhere in the middle ground between complexity and usability lies the following solution. The Parts class below delegates size and each to its @parts array and includes Enumerable to get common traversal and searching methods. This version of Parts does not have all of the behavior of Array, but at least everything that it claims to do actually works.


 1  require 'forwardable'
 2  class Parts
 3    extend Forwardable
 4    def_delegators :@parts, :size, :each
 5    include Enumerable
 6 
 7    def initialize(parts)
 8      @parts = parts
 9    end
10 
11    def spares
12      select {|part| part.needs_spare}
13    end
14  end


Sending + to an instance of this Parts results in a NoMethodError exception. However, because Parts now responds to size, each, and all of Enumerable, and obligingly raises errors when you mistakenly treat it like an actual Array, this code may be good enough. The following example shows that spares and parts can now both respond to size.


 1  mountain_bike =
 2    Bicycle.new(
 3      size:  'L',
 4      parts: Parts.new([chain,
 5                        mountain_tire,
 6                        front_shock,
 7                        rear_shock]))
 8 
 9  mountain_bike.spares.size   # -> 3
10  mountain_bike.parts.size    # -> 4


You again have a workable version of the Bicycle, Parts, and Part classes. It’s time to reevaluate the design.

Manufacturing Parts

Look back at lines 4–7 above. The Part objects held in the chain, mountain_tire, and so on, variables were created so long ago that you may already have forgotten them. Think about the body of knowledge that these four lines represent. Somewhere in your application, some object had to know how to create these Part objects. And here, on lines 4–7 above, this place has to know that these four specific objects go with mountain bikes.

This is a lot of knowledge and it can easily leak all over your application. This leakage is both unfortunate and unnecessary. Although there are lots of different individual parts, there are only a few valid combinations of parts. Everything would be easier if you could describe the different bikes and then use your descriptions to magically manufacture the correct Parts object for any bike.

It’s easy to describe the combination of parts that make up a specific bike. The code below does this with a simple 2-dimensional array, where each row contains three possible columns. The first column contains the part name ('chain', 'tire_size', etc.), the second, the part description ('10-speed', '23', etc.) and the third (which is optional), a Boolean that indicates whether this part needs a spare. Only 'front_shock' on line 9 below puts a value in this third column, the other parts would like to default to true, as they require spares.


 1  road_config =
 2    [['chain',        '10-speed'],
 3     ['tire_size',    '23'],
 4     ['tape_color',   'red']]
 5 
 6  mountain_config =
 7    [['chain',        '10-speed'],
 8     ['tire_size',    '2.1'],
 9     ['front_shock',  'Manitou', false],
10     ['rear_shock',   'Fox']]


Unlike a hash, this simple 2-dimensional array provides no structural information. However, you understand how this structure is organized and you can encode your knowledge into a new object that manufactures Parts.

Creating the PartsFactory

As discussed in Chapter 3, Managing Dependencies, an object that manufactures other objects is a factory. Your past experience in other languages may predispose you to flinch when you hear this word, but think of this as an opportunity to reclaim it. The word factory does not mean difficult, or contrived, or overly complicated; it’s merely the word OO designers use to concisely communicate the idea of an object that creates other objects. Ruby factories are simple and there’s no reason to avoid this intention revealing word.

The code below shows a new PartsFactory module. Its job is to take an array like one of those listed above and manufacture a Parts object. Along the way it may well create Part objects, but this action is private. Its public responsibility is to create a Parts.

This first version of PartsFactory takes three arguments, a config, and the names of the classes to be used for Part, and Parts. Line 6 below creates the new instance of Parts, initializing it with an array of Part objects built from the information in the config.


 1  module PartsFactory
 2    def self.build(config,
 3                   part_class  = Part,
 4                   parts_class = Parts)
 5 
 6      parts_class.new(
 7        config.collect {|part_config|
 8          part_class.new(
 9            name:         part_config[0],
10            description:  part_config[1],
11            needs_spare:  part_config.fetch(2, true))})
12    end
13  end


This factory knows the structure of the config array. In lines 9–11 above it expects name to be in the first column, description to be in the second, and needs_spare to be in the third.

Putting knowledge of config’s structure in the factory has two consequences. First, the config can be expressed very tersely. Because PartsFactory understands config’s internal structure, config can be specified as an array rather than a hash. Second, once you commit to keeping config in an array, you should always create new Parts objects using the factory. To create new Parts via any other mechanism requires duplicating the knowledge that is encoded in lines 9–11 above.

Now that PartsFactory exists, you can use the configuration arrays defined above to easily create new Parts, as shown here:


 1  road_parts = PartsFactory.build(road_config)
 2  # -> [#<Part:0x00000101825b70
 3  #       @name="chain",
 4  #       @description="10-speed",
 5  #       @needs_spare=true>,
 6  #     #<Part:0x00000101825b20
 7  #       @name="tire_size",
 8  #          etc ...
 9 
10  mountain_parts = PartsFactory.build(mountain_config)
11  # -> [#<Part:0x0000010181ea28
12  #        @name="chain",
13  #        @description="10-speed",
14  #        @needs_spare=true>,
15  #     #<Part:0x0000010181e9d8
16  #        @name="tire_size",
17  #        etc ...


PartsFactory, combined with the new configuration arrays, isolates all the knowledge needed to create a valid Parts. This information was previously dispersed throughout the application but now it is contained in this one class and these two arrays.

Leveraging the PartsFactory

Now that the PartsFactory is up and running, have another look at the Part class (repeated below). Part is simple. Not only that, the only even slightly complicated line of code (the fetch on line 7 below) is duplicated in PartsFactory. If PartsFactory created every Part, Part wouldn’t need this code. And if you remove this code from Part, there’s almost nothing left; you can replace the whole Part class with a simple OpenStruct.


 1  class Part
 2    attr_reader :name, :description, :needs_spare
 3 
 4    def initialize(args)
 5      @name         = args[:name]
 6      @description  = args[:description]
 7      @needs_spare  = args.fetch(:needs_spare, true)
 8    end
 9  end


Ruby’s OpenStruct class is a lot like the Struct class that you’ve already seen, it provides a convenient way to bundle a number of attributes into an object. The difference between the two is that Struct takes position order initialization arguments while OpenStruct takes a hash for its initialization and then derives attributes from the hash.

There are good reasons to remove the Part class; this simplifies the code and you may never again need anything as complicated as what you currently have. You can remove all trace of Part by deleting the class and then changing PartsFactory to use OpenStruct to create an object that plays the Part role. The following code shows a new version of PartFactory where part creation has been refactored into a method of its own (line 9).


 1  require 'ostruct'
 2  module PartsFactory
 3    def self.build(config, parts_class = Parts)
 4      parts_class.new(
 5        config.collect {|part_config|
 6          create_part(part_config)})
 7    end
 8 
 9    def self.create_part(part_config)
10      OpenStruct.new(
11        name:        part_config[0],
12        description: part_config[1],
13        needs_spare: part_config.fetch(2, true))
14    end
15  end


Line 13 above is now the only place in the application that defaults needs_spare to true, so PartsFactory must be solely responsible for manufacturing Parts.

This new version of PartsFactory works. As shown below, it returns a Parts that contains an array of OpenStruct objects, each of which plays the Part role.


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


The Composed Bicycle

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.

The following 54 lines of code completely replace the 66-line inheritance hierarchy from Chapter 6.


 1  class Bicycle
 2    attr_reader :size, :parts
 3 
 4    def initialize(args={})
 5      @size       = args[:size]
 6      @parts      = args[:parts]
 7    end
 8 
 9    def spares
10      parts.spares
11    end
12  end
13 
14  require 'forwardable'
15  class Parts
16    extend Forwardable
17    def_delegators :@parts, :size, :each
18    include Enumerable
19 
20    def initialize(parts)
21      @parts = parts
22    end
23 
24    def spares
25      select {|part| part.needs_spare}
26    end
27  end
28 
29  require 'ostruct'
30  module PartsFactory
31    def self.build(config, parts_class = Parts)
32      parts_class.new(
33        config.collect {|part_config|
34          create_part(part_config)})
35    end
36 
37    def self.create_part(part_config)
38      OpenStruct.new(
39        name:        part_config[0],
40        description: part_config[1],
41        needs_spare: part_config.fetch(2, true))
42    end
43  end
44 
45  road_config =
46    [['chain',        '10-speed'],
47     ['tire_size',    '23'],
48     ['tape_color',   'red']]
49 
50  mountain_config =
51    [['chain',        '10-speed'],
52     ['tire_size',    '2.1'],
53     ['front_shock',  'Manitou', false],
54     ['rear_shock',   'Fox']]


This new code works much like the prior Bicycle hierarchy. The only difference is that the spares message now returns an array of Part-like objects instead of a hash, as you can see on lines 7 and 15 below.


 1  road_bike =
 2    Bicycle.new(
 3      size: 'L',
 4      parts: PartsFactory.build(road_config))
 5 
 6  road_bike.spares
 7  # -> [#<OpenStruct PartsFactory::Part name="chain", etc ...
 8 
 9  mountain_bike =
10    Bicycle.new(
11      size: 'L',
12      parts: PartsFactory.build(mountain_config))
13 
14  mountain_bike.spares
15  # -> [#<OpenStruct PartsFactory::Part name="chain", etc ...


Now that these new classes exist, it’s very easy to create a new kind of bike.

Adding support for recumbent bikes took 19 new lines of code in Chapter 6. This task can now be accomplished with 3 lines of configuration (lines 2–4 below).


 1  recumbent_config =
 2    [['chain',        '9-speed'],
 3     ['tire_size',    '28'],
 4     ['flag',         'tall and orange']]
 5 
 6  recumbent_bike =
 7    Bicycle.new(
 8      size: 'L',
 9      parts: PartsFactory.build(recumbent_config))
10 
11  recumbent_bike.spares
12  # -> [#<OpenStruct PartsFactory::Part
13  #       name="chain",
14  #       description="9-speed",
15  #       needs_spare=true>,
16  #     #<OpenStruct PartsFactory::Part
17  #       name="tire_size",
18  #       description="28",
19  #       needs_spare=true>,
20  #     #<OpenStruct PartsFactory::Part
21  #       name="flag",
22  #       description="tall and orange",
23  #       needs_spare=true>]


As shown in lines 11–23 above, you can now create a new bike by simply describing its parts.

Deciding Between Inheritance and Composition

Remember that classical inheritance is a code arrangement technique. Behavior is dispersed among objects and these objects are organized into class relationships such that automatic delegation of messages invokes the correct behavior. Think of it this way: For the cost of arranging objects in a hierarchy, you get message delegation for free.

Composition is an alternative that reverses these costs and benefits. In composition, the relationship between objects is not codified in the class hierarchy; instead objects stand alone and as a result must explicitly know about and delegate messages to one another. Composition allows objects to have structural independence, but at the cost of explicit message delegation.

Now that you’ve seen examples of inheritance and composition you can begin to think about when to use them. The general rule is that, faced with a problem that composition can solve, you should be biased towards doing so. If you cannot explicitly defend inheritance as a better solution, use composition. Composition contains far fewer built-in dependencies than inheritance; it is very often the best choice.

Inheritance is a better solution when its use provides high rewards for low risk. This section examines the costs and benefits of inheritance versus composition and provides guidelines for choosing the best relationship.

Accepting the Consequences of Inheritance

Making wise choices about using inheritance requires a clear understanding of its costs and benefits.

Benefits of Inheritance

Chapter 2, Designing Classes with a Single Responsibility, outlined four goals for code: it should be transparent, reasonable, usable, and exemplary. Inheritance, when correctly applied, excels at the second, third, and fourth goals.

Methods defined near the top of inheritance hierarchies have widespread influence because the height of the hierarchy acts as a lever that multiplies their effects. Changes made to these methods ripple down the inheritance tree. Correctly modeled hierarchies are thus extremely reasonable; big changes in behavior can be achieved via small changes in code.

Use of inheritance results in code that can be described as open–closed; hierarchies are open for extension while remaining closed for modification. Adding a new subclass to an existing hierarchy requires no changes to existing code. Hierarchies are thus usable; you can easily create new subclasses to accommodate new variants.

Correctly written hierarchies are easy to extend. The hierarchy embodies the abstraction and every new subclass plugs in a few concrete differences. The existing pattern is easy to follow and replicating it will be the natural choice of any programmer charged with creating new subclasses. Hierarchies are therefore exemplary; by their nature they provide guidance for writing the code to extend them.

You need look no farther than the source of object-oriented languages themselves to see the value of organizing code using inheritance. In Ruby, the Numeric class provides an excellent example. Integer and Float are modeled as subclasses of Numeric; this is-a relationship is exactly right. Integers and floats are fundamentally numbers. Allowing these two classes to share a common abstraction is the most parsimonious way to organize code.

Costs of Inheritance

Concerns about the use of inheritance fall into two different areas. The first fear is that you might be fooled into choosing inheritance to solve the wrong kind of problem. If you make this mistake a day will come when you need to add behavior but find there’s no easy way do so. Because the model is incorrect, the new behavior won’t fit; in this case you’ll be forced to duplicate or restructure code.

Second, even when inheritance makes sense for the problem, you might be writing code that will be used by others for purposes you did not anticipate. These other programmers want the behavior you have created but may not be able to tolerate the dependencies that inheritance demands.

The previous section on the benefits of inheritance was careful to qualify its assertions as applying only to a “correctly modeled hierarchy.” Imagine reasonable, usable and exemplary as two-sided coins. The benefit side represents the wonderful gains that inheritance provides. If you apply inheritance to a problem for which it is not suited, you effectively flip these coins over and encounter a parallel detriment.

The flip side of the reasonable coin is the very high cost of making changes near the top of an incorrectly modeled hierarchy. In this case, the leveraging effect works to your disadvantage; small changes break everything.

The opposing side of the usable coin is the impossibility of adding behavior when new subclasses represent a mixture of types. The Bicycle hierarchy in Chapter 6 failed when the need for recumbent mountain bikes appeared. This hierarchy already contains subclasses for MountainBike and RecumbentBike; combining the qualities of these two classes into a single object is not possible in the hierarchy as it currently exists. You cannot reuse existing behavior without changing it.

The other side of the exemplary coin is the chaos that ensues when novice programmers attempt to extend incorrectly modeled hierarchies. These inadequate hierarchies should not be extended, they need to be refactored, but novices do not have the skills to do so. Novices are forced to duplicate existing code or to add dependencies on class names, both of which serve to exacerbate existing design problems.

Inheritance, therefore, is a place where the question “What will happen when I’m wrong?” assumes special importance. Inheritance by definition comes with a deeply embedded set of dependencies. Subclasses depend on the methods defined in their superclasses and on the automatic delegation of messages to those superclasses. This is classical inheritance’s greatest strength and biggest weakness; subclasses are bound, irrevocably and by design, to the classes above them in the hierarchy. These built-in dependencies amplify the effects of modifications made to superclasses. 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.

Finally, your consideration of the use of inheritance should be tempered by your expectations about the population who will use your code. If you are writing code for an in-house application in a domain with which you are intimately familiar, you may be able to predict the future well enough to be confident that your design problem is one for which inheritance is a cost-effective solution. As you write code for a wider audience, your ability to anticipate needs necessarily decreases and the suitability of requiring inheritance as part of the interface goes down.

Avoid writing frameworks that require users of your code to subclass your objects 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.

Accepting the Consequences of Composition

Objects built using composition differ from those built using inheritance in two basic ways. Composed objects do not depend on the structure of the class hierarchy, and they delegate their own messages. These differences confer a different set of costs and benefits.

Benefits of Composition

When using composition, the natural tendency is to create many small objects that contain straightforward responsibilities that are accessible through clearly defined interfaces. These well-composed objects excel when measured against several of Chapter 2’s goals for code.

These small objects have a single responsibility and specify their own behavior. They are transparent; it’s easy to understand the code and it’s clear what will happen if it changes. Also, the composed object’s independence from the hierarchy means that it inherits very little code and so is generally immune from suffering side effects as a result of changes to classes above it in the hierarchy.

Because composed objects deal with their parts via an interface, adding a new kind of part is a simple matter of plugging in a new object that honors the interface. From the point of view of the composed object, adding a new variant of an existing part is reasonable and requires no changes to its code.

By their very nature, objects that participate in composition are small, structurally independent, and have well-defined interfaces. This allows their seamless transition into pluggable, interchangeable components. Well-composed objects are therefore easily usable in new and unexpected contexts.

At its best, composition results in applications built of simple, pluggable objects that are easy to extend and have a high tolerance for change.

Costs of Composition

Compositions strengths, as with most things in life, contribute to its weaknesses.

A composed object relies on its many parts. Even if each part is small and easily understood, the combined operation of the whole may be less than obvious. While every individual part may indeed be transparent, the whole may not be.

The benefits of structural independence are gained at the cost of automatic message delegation. The composed object must explicitly know which messages to delegate and to whom. Identical delegation code many be needed by many different objects; composition provides no way to share this code.

As these costs and benefits illustrate, composition is excellent at prescribing rules for assembling an object made of parts but doesn’t provide as much help for the problem of arranging code for a collection of parts that are very nearly identical.

Choosing Relationships

Classical inheritance (Chapter 6), behavior sharing via modules (Chapter 7, Sharing Role Behavior with Modules) and composition are each the perfect solution for the problem they solve. The trick to lowering your application costs is to apply each technique to the right problem.

Some of the grand masters of object-oriented design have given advice about using inheritance and composition.

• “Inheritance is specialization.”—Bertrand Meyer, Touch of Class: Learning to Program Well with Objects and Contracts

• “Inheritance is best suited to adding functionally to existing classes when you will use most of the old code and add relatively small amounts of new code.” ——Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software

• “Use composition when the behavior is more than the sum of it’s parts.”—paraphrase of Grady Booch, Object-Oriented Analysis and Design

Use Inheritance for is-a Relationships

When you select inheritance over composition you are placing a bet that the benefits thereby accrued will outweigh the costs. Some bets are more likely to pay off than others. Small sets of real-world objects that fall naturally into static, transparently obvious specialization hierarchies are candidates to be modeled using classical inheritance.

Imagine a game where players race bicycles. Players assemble their bikes by “buying” parts. One of the parts they can buy is a shock. The game provides six nearly identical shocks; each differs slightly in cost and behavior.

All of these shocks are, well, shocks. Their “shock-ness” is at the core of their identity. Shocks exist in no more atomic category. Variants of shocks are far more alike than they are different. The most accurate and descriptive statement that you can make about any one of the variants is that it is-a shock.

Inheritance is perfect for this problem. Shocks can be modeled as a shallow narrow hierarchy. The hierarchy’s small size makes it understandable, intention revealing, and easily extendable. Because these objects meet the criteria for successful use of inheritance, the risk of being wrong is low, but in the unlikely event that you are wrong, the cost of changing your mind is also low. You can achieve the benefits of inheritance while exposing yourself to few of its risks.

In terms of this Chapter’s example, each different shock plays the role of Part. It inherits common shock behavior and the Part role from its abstract Shock superclass. The PartsFactory currently assumes that every part can be represented by the Part OpenStruct, but you could easily extend the part configuration array to supply the class name for a specific shock. Because you already think of Part as an interface, it’s easy to plug in a new kind of part, even if this part uses inheritance to get some of its behavior.

If requirements change such that there is an explosion in the kinds of shocks, reassess this design decision. Perhaps it still holds, perhaps not. If modeling a bevy of new shocks requires dramatically expanding the hierarchy, or if the new shocks don’t conveniently fit into the existing code, reconsider alternatives at that time.

Use Duck Types for behaves-like-a Relationships

Some problems require many different objects to play a common role. In addition to their core responsibilities, objects might play roles like schedulable, preparable, printable, or persistable.

There are two key ways to recognize the existence of a role. First, although an object plays it, the role is not the object’s main responsibility. A bicycle behaves-like-a schedulable but it is-a bicycle. Second, the need is widespread; many otherwise unrelated objects share a desire to play the same role.

The most illuminating way to think about roles is from the outside, from the point of view of a holder of a role player rather than that of a player of a role. The holder of a schedulable expects it to implement Schedulable’s interface and to honor Schedulable’s contract. All schedulables are alike in that they must meet these expectations.

Your design task is to recognize that a role exists, define the interface of its duck type and provide an implementation of that interface for every possible player. Some roles consist only of their interface, others share common behavior. Define the common behavior in a Ruby module to allow objects to play the role without duplicating the code.

Use Composition for has-a Relationships

Many objects contain numerous parts but are more than the sums of those parts. Bicycles have-a Parts, but the bike itself is something more. It has behavior that is separate from and in addition to the behavior of its parts. Given the current requirements of the bicycle example, the most cost-effective way to model the Bicycle object is via composition.

This is-a versus has-a distinction is at the core of deciding between inheritance and composition. The more parts an object has, the more likely it is that it should be modeled with composition. The deeper you drill down into individual parts, the more likely it is that you’ll discover a specific part that has a few specialized variants and is thus a reasonable candidate for inheritance. For every problem, assess the costs and benefits of alternative design techniques and use your judgment and experience to make the best choice.

Summary

Composition allows you to combine small parts to create more complex objects such that the whole becomes more than the sum of its parts. Composed objects tend to consist of simple, discrete entities that can easily be rearranged into new combinations. These simple objects are easy to understand, reuse, and test, but because they combine into a more complicated whole, the operation of the bigger application may not be as easy to understand as that of the individual parts.

Composition, classical inheritance, and behavior sharing via modules are competing techniques for arranging code. Each has different costs and benefits; these differences predispose them to be better at solving slightly different problems.

These techniques are tools, nothing more, and you’ll become a better designer if you practice each of them. Learning to use them properly is a matter of experience and judgment, and one of the best ways to gain experience is to learn from your own mistakes. The key to improving your design skills is to attempt these techniques, accept your errors cheerfully, remain detached from past design decisions, and refactor mercilessly.

As you gain experience, you’ll get better at choosing the correct technique the first time, your costs will go down, and your applications will improve.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.35.193