dom lizarraga

    dominiclizarraga@hotmail.com

    POOD Session 6: Polymorphism, Remedy Liskov & Make the Easy Change

    21 minutes to read

    Session 6: Get to Know Polymorphism, Remedy Liskov Violations & Make the Easy Change

    Date: September 13, 2025

    This blog post consists in three parts:

    Key Concepts

    Get to know Polymorphism

    Watch 1: Making Sense of Conditionals

    This block contains two videos. One delves into conditionals and the other explores object-oriented polymorphism.

    We still have not the code to be Open. Here, we are going to pick another code smell.

    The most repeated pattern are the conditional, with 2 branches and all checking number == 1 or number == 0.

    The problem with repeating conditionals is evident. How many methods would you have to change to fulfill the six-pack requirement? You have to touch a bunch of places in here because the six-pack-ness is spread out over a number of these conditionals.

    These conditionals provide us with generalizations in the false branches and specializations in the true branches.

    If we had mixed up our styles of code, it would have been much more difficult to tell here what we needed to do next. Consistency of style enables future refactorings and enhances understanding.

    Examples of bad usage of inheritance:

    # BAD: Deep/Wide Inheritance Hierarchy
    class Employee; end
    class FullTimeEmployee < Employee; end
    class PartTimeEmployee < Employee; end
    class ContractEmployee < Employee; end
    class SeniorFullTimeEmployee < FullTimeEmployee; end
    class JuniorFullTimeEmployee < FullTimeEmployee; end
    # ... gets messy and hard to understand
    
    # BAD: Tiny Specializations Leading to Cross-Cutting Problems
    class Employee
      def swag_amount
        100
      end
      
      def tax_rate
        0.20
      end
    end
    
    class LongTermEmployee < Employee
      def swag_amount
        super * 2  # specializes only swag
      end
    end
    
    class OutOfStateEmployee < Employee
      def tax_rate
        0.15  # specializes only taxes
      end
    end
    
    # Problem: Need LongTermOutOfStateEmployee? Can't inherit from both!
    
    # GOOD: Use Composition for Cross-Cutting Concerns
    class Employee
      def initialize(swag_policy:, tax_policy:)
        @swag_policy = swag_policy
        @tax_policy = tax_policy
      end
      
      def swag_amount
        @swag_policy.calculate
      end
      
      def tax_rate
        @tax_policy.rate
      end
    end
    
    class StandardSwagPolicy
      def calculate
        100
      end
    end
    
    class LongTermSwagPolicy
      def calculate
        200
      end
    end
    
    class InStateTaxPolicy
      def rate
        0.20
      end
    end
    
    class OutOfStateTaxPolicy
      def rate
        0.15
      end
    end
    
    # Now you can mix and match any combination:
    Employee.new(
      swag_policy: LongTermSwagPolicy.new,
      tax_policy: OutOfStateTaxPolicy.new
    )
    
    # ACCEPTABLE: Inheritance for Leaf Nodes (Small, Focused Classes)
    class BottleNumber
      def container
        "bottles"
      end
    end
    
    class BottleNumber1 < BottleNumber
      def container
        "bottle"  # specializes most of the small class
      end
    end
    
    class BottleNumber0 < BottleNumber
      def quantity
        "no more"  # specializes most of the small class
      end
    end
    

    Watch 2: Replacing Conditionals with Polymorphism

    “Poly” means many, “morph” in this case means form. Many forms.

    We use polymorphism to describe a situation in which many different objects can respond to the same message.

    This means that the message sender doesn’t know or care the type of the object that it gets passed, and it means that later you can create new objects that polymorphously play some existing role and get that behavior into your app without having to change anything about those objects that are sending the messages.

    Let’s implement a Factory object model that evaluates and creates depending on the request, different new objects, these new objects will inherit from a general BottleNumber and will contain specializations. This will reduce the conditionals we have into several new small classes.

    This change is like primitive obsession but instead of obsessing upon the entire class, we actually obsess on instances of the class (0 and 1).

    class Bottles
      def song
        verses(99, 0)
      end
    
      def verses(upper, lower)
        upper.downto(lower).map { |i| verse(i) }.join("\n")
      end
    
      def verse(number)
        bottle_number = bottle_number_for(number)
        next_bottle_number = bottle_number_for(bottle_number.successor)
    
        "#{bottle_number} of beer on the wall, ".capitalize +
        "#{bottle_number} of beer.\n" +
        "#{bottle_number.action}, " +
        "#{next_bottle_number} of beer on the wall.\n"
      end
    
      # Factory
      def bottle_number_for(number)
        case number
        when 0
          BottleNumber0
        when 1
          BottleNumber1
        else
          BottleNumber
        end.new(number)
      end
    end
    
    class BottleNumber
      attr_reader :number
      def initialize(number)
        @number = number
      end
    
      def to_s
        "#{quantity} #{container}"
      end
    
      def quantity
        number.to_s
      end
    
      def container
         "bottles"
      end
    
      def action
        "Take #{pronoun} down and pass it around"
      end
    
      def pronoun
        "one"
      end
    
      def successor
        number - 1
      end
    end
    
    class BottleNumber0 < BottleNumber
      def quantity
        "no more"
      end
    
      def action
        "Go to the store and buy some more"
      end
    
      def successor
        99
      end
    end
    
    class BottleNumber1 < BottleNumber
      def container
        "bottle"
      end
    
      def pronoun
        "it"
      end
    end
    

    Quiz:

    This refactoring is super easy, yet it greatly increases the abstraction of the code. List some qualities of the current code that make this refactoring so easy.

    All of the following are true:

    1. The tests run quickly.
    2. The code has a consistent style.
    3. The methods have a single responsibility.
    4. The conditionals test for equality.

    Imagine the code you would have written to solve the 99 bottles problem before you knew about Shameless Green or read the 99 Bottles of OOP book. (You may actually have made a stab at solving the problem before starting the course–that’s the code I’m asking about here.) Would it have been equally easy to refactor that code into this state? If not, what qualities of the code would have made the refactoring difficult?

    It’s common for that first attempt to have the following qualities:

    1. The abstractions are incorrect or incomplete.
    2. The code and the conditionals have an inconsistent style.
    3. Methods have more than one responsibility.

    Remedy Liskov Violations

    Watch 1: Transitioning Between Types

    Here, we are tackling the Liskov violation that .succesor represents, remember it should return BottleNumber not a number.

    We named the factory "self.for". The name ".for" implies that it takes an argument, but in my experience factories always do. The job of a factory is to pick an object, and it needs some thing to pick on.

    class Bottles
      def song
        verses(99, 0)
      end
    
      def verses(upper, lower)
        upper.downto(lower).map { |i| verse(i) }.join("\n")
      end
    
      def verse(number)
        bottle_number = BottleNumber.for(number)
    
        "#{bottle_number} of beer on the wall, ".capitalize +
        "#{bottle_number} of beer.\n" +
        "#{bottle_number.action}, " +
        "#{bottle_number.successor} of beer on the wall.\n"
      end
    end
    
    class BottleNumber
      attr_reader :number
    
      def initialize(number)
        @number = number
      end
    
      def self.for(number)
        case number
        when 0
          BottleNumber0
        when 1
          BottleNumber1
        else
          BottleNumber
        end.new(number)
      end
    
      def to_s
        "#{quantity} #{container}"
      end
    
      def quantity
        number.to_s
      end
    
      def container
        "bottles"
      end
    
      def action
        "Take #{pronoun} down and pass it around"
      end
    
      def pronoun
        "one"
      end
    
      def successor
        BottleNumber.for(number - 1)
      end
    end
    
    class BottleNumber0 < BottleNumber
      def quantity
        "no more"
      end
    
      def action
        "Go to the store and buy some more"
      end
    
      def successor
        BottleNumber.for(99)
      end
    end
    
    class BottleNumber1 < BottleNumber
      def container
        "bottle"
      end
    
      def pronoun
        "it"
      end
    end
    

    Quiz

    What is the official definition of the Liskov Substitution Principle?

    • “Functions that use pointers to base classes must be able to use objects of derived classes without knowing it.”

    Or more generally…

    • “Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application”

    If ‘official’ Liskov has to do with inheritance, how can whatever’s wrong with the #successor methods be a Liskov violation? Write a new Liskov Substitution Principle definition that extends it to cover both problems.

    • The Liskov Substitution Principle can be expanded to include not only subtypes but also any other object that is meant to be interchangeable with current one. Liskov requires that methods return the objects that they “promise” to return.

    • In our case the very name of the #successor method implies that it returns something that acts like a BottleNumber, that is, that it returns an object that conforms to the API of the receiver. Returning a number instead of a BottleNumber forces that caller to check the returned object’s type in order to know how to talk to it. This is a Liskov violation.

    Make the Easy Change

    Watch 1: Making the Easy Change

    In this lesson we discovered that the code is now open to the six-pack requirement.

    A quick recap of what we’ve done so far:

    1. We started out by writing the Shameless Green implementation of 99 Bottles.

    2. then we got a new requirement to say 1 six-pack instead of 6 bottles.

    3. We decided to make this code open to change (we didn’t really know how to do that)

    4. Therefore we used that flow chart that suggested that we find code smells (they are 24).

    code_is_open_diagram
    1. We identifed code smells, and then we picked the code smell that we thought, if we corrected it (duplicate code).

    2. We had to isolate the things we want to vary.

    3. The first code smell that we worked on was all that duplication in the verse method; we decided it contained concealed concepts, and we used the flocking rules to identify those concepts, to give those concepts names.

    • Selecting the most similar pieces of code

    • Finding the smallest difference between them

    • Making the simplest change to remove that difference

    1. After that, we decided that all the methods that we’d created using the flocking rules were obsessing upon a primitive.

    2. We extracted the BottleNumber class in order to cure that obsession.

    3. Finally, we noticed that we had repeating conditionals in that BottleNumber class, and we cured those conditionals by following the Replace Conditional with Polymorphism recipe and making a little inheritance hierarchy of different players of the BottleNumber role.

    For implementing the six-pack we switched to TDD so the first thing to do is writing a failing test.

    We changed in bottles_test.rb the following, notice the “1 six-pack”

    7 bottles of beer on the wall, 7 bottles of beer.
    Take one down and pass it around, 1 six-pack of beer on the wall.
    
    1 six-pack of beer on the wall, 1 six-pack of beer.
    Take one down and pass it around, 5 bottles of beer on the wall.
    

    Then ran tests and failed:

     7 bottles of beer on the wall, 7 bottles of beer.
    -Take one down and pass it around, 1 six-pack of beer on the wall.
    +Take one down and pass it around, 6 bottles of beer on the wall.
     
    -1 six-pack of beer on the wall, 1 six-pack of beer.
    +6 bottles of beer on the wall, 6 bottles of beer.
     Take one down and pass it around, 5 bottles of beer on the wall.
    

    Then we added a new class of specialization BottleNumber6 with two methods #quantity and #container and don’t forget about the Factory, we gotta add one case branch.

    class BottleNumber
      attr_reader :number
    
      def initialize(number)
        @number = number
      end
    
      def self.for(number)
        case number
        when 0
          BottleNumber0
        when 1
          BottleNumber1
        when 6
          BottleNumber6
        else
          BottleNumber
        end.new(number)
      end
    ...
    
    class BottleNumber6 < BottleNumber
      def quantity
        "1"
      end
    
      def container
        "six-pack"
      end
    end
    

    “Make the change easy, (warning, this might be hard), then make the easy change.”, and that’s exactly the experience that we had. We’ve spent chapters making the code open, but once it was open, the change was easy.

    If you learn the code smells and you get familiar with even just the names of the refactorings, you can identify code smells and look up the refactoring recipes when you want to use them.

    It’ll make your code better and your life easier.

    Watch 2: Defending the Domain

    It’s true that the number of bugs in code is related to the volume of code, the more code you write, the more bugs you’ll have, so it’s better to minimize the amount of code you have.

    However, it’s not a good idea to reduce it beyond the logical point. It’s only a good idea to reduce it if it’s correct.

    The original class, Bottles, can know about BottleNumber. It has to know about how to get them but once we extract those BottleNumber# classes we have to turn our back on Bottles.

    Bottles can know about the things that it’s gonna contain, BottleNumber can’t know about the objects that will contain it. The relationship can’t be bidirectional.

    It’d mean you can never use them in another context

    The above ideas were to stand your domain on the next proposal:

    class BottleNumber6 < BottleNumber
      def quantity
        "1"
      end
    
      def container
        "six-pack"
      end
    end
    # VS
    class BottleNumber6 < BottleNumber
      def to_s
        "#{quantity} #{container}"
      end
    end
    

    Sandi’s summary

    The purpose of this block was to produce a code arrangement that was open to the six-pack requirement. Not only did it succeed in fulfilling that requirement, but along the way it also resolved a number of other issues.

    This block explored the Data Clump code smell. It replaced a Switch Statement with a set of polymorphic objects, which it created using a factory. It corrected the Liskov violation in successor, and used that problem as a jumping-off point for a more general lesson about how to change the return types of polymorphic methods.

    The BottleNumber for factory was straightforward and most certainly did the job. While simple factories like this work great in many situations, they’re not best for every case. There’s a whole world of different styles of factories waiting to be explored.