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.