Exercise 44: Inheritance Vs. Composition

Warning

This is being rewritten based on the Python version, there may be errors.

In the fairy tales about heroes defeating evil villains there's always a dark forest of some kind. It could be a cave, a forest, another planet, just some place that everyone knows the hero shouldn't go. Of course, shortly after the villain is introduced you find out, yes, the hero has to go to that stupid forest to kill the bad guy. It seems the hero just keeps getting into situations that require him to risk his life in this evil forest.

You rarely read fairy tales about the heroes who are smart enough to just avoid the whole situation entirely. You never hear a hero say, "Wait a minute, if I leave to make my fortunes on the high seas leaving Buttercup behind I could die and then she'd have to marry some ugly prince named Humperdink. Humperdink! I think I'll stay here and start a Farm Boy For Rent business." If he did that there'd be no fire swamp, dying, reanimation, sword fights, giants, or any kind of story really. Because of this, the forest in these stories seems to exist like a black hole that drags the hero in no matter what they do.

In object oriented programming, Inheritance is the evil forest. Experienced programmers know to avoid this evil because they know that deep inside the Dark Forest Inheritance is the Evil Queen Meta-Programming. She likes to eat software and programmers with her massive complexity teeth, chewing on the flesh of the fallen. But, the forest is so powerful and so tempting that nearly every programmer has to go into it, and try to make it out alive with the Evil Queen's head before they can call themselves real programmers. You just can't resist the Inheritance Forest's pull, so you go in. After the adventure you learn to just stay out of that stupid forest and bring and army if you are ever forced to go in again.

This is basically a funny way to say that I'm going to teach you something you should avoid called Inheritance. Programmers who are currently in the forest battling the Queen will probably tell you that you have to go in. They say this because they need your help since what they've created is probably too much for them to handle. But, you should always remember this:

Most of the uses of inheritance can be simplified or replaced with composition, and meta-programming should be used sparingly.

What Is Inheritance?

Inheritance is used to indicate that one class will get most or all of its features from a parent class. This happens implicitly whenever you write class Foo < Bar which says "Make a class Foo that inherits from Bar." When you do this, the language makes any action that you do on instances of Foo also work as if they were done to an instance of Bar. Doing this lets you put common functionality in the Bar class, then specialize that functionality in the Foo class as needed.

When you are doing this kind of specialization, there's three ways that the parent and child classes can interact:

  1. Actions on the child imply an action on the parent.
  2. Actions on the child override the action on the parent.
  3. Actions on the child alter the action on the parent.

I will now demonstrate each of these in order and show you code for them.

Implicit Inheritance

First I will show you the implicit actions that happen when you define a function in the parent, but not in the child.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Parent

    def implicit()
        puts "PARENT implicit()"
    end
end

class Child < Parent
end    

dad = Parent.new()
son = Child.new()

dad.implicit()
son.implicit()

This creates a class named Child but says that there's nothing new to define in it. Instead it will inherit all of its behavior from Parent. When you run this code you get the following:

PARENT implicit()
PARENT implicit()

Notice how even though I'm calling son.implicit() on line 16, and even though Child does not have a implicit function defined, it still works and it calls the one defined in Parent. This shows you that, if you put functions in a base class (i.e. Parent) then all subclasses (i.e. Child) will automatically get those features. Very handy for repetitive code you need in many classes.

Override Explicitly

The problem with implicitly having functions called is sometimes you want the child to behave differently. In this case you want to override the function in the child, effectively replacing the functionality. To do this just define a function with the same name in Child. Here's an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Parent

    def override()
        puts "PARENT override()"
    end
end

class Child < Parent
    
    def override()
        puts "CHILD override()"
    end
end

dad = Parent.new()
son = Child.new()

dad.override()
son.override()

In this example example I have a function named override in both classes, so let's see what happens when you run it.

PARENT override()
CHILD override()

As you can see, when line 14 runs, it runs the Parent.override function because that variabe (dad) is a Parent. But, when line 15 runs it prints out the Child.override messages because son is an instance of Child and child overrides that function by defining it's own version.

Take a break right now and try playing with these two concepts before continuing.

Alter Before Or After

The third way to use inheritance is a special case of overriding where you want to alter the behavior before or after you the parent class's version runs. You first override the function just like in the last example, but then you use a Ruby built-in function named super to get the Parent version to call. Here's the example of doing that so you can make sense of this description:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Parent

    def altered()
        puts "PARENT altered()"
    end

end

class Child < Parent

    def altered()
        puts "CHILD, BEFORE PARENT altered()"
        super()
        puts "CHILD, AFTER PARENT altered()"
    end

end

dad = Parent.new()
son = Child.new()

dad.altered()
son.altered()

The important lines here are 9-11, where in the child I do the following when son.altered() is called:

  1. Because I've overridden Parent.altered the Child.altered version runs, and line 9 executes like you'd expect.
  2. In this case I want to do a before and after so after line 9, I want to use super to get the Parent.altered version.
  3. On line 10 I call super(), which reapeats this function call on the parent class.
  4. At this point, the Parent.altered version of the function runs, and that prints out the parent message.
  5. Finally, this returns from the Parent.altered and the Child.altered function continues to print out the after message.

If you then run this you should see this:

PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()

All Three Combined

To demonstrate all of these, I have a final version that shows each kind of interaction from inheritance in one file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Parent

    def override()
        puts "PARENT override()"
    end

    def implicit()
        puts "PARENT implicit()"
    end

    def altered()
        puts "PARENT altered()"
    end
end

class Child < Parent
    
    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE PARENT altered()"
        super()
        puts "CHILD, AFTER PARENT altered()"
    end
end

dad = Parent.new()
son = Child.new()

dad.implicit()
son.implicit()

dad.override()
son.override()

dad.altered()
son.altered()

Go through each line of this code, and write a comment explaining what that line does and whether it's an override or not. Then, run it and see that you get what you expected:

PARENT implicit()
PARENT implicit()
PARENT override()
CHILD override()
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()

The Reason For super()

In the Child.altered() function I used a special function named super(). This isn't a function you define, but instead one Ruby gives you. This function figures out how to call the same function, but in a parent class. The reason you need a special function is that what is "the parent class" is a bit complex. It relies on Ruby knowing the full inheritance structure of the current class, and any possible modules you've added to it with mixins (which I'll show you soon). By using super() you don't need to worry about figuring this out and can just let Ruby do it.

You use super() by passing it the right number of arguments for the parent class's argument count. In the example above, neither version of altered() had arguments, so I just called super(). If the class Parent had arguments, then I'd add them to the super() call.

Using super() With initialize

The most common use of super() is actually in initialize functions in base classes. This is usually the only place where you need to do some things in a child, then complete the initialization in the parent. Here's a quick example of doing that in the Child from these examples:

class Child < Parent
    def initialize(self, stuff):
        self.stuff = stuff
        super()
    end
end

This is pretty much the same as the Child.altered example above, except I'm setting some variables in the initialize before having the Parent initialize with its Parent.initialize.

Composition

Inheritance is useful, but another way to do the exact same thing is just to use other classes and modules, rather than rely on implicit inheritance. If you look at the three ways to exploit inheritance, two of the three involve writing new code to replace or alter functionality. This can easily be replicated by just calling functions on another class or from a module. Here's an example of doing this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Other

    def override()
        puts "OTHER override()"
    end

    def implicit()
        puts "OTHER implicit()"
    end

    def altered()
        puts "OTHER altered()"
    end
end

class Child

    def initialize()
        @other = Other.new()
    end

    def implicit()
        @other.implicit()
    end
    
    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE OTHER altered()"
        @other.altered()
        puts "CHILD, AFTER OTHER altered()"
    end
end

son = Child.new()

son.implicit()
son.override()
son.altered()

In this code I'm not using the name Parent, since there is not a parent-child is-a relationship. This is a has-a relationship, where Child has-a Other that it uses to get its work done. When I run this I get the following output:

OTHER implicit()
CHILD override()
CHILD, BEFORE OTHER altered()
OTHER altered()
CHILD, AFTER OTHER altered()

You can see that most of the code in Child and Other is the same to accomplish the same thing. The only difference is that I had to define a Child.implicit function to do that one action. I could then ask myself if I need this Other to be a class, and could I just make it into a module named Other. I totally could make this into a module instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module Other

    def Other.override()
        puts "OTHER override()"
    end

    def Other.implicit()
        puts "OTHER implicit()"
    end

    def Other.altered()
        puts "OTHER altered()"
    end
end

class Child

    def implicit()
        Other.implicit()
    end
    
    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE OTHER altered()"
        Other.altered()
        puts "CHILD, AFTER OTHER altered()"
    end
end

son = Child.new()

son.implicit()
son.override()
son.altered()

Nearly the exact same thing just using a module. The choice of which is better in the above code (class Other vs. module Other) depends on if you need to maintain state in each function call in Other. If each function can stand on its own, and they're mostly utilities then use a module. If however the set of functions make up a cohesive "thing" that keeps state then use a class.

When To Use Inheritance Or Composition

The question of "inheritance vs. composition" comes down to an attempt to solve the problem of reusable code. You don't want to have duplicated code all over your code, since that's not clean and efficient. Inheritance solves this problem by creating a mechanism for you to have implied features in base classes. Composition solves this by giving you modules and the ability to simply call functions in other classes.

If both solutions solve the problem of reuse, then which one is appropriate in which situations? The answer is incredibly subjective, but I'll give you my three guidelines for when to do which:

  1. Avoid meta-programming at all costs, as it's too complex to be useful reliably. If you're stuck with it, then be prepared to spend time finding where everything is coming from.
  2. Use composition to package up code into modules that is used in many different unrelated places and situations.
  3. Use inheritance only when there are clearly related reusable pieces of code that fit under a single common concept, or if you have to because of something you're using.

However, do not be a slave to these rules. The thing to remember about object oriented programming is that it is entirely a social convention programmers have created to package and share code. Because it's a social convention, but one that's codified in Ruby, you may be forced to avoid these rules because of the people you work with. In that case, find out how they use things and then just adapt to the situation.

Extra Credit

There is only one extra credit for this exercise because it is a big exercise. Go and read this https://github.com/styleguide/ruby and start trying to use it in your code. You'll notice that some of it is different from what you've been learning in this book, but now you should be able to understand their recommendations and use them in your own code. The rest of the code in this book may or may not follow these guidelines depending on if it makes the code more confusing. I suggest you also do this, as comprehension is more important than impressing everyone with you knowledge of esoteric style rules.