Chapter 6. Acquiring Behavior Through Inheritance

Well-designed applications are constructed of reusable code. Small, trustworthy self-contained objects with minimal context, clear interfaces, and injected dependencies are inherently reusable. This book has, up to now, concentrated on creating objects with exactly these qualities.

Most object-oriented languages, however, have another code sharing technique, one built into the very syntax of the language: inheritance. This chapter offers a detailed example of how to write code that properly uses inheritance. Its goal is to teach you to build a technically sound inheritance hierarchy; its purpose is to prepare you to decide if you should.

Once you understand how to use classical inheritance, the concepts are easily transferred to other inheritance mechanisms. Inheritance is thus a topic for two chapters. This chapter contains a tutorial that illustrates how to write inheritable code. Chapter 7, Sharing Role Behavior with Modules, expands these techniques to the problem of sharing code via Ruby modules.

Understanding Classical Inheritance

The idea of inheritance may seem complicated but as with all complexity, there’s a simplifying abstraction. Inheritance is, at its core, a mechanism for automatic message delegation. It defines a forwarding path for not-understood messages. It creates relationships such that, if one object cannot respond to a received message, it delegates that message to another. You don’t have to write code to explicitly delegate the message, instead you define an inheritance relationship between two objects and the forwarding happens automatically.

In classical inheritance these relationships are defined by creating subclasses. Messages are forwarded from subclass to superclass; the shared code is defined in the class hierarchy.

The term classical is a play on the word class, not a nod to an archaic technique, and it serves to distinguish this superclass/subclass mechanism from other inheritance techniques. JavaScript, for example, has prototypical inheritance and Ruby has modules (more on modules in the next chapter), both of which also provide a way to share code via automatic delegation of messages.

The uses and misuses of inheritance are best understood by example, and this chapter’s example provides a thorough grounding in the techniques of classical inheritance. The example begins with a single class and goes through a number of refactorings to reach a satisfactory set of subclasses. Each step is small and easily understood but it takes a whole chapter’s worth of code to illustrate all of the ideas.

Recognizing Where to Use Inheritance

The first challenge is recognizing where inheritance would be useful. This section illustrates how to know when you have the problem that inheritance solves.

Assume that FastFeet leads road bike trips. Road bicycles are lightweight, curved handlebar (drop bar), skinny tired bikes that are meant for paved roads. Figure 6.1 shows a road bike.

Image

Figure 6.1. A lightweight, drop-bar, skinny tired road bike.

Mechanics are responsible for keeping bicycles running (no matter how much abuse customers heap upon them), and they take an assortment of spare parts on every trip. The spares they need depend on which bicycles they take.

Starting with a Concrete Class

FastFeet’s application already has a Bicycle class, shown below. Every road bike that’s going on a trip is represented by an instance of this class.

Bikes have an overall size, a handlebar tape color, a tire size, and a chain type. Tires and chains are integral parts and so spares must always be taken. Handlebar tape may seem less necessary, but in real life it is just as required. No self-respecting cyclist would tolerate dirty or torn bar tape; mechanics must carry spare tape in the correct, matching color.


 1  class Bicycle
 2    attr_reader :size, :tape_color
 3 
 4    def initialize(args)
 5      @size       = args[:size]
 6      @tape_color = args[:tape_color]
 7    end
 8 
 9    # every bike has the same defaults for
10    # tire and chain size
11    def spares
12      { chain:        '10-speed',
13        tire_size:    '23',
14        tape_color:   tape_color}
15    end
16 
17    # Many other methods...
18  end
19 
20  bike = Bicycle.new(
21          size:       'M',
22          tape_color: 'red' )
23 
24  bike.size     # -> 'M'
25  bike.spares
26  # -> {:tire_size   => "23",
27  #     :chain       => "10-speed",
28  #     :tape_color  => "red"}


Bicycle instances can respond to the spares, size, and tape_color messages and a Mechanic can figure out what spare parts to take by asking each Bicycle for its spares. Despite the fact that the spares method commits the sin of embedding default strings directly inside itself, the above code is fairly reasonable. This model of a bicycle is obviously missing a few bolts and is not something you could actually ride, but it will do for this chapter’s example.

This class works just fine until something changes. Imagine that FastFeet begins to lead mountain bike trips.

Mountain bikes and road bikes are much alike but there are clear differences between them. Mountain bikes are meant to be ridden on dirt paths instead of paved roads. They have sturdy frames, fat tires, straight-bar handlebars (with rubber hand grips instead of tape), and suspension. The bicycle in Figure 6.2 has front suspension only, but some mountain bikes also have rear, or “full” suspension.

Image

Figure 6.2. A beefy, straight-bar, front-suspension, fat-tired mountain bike.

Your design task is to add support for mountain bikes to FastFeet’s application.

Much of the behavior that you need already exists; mountain bikes are definitely bicycles. They have an overall bike size and a chain and tire size. The only differences between road and mountain bikes are that road bikes need handlebar tape and mountain bikes have suspension.

Embedding Multiple Types

When a preexisting concrete class contains most of the behavior you need, it’s tempting to solve this problem by adding code to that class. This next example does just that, it changes the existing Bicycle class so that spares works for both road and mountain bikes.

As you see below, three new variables have been added, along with their corresponding accessors. The new front_shock and rear_shock variables hold mountain bike specific parts. The new style variable determines which parts appear on the spares list. Each of these new variables is handled properly by the initialize method.

The code to add these three variables is simple, even mundane; the change to spares proves more interesting. The spares method now contains an if statement that checks the contents of the variable style. This style variable acts to divide instances of Bicycle into two different categories—those whose style is :road and those whose style is anything else.

If any alarms are going off as you review this code, please be reassured, they will soon be silenced. This example is simply a detour that illustrates an antipattern, that is, a common pattern that appears to be beneficial but is actually detrimental, and for which there is a well-known alternative.


Note

In case you’re confused by the tire sizes below, know that bicycle tire sizing is, by tradition, inconsistent. Road bikes originated in Europe and use metric sizing; a 23-millimeter tire is slightly less than an inch wide. Mountain bikes originated in the United States and give tire sizes in inches. In the example below, the 2.1-inch mountain bike tire is more than twice as wide as the 23 mm road bike tire.



 1  class Bicycle
 2    attr_reader :style, :size, :tape_color,
 3                :front_shock, :rear_shock
 4 
 5    def initialize(args)
 6      @type        = args[:style]
 7      @size        = args[:size]
 8      @tape_color  = args[:tape_color]
 9      @front_shock = args[:front_shock]
10      @rear_shock  = args[:rear_shock]
11    end
12 
13    # checking "style" starts down a slippery slope
14    def spares
15      if style == :road
16        { chain:        '10-speed',
17          tire_size:    '23',       # milimeters
18          tape_color:   tape_color }
19      else
20        { chain:        '10-speed',
21          tire_size:    '2.1',      # inches
22          rear_shock:   rear_shock }
23      end
24    end
25  end
26 
27  bike = Bicycle.new(
28          style:        :mountain,
29          size:         'S',
30          front_shock:  'Manitou',
31          rear_shock:   'Fox')
32 
33  bike.spares
34  # -> {:tire_size   => "2.1",
35  #     :chain       => "10-speed",
36  #     :rear_shock  => 'Fox'}


This code makes decisions about spare parts based on the value held in style; structuring the code this way has many negative consequences. If you add a new style you must change the if statement. If you write careless code where the last option is the default (as does the code above) an unexpected style will do something but perhaps not what you expect. Also, the spares method started out containing embedded default strings, some of these strings are now duplicated on each side of the if statement.

Bicycle has an implied public interface that includes spares, size, and all the individual parts. The size method still works, spares generally works, but the parts methods are now unreliable. It’s impossible to predict, for any specific instance of Bicycle, whether a specific part has been initialized. Objects holding onto an instance of Bicycle may, for example, be tempted to check style before sending it tape_color or rear_shock.

The code wasn’t great to begin with; this change did nothing to improve it.

The initial Bicycle class was imperfect but its imperfections were hidden—encapsulated within the class. These new flaws have broader consequences. Bicycle now has more than one responsibility, contains things that might change for different reasons, and cannot be reused as is.

This pattern of coding will lead to grief but is not without value. It vividly illustrates an antipattern that, once noticed, suggests a better design.

This code contains an if statement that checks an attribute that holds the category of self to determine what message to send to self. This should bring back memories of a pattern discussed in the previous chapter on duck typing, where you saw an if statement that checked the class of an object to determine what message to send to that object.

In both of these patterns an object decides what message to send based on a category of the receiver. You can think of the class of an object as merely a specific case of an attribute that holds the category of self; considered this way, these patterns are the same. In each case if the sender could talk it would be saying “I know who you are and because of that I know what you do.” This knowledge is a dependency that raises the cost of change.

Be on the lookout for this pattern. While sometimes innocent and occasionally defensible, its presence might be exposing a costly flaw in your design. Chapter 5, Reducing Costs with Duck Typing, used this pattern to discover a missing duck type; here the pattern indicates a missing subtype, better known as a subclass.

Finding the Embedded Types

The if statement in the spares method above switches on a variable named style, but it would have been just as natural to call that variable type or category. Variables with these kinds of names are your cue to notice the underlying pattern. Type and category are words perilously similar to those you would use when describing a class. After all, what is a class if not a category or type?

The style variable effectively divides instances of Bicycle into two different kinds of things. These two things share a great deal of behavior but differ along the style dimension. Some of Bicycle's behavior applies to all bicycles, some only to road bikes, and some only to mountain bikes. This single class contains several different, but related, types.

This is the exact problem that inheritance solves; that of highly related types that share common behavior but differ along some dimension.

Choosing Inheritance

Before proceeding to the next example it’s worth examining inheritance in more detail. Inheritance may seem like a mysterious art but, like most design ideas, it’s simple when looked from the right perspective.

It goes without saying that objects receive messages. No matter how complicated the code, the receiving object ultimately handles any message in one of two ways. It either responds directly or it passes the message on to some other object for a response.

Inheritance provides a way to define two objects as having a relationship such that when the first receives a message that it does not understand, it automatically forwards, or delegates, the message to the second. It’s as simple as that.

The word inheritance suggests a biological family tree where a few progenitors sit at the top and descendents branch off below. This family tree image is, however, a bit misleading. In many parts of the biological world it’s common for descendents to have two ancestors. You, for example, quite likely have two parents. Languages that allow objects to have multiple parents are described as having multiple inheritance and the designers of these languages face interesting challenges. When an object with multiple parents receives a message that it does not understand, to which parent ought it forward that message? If more than one of its parents implements the message, which implementation has priority? As you might guess, things get complicated quickly.

Many object-oriented languages sidestep these complications by providing single inheritance, whereby a subclass is allowed only one parent superclass. Ruby does this; it has single inheritance. A superclass may have many subclasses, but each subclass is permitted only one superclass.

Message forwarding via classical inheritance takes place between classes. Because duck types cut across classes, they do not use classical inheritance to share common behavior. Duck types share code via Ruby modules (more on modules in the next chapter).

Even if you have never explicitly created a class hierarchy of your own, you use inheritance. When you define a new class but do not specify its superclass, Ruby automatically sets your new class’s superclass to Object. Every class you create is, by definition, a subclass of something.

You also already benefit from automatic delegation of messages to superclasses. When an object receives a message it does not understand, Ruby automatically forwards that message up the superclass chain in search of a matching method implementation. A simple example is illustrated in Figure 6.3, which shows how Ruby objects respond to the nil? message.

Image

Figure 6.3. NilClass answers true to nil?, string (and all others) answer false.

Remember that in Ruby, nil is an instance of class NilClass; it’s an object like any other. Ruby contains two implementations of nil?, one in NilClass, and the other in Object. The implementation in NilClass unconditionally returns true, the one in Object, false.

When you send nil? to an instance of NilClass, it, obviously, answers true. When you send nil? to anything else, the message travels up the hierarchy from one superclass to the next until it reaches Object, where it invokes the implementation that answers false. Thus, nil reports that it is nil and all other objects report that they are not. This elegantly simple solution illustrates the power and usefulness of inheritance.

That fact that unknown messages get delegated up the superclass hierarchy implies that subclasses are everything their superclasses are, plus more. An instance of String is a String, but it’s also an Object. Every String is assumed to contain Object’s entire public interface and must respond appropriately to any message defined in that interface. Subclasses are thus specializations of their superclasses.

The current Bicycle example embeds multiple types inside the class. It’s time to abandon this code and revert to the original version of Bicycle. Perhaps mountain bikes are a specialization of Bicycle; perhaps this design problem can be solved using inheritance.

Drawing Inheritance Relationships

Just as you used UML sequence diagrams to communicate message passing in Chapter 4, Creating Flexible Interfaces, you can use UML class diagrams to illustrate class relationships.

Figure 6.4 contains a class diagram. The boxes represent classes. The connecting line indicates that the classes are related. The hollow triangle means that the relationship is inheritance. The pointed end of the triangle is attached to the box containing the superclass. Thus, the figure shows Bicycle as a superclass of MountainBike.

Image

Figure 6.4. MountainBike is a subclass of bicycle.

Misapplying Inheritance

Under the premise that the journey is more useful than the destination, and that experiencing common mistakes by proxy is less painful than experiencing them in person, this next section continues to show code that is unworthy of emulation. The code illustrates common difficulties encountered by novices. If you are practiced at using inheritance and are comfortable with these techniques, feel free to skim. However, if you are new to inheritance, or you find that all of your attempts go awry, then follow along carefully.

The following is a first attempt at a MountainBike subclass. This new subclass is a direct descendent of the original Bicycle class. It implements two methods, initialize and spares. Both of these methods are already implemented in Bicycle, therefore, they are said to be overridden by MountainBike.

In the following code each of the overridden methods sends super.


 1  class MountainBike < Bicycle
 2    attr_reader :front_shock, :rear_shock
 3 
 4    def initialize(args)
 5      @front_shock = args[:front_shock]
 6      @rear_shock  = args[:rear_shock]
 7      super(args)
 8    end
 9 
10    def spares
11      super.merge(rear_shock: rear_shock)
12    end
13  end


Sending super in any method passes that message up the superclass chain. Thus, for example, the send of super in MountainBike’s initialize method (line 7 above) invokes the initialize method of its superclass, Bicycle.

Jamming the new MountainBike class directly under the existing Bicycle class was blindly optimistic, and, predictably, running the code exposes several flaws. Instances of MountainBike have some behavior that just doesn’t make sense. The following example shows what happens if you ask a MountainBike for its size and spares. It reports its size correctly but says that it has skinny tires and implies that it needs handlebar tape, both of which are incorrect.


 1  mountain_bike = MountainBike.new(
 2                    size:         'S',
 3                    front_shock:  'Manitou',
 4                    rear_shock:   'Fox')
 5 
 6  mountain_bike.size # -> 'S'
 7 
 8  mountain_bike.spares
 9  # -> {:tire_size   => "23",       <- wrong!
10  #     :chain       => "10-speed",
11  #     :tape_color  => nil,        <- not applicable
12  #     :front_shock => 'Manitou',
13  #     :rear_shock  => "Fox"}


It comes as no surprise that instances of MountainBike contain a confusing mishmash of road and mountain bike behavior. The Bicycle class is a concrete class that was not written to be subclassed. It combines behavior that is general to all bicycles with behavior that is specific to road bikes. When you slam MountainBike under Bicycle, you inherit all of this behavior—the general and the specific, whether it applies or not.

Figure 6.5 takes serious liberties with class diagrams to illustrate this idea. It shows road bike behavior embedded inside of Bicycle. The way this code is arranged causes MountainBike to inherit behavior that it does not want or need.

Image

Figure 6.5. Bicycle combines general bicycle behavior with specific road bike behavior.

The Bicycle class contains behavior that is appropriate for both a peer and a parent of MountainBike. Some of the behavior in Bicycle is correct for MountainBike, some is wrong, and some doesn’t even apply. As written, Bicycle should not act as the superclass of MountainBike.

Because design is evolutionary, this situation arises all the time. The problem here started with the names of these classes.

Finding the Abstraction

In the beginning, there was one idea, a bicycle, and it was modeled as a single class, Bicycle. The original designer chose a generic name for an object that was actually slightly more specialized. The existing Bicycle class doesn’t represent just any kind of bicycle, it represents a specific kind—a road bike.

This naming choice is perfectly appropriate in an application where every Bicycle is a road bike. When there’s only one kind of bike, choosing RoadBike for the class name is unnecessary, perhaps even overly specific. Even if you suspect that you will someday have mountain bikes, Bicycle is a fine choice for the first class name, and is sufficient unto the day.

However, now that MountainBike exists, Bicycle’s name is misleading. These two class names imply inheritance; you immediately expect MountainBike to be a specialization of Bicycle. It’s natural to write code that creates MountainBike as a subclass of Bicycle. This is the right structure, the class names are correct, but the code in Bicycle is now very wrong.

Subclasses are specializations of their superclasses. A MountainBike should be everything a Bicycle is, plus more. Any object that expects a Bicycle should be able to interact with a MountainBike in blissful ignorance of its actual class.

These are the rules of inheritance; break them at your peril. For inheritance to work, two things must always be true. First, the objects that you are modeling must truly have a generalization–specialization relationship. Second, you must use the correct coding techniques.

It makes perfect sense to model mountain bike as a specialization of bicycle; the relationship is correct. However, the code above is a mess and if propagated will lead to disaster. The current Bicycle class intermingles general bicycle code with specific road bike code. It’s time to separate these two things, to move the road bike code out of Bicycle and into a separate RoadBike subclass.

Creating an Abstract Superclass

Figure 6.6 shows a new class diagram where Bicycle is the superclass of both MountainBike and RoadBike. This is your goal; it’s the inheritance structure you intend to create. Bicycle will contain the common behavior, and MountainBike and RoadBike will add specializations. Bicycle’s public interface should include spares and size, and the interfaces of its subclasses will add their individual parts.

Image

Figure 6.6. Bicycle as the superclass of MountainBike and RoadBike.

Bicycle now represents an abstract class. Chapter 3, Managing Dependencies, defined abstract as being disassociated from any specific instance, and that definition still holds true. This new version of Bicycle will not define a complete bike, just the bits that all bicycles share. You can expect to create instances of MountainBike and RoadBike, but Bicycle is not a class to which you would ever send the new message. It wouldn’t make sense; Bicycle no longer represents a whole bike.

Some object-oriented programming languages have syntax that allows you to explicitly declare classes as abstract. Java, for example, has the abstract keyword. The Java compiler itself prevents creation of instances of classes to which this keyword has been applied. Ruby, in line with its trusting nature, contains no such keyword and enforces no such restriction. Only good sense prevents other programmers from creating instances of Bicycle; in real life, this works remarkably well.

Abstract classes exist to be subclassed. This is their sole purpose. They provide a common repository for behavior that is shared across a set of subclasses—subclasses that in turn supply specializations.

It almost never makes sense to create an abstract superclass with only one subclass. Even though the original Bicycle class contains general and specific behavior and it’s possible to imagine modeling it as two classes from the very beginning, do not. Regardless of how strongly you anticipate having other kinds of bikes, that day may never come. Until you have a specific requirement that forces you to deal with other bikes, the current Bicycle class is good enough.

Even though you now have a requirement for two kinds of bikes, this still may not be the right moment to commit to inheritance. Creating a hierarchy has costs; the best way to minimize these costs is to maximize your chance of getting the abstraction right before allowing subclasses to depend on it. While the two bikes you know about supply a fair amount of information about the common abstraction, three bikes would supply a great deal more. If you could put this decision off until FastFeet asked for a third kind of bike, your odds of finding the right abstraction would improve dramatically.

A decision to put off the creation of the Bicycle hierarchy commits you to writing MountainBike and RoadBike classes that duplicate a great deal of code. A decision to proceed with the hierarchy accepts the risk that you may not yet have enough information to identify the correct abstraction. Your choice about whether to wait or to proceed hinges on how soon you expect a third bike to appear versus how much you expect the duplication to cost. If a third bike is imminent, it may be best to duplicate the code and wait for better information. However, if the duplicated code would need to change every day, it may be cheaper to go ahead and create the hierarchy. You should wait, if you can, but don’t fear to move forward based on two concrete cases if this seems best.

For now, assume you have good reason to create a Bicycle hierarchy even though you only know about two bikes. The first step in creating the new hierarchy is to make a class structure that mirrors Figure 6.6. Ignoring the rightness of the code for a moment, the simplest way to make this change is to rename Bicycle to RoadBike and to create a new, empty Bicycle class. The following example does just that.


 1  class Bicycle
 2    # This class is now empty.
 3    # All code has been moved to RoadBike.
 4  end
 5 
 6  class RoadBike < Bicycle
 7    # Now a subclass of Bicycle.
 8    # Contains all code from the old Bicycle class.
 9  end
10 
11  class MountainBike < Bicycle
12    # Still a subclass of Bicycle (which is now empty).
13    # Code has not changed.
14  end


The new RoadBike class is defined as a subclass of Bicycle. The existing MountainBike class already subclassed Bicycle. Its code did not change, but its behavior certainly has because its superclass is now empty. Code that MountainBike depends on has been removed from its parent and placed in a peer.

This code rearrangement merely moved the problem, as illustrated in Figure 6.7. Now, instead of containing too much behavior, Bicycle contains none at all. The common behavior needed by all bicycles is stuck down inside of RoadBike and is therefore inaccessible to MountainBike.

Image

Figure 6.7. Now RoadBike contains all the common behavior.

This rearrangement improves your lot because it’s easier to promote code up to a superclass than to demote it down to a subclass. The reasons for this are not yet obvious but will become so as the example proceeds.

The next few iterations concentrate on achieving this new class structure by moving common behavior into Bicycle and using that behavior effectively in the subclasses.

RoadBike still contains everything it needs and thus it still works, but MountainBike is now seriously broken. As an example, here’s what happens if you create instances of each subclass and ask them for size. RoadBike returns the correct response, MountainBike just blows up.


 1  road_bike = RoadBike.new(
 2                size:       'M',
 3                tape_color: 'red' )
 4 
 5  road_bike.size  # => "M"
 6 
 7  mountain_bike = MountainBike.new(
 8                    size:         'S',
 9                    front_shock:  'Manitou',
10                    rear_shock:   'Fox')
11 
12  mountain_bike.size
13  # NoMethodError: undefined method 'size'


It’s obvious why this error occurs; neither MountainBike nor any of its superclasses implement size.

Promoting Abstract Behavior

The size and spares methods are common to all bicycles. This behavior belongs in Bicycle’s public interface. Both methods are currently stuck down in RoadBike; the task here is to move them up to Bicycle so the behavior can be shared. Because the code dealing with size is simplest it’s the most natural place to start.

Promoting size behavior to the superclass requires three changes as shown in the example below. The attribute reader and initialization code move from RoadBike to Bicycle (lines 2 and 5), and RoadBike’s initialize method adds a send of super (line 14).


 1  class Bicycle
 2    attr_reader :size     # <- promoted from RoadBike
 3 
 4    def initialize(args={})
 5      @size = args[:size] # <- promoted from RoadBike
 6    end
 7  end
 8 
 9  class RoadBike < Bicycle
10    attr_reader :tape_color
11 
12    def initialize(args)
13      @tape_color = args[:tape_color]
14      super(args)  # <- RoadBike now MUST send 'super'
15    end
16    # ...
17  end


RoadBike now inherits the size method from Bicycle. When a RoadBike receives size, Ruby itself delegates the message up the superclass chain, searching for an implementation and finding the one in Bicycle. This message delegation happens automatically because RoadBike is a subclass of Bicycle.

Sharing the initialization code that sets the @size variable, however, requires a bit more from you. This variable is set in Bicycle’s initialize method, a method that RoadBike also implements, or overrides.

When RoadBike overrides initialize, it provides a receiver for this message, one that perfectly satisfies Ruby and prevents the message’s automatic delegation to Bicycle. If both initialize methods need to be run, RoadBike is now obligated to do the delegation itself; it must send super to explicitly pass this message on to Bicycle, as it did in line 14 above.

Before this change, RoadBike responded correctly to size but MountainBike did not. The behavior they share in common in now defined in Bicycle, their common superclass. The magic of inheritance is such that both now respond correctly to size as shown below.


 1  road_bike = RoadBike.new(
 2                size:       'M',
 3                tape_color: 'red' )
 4 
 5  road_bike.size  # -> ""M""
 6 
 7  mountain_bike = MountainBike.new(
 8                    size:         'S',
 9                    front_shock:  'Manitou',
10                    rear_shock:   'Fox')
11 
12  mountain_bike.size # -> 'S'


The alert reader will notice the code that handles bicycle size has been moved twice. It was in the original Bicycle class, got moved down to RoadBike, and now has been promoted back up to Bicycle. The code has not changed; it has just been moved twice.

You might be tempted to skip the middleman and just leave this bit of code in Bicycle to begin with, but this push-everything-down-and-then-pull-some-things-up strategy is an important part of this refactoring. Many of the difficulties of inheritance are caused by a failure to rigorously separate the concrete from the abstract. Bicycle’s original code intermingled the two. If you begin this refactoring with that first version of Bicycle, attempting to isolate the concrete code and push it down to RoadBike, any failure on your part will leave dangerous remnants of concreteness in the superclass. However, if you start by moving every bit of the Bicycle code to RoadBike, you can then carefully identify and promote the abstract parts without fear of leaving concrete artifacts.

When deciding between refactoring strategies, indeed, when deciding between design strategies in general, it’s useful to ask the question: “What will happen if I’m wrong?” In this case, if you create an empty superclass and push the abstract bits of code up into it, the worst that can happen is that you will fail to find and promote the entire abstraction.

This “promotion” failure creates a simple problem, one that is easily found and easily fixed. When a bit of the abstraction gets left behind, the oversight becomes visible as soon as another subclass needs the same behavior. In order to give all subclasses access to the behavior you’ll be forced to either duplicate the code (in each subclass) or promote it (to the common superclass). Because even the most junior programmers have been taught not to duplicate code, this problem gets noticed no matter who works on the application in the future. The natural course of events is such that the abstraction gets identified and promoted, and the code improves. Promotion failures thus have low consequences.

However, if you attempt this refactoring from the opposite direction, trying to convert an existing class from concrete to abstract by pushing just the concrete parts down into a new subclass, you might accidentally leave remnants of concrete behavior behind. By definition this leftover concrete behavior does not apply to every possible new subclass. Subclasses thus begin to violate the basic rule of inheritance; they are not truly specializations of their superclasses. The hierarchy becomes untrustworthy.

Untrustworthy hierarchies force objects that interact with them to know their quirks. Inexperienced programmers do not understand and cannot fix a faulty hierarchy; when asked to use one they will embed knowledge of its quirks into their own code, often by explicitly checking the classes of objects. Knowledge of the structure of the hierarchy leaks into the rest of the application, creating dependencies that raise the cost of change. This is not a problem you want to leave behind. The consequences of a demotion failure can be widespread and severe.

The general rule for refactoring into a new inheritance hierarchy is to arrange code so that you can promote abstractions rather than demote concretions.

In light of this discussion, the question posed a few paragraphs ago might more usefully be phrased: “What will happen when I’m wrong?” Every decision you make includes two costs: one to implement it and another to change it when you discover that you were wrong. Taking both costs into account when choosing among alternatives motivates you to make conservative choices that minimize the cost of change.

With this in mind, turn your attention to spares.

Separating Abstract from Concrete

RoadBike and MountainBike both implement a version of spares. RoadBike’s definition (repeated below) is the original one that was copied from the concrete Bicycle class. It is self-contained and thus still works.


 1  class RoadBike < Bicycle
 2    # ...
 3    def spares
 4      { chain:        '10-speed',
 5        tire_size:    '23',
 6        tape_color:   tape_color}
 7    end
 8  end


The spares definition in MountainBike (also repeated below) is leftover from the first attempt at subclassing. This method sends super, expecting a superclass to also implement spares.


 1  class MountainBike < Bicycle
 2    # ...
 3    def spares
 4      super.merge({rear_shock:  rear_shock})
 5    end
 6  end


Bicycle, however, does not yet implement the spares method, so sending spares to a MountainBike results in the following NoMethodError exception:


 1  mountain_bike.spares
 2  # NoMethodError: super: no superclass method 'spares'


Fixing this problem obviously requires adding a spares method to Bicycle, but doing so is not as simple as promoting the existing code from RoadBike.

RoadBike’s spares implementation knows far too much. The chain and tire_size attributes are common to all bicycles, but tape_color should be known only to road bikes. The hard-coded chain and tire_size values are not the correct defaults for every possible subclass. This method has many problems and cannot be promoted as is.

It mixes a bunch of different things. When this awkward mix was hidden inside a single method of a single class it was survivable, even (depending on your tolerance) ignorable, but now that you would like to share only part of this behavior, you must untangle the mess and separate the abstract parts from the concrete parts. The abstractions will be promoted up to Bicycle, the concrete parts will remain in RoadBike.

Put away thoughts of the overall spares method for a moment and concentrate on promoting just the pieces that all bicycles share, chain and tire_size. They are attributes, like size, and should be represented by accessors and setters instead of hard-coded values. Here are the requirements:

• Bicycles have a chain and a tire size.

• All bicycles share the same default for chain.

Subclasses provide their own default for tire size.

• Concrete instances of subclasses are permitted to ignore defaults and supply instance-specific values.

The code for similar things should follow a similar pattern. Here’s new code that handles size, chain, and tire_size in a similar way.


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


RoadBike and MountainBike inherit the attr_reader definitions in Bicycle and both send super in their initialize methods. All bikes now understand size, chain, and tire_size and each may supply subclass-specific values for these attributes. The first and last requirements listed above have been met.

Despite the buildup, there’s nothing special about this code. Good sense suggests that it should have been written like this in the beginning; it’s high time this version appeared. It is inheritable by subclasses, certainly, but nothing about the code suggests that it expects to be inherited.

Meeting the two requirements that deal with defaults, however, adds something interesting.

Using the Template Method Pattern

This next change alters Bicycle’s initialize method to send messages to get defaults. There are two new messages, default_chain and default_tire_size, in lines 6 and 7 below.

While wrapping the defaults in methods is good practice in general, these new message sends serve a dual purpose. Bicycle’s main goal in sending these messages is to give subclasses an opportunity to contribute specializations by overriding them.

This technique of defining a basic structure in the superclass and sending messages to acquire subclass-specific contributions is known as the template method pattern.

In the following code, MountainBike and RoadBike take advantage of only one of these opportunities for specialization. Both implement default_tire_size, but neither implements default_chain. Each subclass thus supplies its own default for tire size but inherits the common default for chain.


 1  class Bicycle
 2    attr_reader :size, :chain, :tire_size
 3 
 4    def initialize(args={})
 5      @size       = args[:size]
 6      @chain      = args[:chain]     || default_chain
 7      @tire_size  = args[:tire_size] || default_tire_size
 8    end
 9 
10    def default_chain       # <- common default
11      '10-speed'
12    end
13  end
14 
15  class RoadBike < Bicycle
16    # ...
17    def default_tire_size   # <- subclass default
18      '23'
19    end
20  end
21 
22  class MountainBike < Bicycle
23    # ...
24    def default_tire_size   # <- subclass default
25      '2.1'
26    end
27  end


Bicycle now provides structure, a common algorithm if you will, for its subclasses. Where it permits them to influence the algorithm, it sends messages. Subclasses contribute to the algorithm by implementing matching methods.

All bicycles now share the same default for chain but use different defaults for tire size, as shown below:


 1  road_bike = RoadBike.new(
 2                size:       'M',
 3                tape_color: 'red' )
 4 
 5  road_bike.tire_size     # => '23'
 6  road_bike.chain         # => "10-speed"
 7 
 8  mountain_bike = MountainBike.new(
 9                    size:         'S',
10                    front_shock:  'Manitou',
11                    rear_shock:   'Fox')
12 
13  mountain_bike.tire_size # => '2.1'
14  road_bike.chain         # => "10-speed"


It’s too early to celebrate this success, however, because there’s still something wrong with the code. It contains a booby trap, awaiting the unwary.

Implementing Every Template Method

Bicycle’s initialize method sends default_tire_size but Bicycle itself does not implement it. This omission can cause problems downstream. Imagine that FastFeed adds another new bicycle type, the recumbent. Recumbents are low, long bicycles that place the rider in a laid-back, reclining position; these bikes are fast and easy on the rider’s back and neck.

What happens if some programmer innocently creates a new RecumbentBike subclass but neglects to supply a default_tire_size implementation? He encounters the following error.


 1  class RecumbentBike < Bicycle
 2    def default_chain
 3      '9-speed'
 4    end
 5  end
 6 
 7  bent = RecumbentBike.new
 8  # NameError: undefined local variable or method
 9  #   'default_tire_size'


The original designer of the hierarchy rarely encounters this problem. She wrote Bicycle; she understands the requirements that subclasses must meet. The existing code works. These errors occur in the future, when the application is being changed to meet a new requirement, and are encountered by other programmers, ones who understand far less about what’s going on.

The root of the problem is that Bicycle imposes a requirement upon its subclasses that is not obvious from a glance at the code. As Bicycle is written, subclasses must implement default_tire_size. Innocent and well-meaning subclasses like RecumbentBike may fail because they do not fulfill requirements of which they are unaware.

A world of potential hurt can be assuaged, in advance, by following one simple rule. Any class that uses the template method pattern must supply an implementation for every message it sends, even if the only reasonable implementation in the sending class looks like this:


 1  class Bicycle
 2    #...
 3    def default_tire_size
 4      raise NotImplementedError
 5    end
 6  end


Explicitly stating that subclasses are required to implement a message provides useful documentation for those who can be relied upon to read it and useful error messages for those who cannot.

Once Bicycle provides this implementation of default_tire_size, creating a new RecumbentBike fails with the following error.


 1  bent = RecumbentBike.new
 2  #  NotImplementedError: NotImplementedError


While it is perfectly acceptable to merely raise this error and rely on the stack trace to track down its source, you may also explicitly supply additional information, as shown in line 5 below.


 1  class Bicycle
 2    #...
 3    def default_tire_size
 4      raise NotImplementedError,
 5            "This #{self.class} cannot respond to:"
 6    end
 7  end


This additional information makes the problem inescapably clear. As running this code shows, this RecumbentBike needs access to an implementation of default_tire_size.


 1  bent = RecumbentBike.new
 2  #  NotImplementedError:
 3  #    This RecumbentBike cannot respond to:
 4  #             'default_tire_size'


Whether encountered two minutes or two months after writing the RecumbentBike class, this error is unambiguous and easily corrected.

Creating code that fails with reasonable error messages takes minor effort in the present but provides value forever. Each error message is a small thing, but small things accumulate to produce big effects and it is this attention to detail that marks you as a serious programmer. Always document template method requirements by implementing matching methods that raise useful errors.

Managing Coupling Between Superclasses and Subclasses

Bicycle now contains most of the abstract bicycle behavior. It has code to manage overall bike size, chain, and tire size, and its structure invites subclasses to supply common defaults for these attributes. The superclass is almost complete; it’s missing only an implementation of spares.

This spares superclass implementation can be written in a number of ways; the alternatives vary in how tightly they couple the subclasses and superclasses together. Managing coupling is important; tightly coupled classes stick together and may be impossible to change independently.

This section shows two different implementations of spares—an easy, obvious one and another that is slightly more sophisticated but also more robust.

Understanding Coupling

This first implementation of spares is simplest to write but produces the most tightly coupled classes.

Remember that RoadBike’s current implementation looks like this:


 1  class RoadBike < Bicycle
 2    # ...
 3    def spares
 4      { chain:        '10-speed',
 5        tire_size:    '23',
 6        tape_color:   tape_color}
 7    end
 8  end


This method is a mishmash of different things and the last attempt at promoting it took a detour to clean up the code. That detour extracted the hard-coded values for chain and tire into variables and messages, and promoted just those parts up the Bicycle. The methods that deal with chain and tire size are now available in the superclass.

MountainBike’s current spares implementation looks like this:


 1  class MountainBike < Bicycle
 2    # ...
 3    def spares
 4      super.merge({rear_shock:  rear_shock})
 5    end
 6  end


MountainBike’s spares method sends super; it expects one of its superclasses to implement spares. MountainBike merges its own spare parts hash into the result returned by super, clearly expecting that result to also be a hash.

Given that Bicycle can now send messages to get chain and tire size and that its spares implementation ought to return a hash, adding the following spares method meets MountainBike’s needs.


 1  class Bicycle
 2    #...
 3    def spares
 4      { tire_size:  tire_size,
 5        chain:      chain}
 6    end
 7  end


Once this method is placed in Bicycle all of MountainBike works. Bringing RoadBike along is merely a matter of changing its spares implementation to mirror MountainBike’s, that is, replacing the code for chain and tire size with a send to super and adding the road bike specializations to the resulting hash.

Assuming this final change to MountainBike has been made, the following listing shows all of the code written so far and completes the first implementation of this hierarchy.

Notice that the code follows a discernible pattern. Every template method sent by Bicycle is implemented in Bicycle itself, and MountainBike and RoadBike both send super in their initialize and spares methods.


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


This class hierarchy works, and you might be tempted to stop right here. However, just because it works doesn’t guarantee that it’s good enough. It still contains a booby trap worth removing.

Notice that the MountainBike and RoadBike subclasses follow a similar pattern. They each know things about themselves (their spare parts specializations) and things about their superclass (that it implements spares to return a hash and that it responds to initialize).

Knowing things about other classes, as always, creates dependencies and dependencies couple objects together. The dependencies in the code above are also the booby traps; both are created by the sends of super in the subclasses.

Here’s an illustration of the trap. If someone creates a new subclass and forgets to send super in its initialize method, he encounters this problem:


 1  class RecumbentBike < Bicycle
 2    attr_reader :flag
 3 
 4    def initialize(args)
 5      @flag = args[:flag]  # forgot to send 'super'
 6    end
 7 
 8    def spares
 9      super.merge({flag: flag})
10    end
11 
12    def default_chain
13      '9-speed'
14    end
15 
16    def default_tire_size
17      '28'
18    end
19  end
20 
21  bent = RecumbentBike.new(flag: 'tall and orange')
22  bent.spares
23  # -> {:tire_size => nil, <- didn't get initialized
24  #     :chain     => nil,
25  #     :flag      => "tall and orange"}


When RecumbentBike fails to send super during initialize it misses out on the common initialization provided by Bicycle and does not get a valid size, chain, or tire size. This error can manifest at a time and place far distant from its cause, making it very hard to debug.

A similarly devilish problem occurs if RecumbentBike forgets to send super in its spares method. Nothing blows up, instead the spares hash is just wrong and this wrongness may not become apparent until a Mechanic is standing by the road with a broken bike, searching the spare parts bin in vain.

Any programmer can forget to send super and therefore cause these errors, but the primary culprits (and the primary victims) are programmers who don’t know the code well but are tasked, in the future, with creating new subclasses of Bicycle.

The pattern of code in this hierarchy requires that subclasses not only know what they do but also how they are supposed to interact with their superclass. It makes sense that subclasses know the specializations they contribute (they are obviously the only classes who can know them), but forcing a subclass to know how to interact with its abstract superclass causes many problems.

It pushes knowledge of the algorithm down into the subclasses, forcing each to explicitly send super to participate. It causes duplication of code across subclasses, requiring that all send super in exactly the same places. And it raises the chance that future programmers will create errors when writing new subclasses, because programmers can be relied upon to include the correct specializations but can easily forget to send super.

When a subclass sends super it’s effectively declaring that it knows the algorithm; it depends on this knowledge. If the algorithm changes, then the subclasses may break even if their own specializations are not otherwise affected.

Decoupling Subclasses Using Hook Messages

All of these problems can be avoided with one final refactoring. Instead of allowing subclasses to know the algorithm and requiring that they send super, superclasses can instead send hook messages, ones that exist solely to provide subclasses a place to contribute information by implementing matching methods. This strategy removes knowledge of the algorithm from the subclass and returns control to the superclass.

In the following example, this technique is used to give subclasses a way to contribute to initialization. Bicycle’s initialize method now sends post_initialize and, as always, implements the matching method, one that in this case does nothing.

RoadBike supplies its own specialized initialization by overriding post_initialize, as you see here:


 1  class Bicycle
 2 
 3    def initialize(args={})
 4      @size       = args[:size]
 5      @chain      = args[:chain]     || default_chain
 6      @tire_size  = args[:tire_size] || default_tire_size
 7 
 8      post_initialize(args)   # Bicycle both sends
 9    end
10 
11    def post_initialize(args) # and implements this
12      nil
13    end
14    # ...
15  end
16 
17  class RoadBike < Bicycle
18 
19    def post_initialize(args)         # RoadBike can
20      @tape_color = args[:tape_color] # optionally
21    end                               # override it
22    # ...
23  end


This change doesn’t just remove the send of super from RoadBike’s initialize method, it removes the initialize method altogether. RoadBike no longer controls initialization; it instead contributes specializations to a larger, abstract algorithm. That algorithm is defined in the abstract superclass Bicycle, which in turn is responsible for sending post_initialize.

RoadBike is still responsible for what initialization it needs but is no longer responsible for when its initialization occurs. This change allows RoadBike to know less about Bicycle, reducing the coupling between them and making each more flexible in the face of an uncertain future. RoadBike doesn’t know when its post_initialize method will be called and it doesn’t care what object actually sends the message. Bicycle (or any other object) could send this message at any time, there is no requirement that it be sent during object initialization.

Putting control of the timing in the superclass means the algorithm can change without forcing changes upon the subclasses.

This same technique can be used to remove the send of super from the spares method. Instead of forcing RoadBike to know that Bicycle implements spares and that Bicycle’s implementation returns a hash, you can loosen coupling by implementing a hook that gives control back to Bicycle.

The following example changes Bicycle’s spares method to send local_spares. Bicycle provides a default implementation, one that returns an empty hash. RoadBike takes advantage of this hook and overrides it to return its own version of local_spares, adding road bike specific spare parts.


 1  class Bicycle
 2    # ...
 3    def spares
 4      { tire_size: tire_size,
 5        chain:     chain}.merge(local_spares)
 6    end
 7 
 8    # hook for subclasses to override
 9    def local_spares
10      {}
11    end
12 
13  end
14 
15  class RoadBike < Bicycle
16    # ...
17    def local_spares
18      {tape_color: tape_color}
19    end
20 
21  end


RoadBike’s new implementation of local_spares replaces its former implementation of spares. This change preserves the specialization supplied by RoadBike but reduces its coupling to Bicycle. RoadBike no longer has to know that Bicycle implements a spares method; it merely expects that its own implementation of local_spares will be called, by some object, at some time.

After making similar changes to MountainBike, the final hierarchy looks like this:


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


RoadBike and MountainBike are more readable now that they contain only specializations. It’s clear at a glance what they do, and it’s clear that they are specializations of Bicycle.

New subclasses need only implement the template methods. This final example illustrates how simple it is to create a new subclass, even for someone unfamiliar with the application. Here is class RecumbentBike, a new specialization of Bicycle:


 1  class RecumbentBike < Bicycle
 2    attr_reader :flag
 3 
 4    def post_initialize(args)
 5      @flag = args[:flag]
 6    end
 7 
 8    def local_spares
 9      {flag: flag}
10    end
11 
12    def default_chain
13      "9-speed"
14    end
15 
16    def default_tire_size
17      '28'
18    end
19  end
20 
21  bent = RecumbentBike.new(flag: 'tall and orange')
22  bent.spares
23  # -> {:tire_size => "28",
24  #     :chain     => "10-speed",
25  #     :flag      => "tall and orange"}


The code in RecumbentBike is transparently obvious and is so regular and predictable that it might have come off of an assembly line. It illustrates the strength and value of inheritance; when the hierarchy is correct, anyone can successfully create a new subclass.

Summary

Inheritance solves the problem of related types that share a great deal of common behavior but differ across some dimension. It allows you to isolate shared code and implement common algorithms in an abstract class, while also providing a structure that permits subclasses to contribute specializations.

The best way to create an abstract superclass is by pushing code up from concrete subclasses. Identifying the correct abstraction is easiest if you have access to at least three existing concrete classes. This chapter’s simple example relied on just two but in the real world you are often better served to wait for the additional information that three cases supply.

Abstract superclasses use the template method pattern to invite inheritors to supply specializations, and use hook methods to allow these inheritors to contribute these specializations without being forced to send super. Hook methods allow subclasses to contribute specializations without knowing the abstract algorithm. They remove the need for subclasses to send super and therefore reduce the coupling between layers of the hierarchy and increase its tolerance for change.

Well-designed inheritance hierarchies are easy to extend with new subclasses, even for programmers who know very little about the application. This ease of extension is inheritance’s greatest strength. When your problem is one of needing numerous specializations of a stable, common abstraction, inheritance can be an extremely low-cost solution.

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

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