dom lizarraga

    dominiclizarraga@hotmail.com

    POOD Session 10: Beware Inheritance & Lean Towards Composition

    12 minutes to read

    Session 10: Beware Inheritance & Lean Towards Composition

    Date: October 11, 2025

    This blog post consists in two parts:

    Key Concepts

    Beware Inheritance

    This block contains an example that illustrates how convenient inheritance can be, and how it can go very badly wrong.

    In this section we’re going to create a beautiful, simple, little inheritance hierarchy to show how useful inheritance can be to quickly getting things done, and then we’re going to introduce a change in requirements that makes everything go horribly wrong to show when you should switch from inheritance to composition.

    The new requirement is a variant of House that mixes up the bits, randomly. We need the original requirement to still work, House should still work just like it does, but they also want another variant where the lines come out mixed up, randomly, every time.

    Let’s do that with inheritance.

    First proposal:

    # original code
    class House
      DATA =
        [ "the horse and the hound and the horn that belonged to",
          "the farmer sowing his corn that kept",
          "the rooster that crowed in the morn that woke",
          "the priest all shaven and shorn that married",
          "the man all tattered and torn that kissed",
          "the maiden all forlorn that milked",
          "the cow with the crumpled horn that tossed",
          "the dog that worried",
          "the cat that killed",
          "the rat that ate",
          "the malt that lay in",
          "the house that Jack built"]
    
      def recite
        1.upto(12).collect {|i| line(i)}.join("\n")
      end
    
      def phrase(num)
        data.last(num).join(" ")
      end
    
      def line(num)
        "This is #{phrase(num)}.\n"
      end
    
      def data
        DATA
      end
    end
    
    # RandomHouse code
    class RandomHouse < House
      def data
        @data ||= super.shuffle
      end
    end
    
    # PirateHouse code
    class PirateHouse < House
      def prefix
        "Thar be"
      end
    end
    
    puts 
    puts RandomHouse.new.line(12)
    
    puts
    puts PirateHouse.new.line(12)
    

    New requirement is going to make everything break badly with this inheritance approach. Implement RandomPirateHouse without duplicating code. Module is not feasable, composition is the right solution.

    You want your inheritance hierarchies to be shallow, not very deep, and narrow, not very wide. You want the subclasses to really be a kind of thing of the superclasses.

    Use of inheritance only on your object dependency graph leaf nodes. t the core of your app are key objects that depend on others, forming a chain outward. At the edges are leaf objects like BottleNumber# that are depended on but depend on nothing else.

    When you create subclasses that are that far out away from the center of your domain they’re probably not all that important, and they’re probably not going to change that much, and it might not go badly wrong to use inheritance out there.

    Lean Towards Composition

    In the last section we implemented a little inheritance hierarchy that worked however once we wanted to introduce a new change it flopped.

    First proposal:

    class House
      DATA =
        [ "the horse and the hound and the horn that belonged to",
          "the farmer sowing his corn that kept",
          "the rooster that crowed in the morn that woke",
          "the priest all shaven and shorn that married",
          "the man all tattered and torn that kissed",
          "the maiden all forlorn that milked",
          "the cow with the crumpled horn that tossed",
          "the dog that worried",
          "the cat that killed",
          "the rat that ate",
          "the malt that lay in",
          "the house that Jack built"]
      attr_reader :data
    
      def initialize(random=false)
        @data = 
          if random == true
            DATA.shuffle
          else
            DATA
          end
      end
    
      def recite
        1.upto(12).collect {|i| line(i)}.join("\n")
      end
    
      def phrase(num)
        data.last(num).join(" ")
      end
    
      def line(num)
        "#{prefix} #{phrase(num)}.\n"
      end
    
      def prefix
        "This is"
      end
    end
    
    puts
    
    # this code looks weird
    puts House.new(true).line(12)
    

    In object-oriented programming, we can expect that if an object knows enough to pass you a true or a false, or a truck or a carrot, they know enough to pass you a smarter thing.

    You shouldn’t have to look at the type and supply behavior for different kinds of things. You should just have a thing that came in that you can send a message to, and get the thing back that you want.

    We don’t like this code at all, but the good thing about writing this conditional is, it tells us exactly what we wish we had.

    We need an object to stand in for true that behaves like what’s in the true branch, and we need another object to stand in for false that returns what’s in the false branch. Those two objects play a common role. They have to conform to the same API, and in the methods that conform to that API they have different behavior so that they can be treated interchangeably, but they do different things.

    We know we need two classes, and we know they have to conform to the same API, but we don’t want to have to name them right now.

    Second proposal:

    class House
      DATA =
        [ "the horse and the hound and the horn that belonged to",
          "the farmer sowing his corn that kept",
          "the rooster that crowed in the morn that woke",
          "the priest all shaven and shorn that married",
          "the man all tattered and torn that kissed",
          "the maiden all forlorn that milked",
          "the cow with the crumpled horn that tossed",
          "the dog that worried",
          "the cat that killed",
          "the rat that ate",
          "the malt that lay in",
          "the house that Jack built"]
      attr_reader :data
    
      def initialize(orderer: OriginalOrderer.new)
        @data = orderer.order(DATA)
      end
    
      def recite
        1.upto(12).collect {|i| line(i)}.join("\n")
      end
    
      def phrase(num)
        data.last(num).join(" ")
      end
    
      def line(num)
        "#{prefix} #{phrase(num)}.\n"
      end
    
      def prefix
        "This is"
      end
    end
    
    class RandomOrderer
      def order(data)
        data.shuffle
      end
    end
    
    class OriginalOrderer
      def order(data)
        data.shuffle
      end
    end
    
    # we can now invoke RandomOrderer and PiratePrefixer since we rely on composition
    puts House.new(orderer: RandomOrderer.new).line(12)
    puts
    puts House.new(prefixer: PiratePrefixer.new).line(12)
    

    We should name objects after we have written down the code, remember Objects are, not doers.

    And for the new requirement that entailed RandomPirateHouse we can do the follwing:

    class House
      DATA =
        [ "the horse and the hound and the horn that belonged to",
          "the farmer sowing his corn that kept",
          "the rooster that crowed in the morn that woke",
          "the priest all shaven and shorn that married",
          "the man all tattered and torn that kissed",
          "the maiden all forlorn that milked",
          "the cow with the crumpled horn that tossed",
          "the dog that worried",
          "the cat that killed",
          "the rat that ate",
          "the malt that lay in",
          "the house that Jack built"]
      attr_reader :data, :prefix
    
      def initialize(orderer: OriginalOrderer.new, prefixer: MundanePrefixer.new)
        @data = orderer.order(DATA)
        @prefix = prefixer.prefix
      end
    ...
    
    class PiratePrefixer
      def prefix
        "Thar be"
      end
    end
    
    class MundanePrefixer
      def prefix
        "This is"
      end
    end
    
    puts
    puts House.new(orderer: RandomOrderer.new, prefixer: PiratePrefixer.new).line(12)
    

    Here is another variant we want to tackle and is regarding the ending of the sentences.

    class RandomButLastOrderer
      def order(data)
        data[0..-2].shuffle << data.last
      end
    end
    

    Conclusion: Sandi does use inheritance a lot. It can be super easy, and handy, but as soon as it starts going wrong, as soon as you start creating deep or wide hierarchies, or subclasses that duplicate behavior across the hierarchy, you have to switch to composition.

    Composition is a little bit harder for humans to think about, but once you get it, once you get the knack in your head, it’s easy to use, and it’s very flexible and changeable. The key to composition is being able to imagine objects that apparently do nothing.