dom lizarraga

dominiclizarraga@hotmail.com

POOD Session 3: Identify Abstractions

33 minutes to read

Session 3: Identify Abstractions & Dry Out Shameless Green

Date: August 23, 2025

This blog post consists in two parts:

Key Concepts

  • Flocking Rules – A systematic way to discover hidden abstractions by:
  1. Selecting the most similar pieces of code

  2. Finding the smallest difference between them

  3. Making the simplest change to remove that difference

  • Abstraction Discovery – Don’t invent abstractions upfront; uncover them by observing recurring patterns and responsibilities that emerge through refactoring.

  • Naming by Responsibility – When extracting methods, name them after what they do, not how they do it. (e.g. quantity, container, pronoun, action, successor)

  • Liskov Substitution Principle – Objects should behave as they claim to; avoid conditionals that depend on an object’s type.

  • Duck Typing – In Ruby, objects are defined by their behavior, not their class. If two objects respond to the same messages, they share a “role.” e.g. PrivateContract and CommercialContract both respond to #name, so they can be used interchangeably.

  • Depending on Abstractions

  • Emergent Design – By consistently applying the Flocking Rules, abstractions emerge naturally; you don’t need to foresee the design from the start.

Identify Abstractions

Watch 1: Following the Flocking Rules

Quick reminder: We need to change the code to display “six-pack” instead of “6 bottles” which means make the code open to extension, closed to modification

Here are the steps to find abstractions in code, these are mini-decisions, we still don’t know the outcome:

  1. Select the things that are most alike
  2. Find the smallest difference between them
  3. Make the simplest change to remove the difference

a) parse the new code b) parse and execute it c) parse, execute and use its results d) deleted unused code

Remember to

  • change one line at the time.
  • run tests after each change
  • if the test fail, undo and make a better change

Set of rules of flocking birds 🦅-🦅-🦅-🦅

  • alignment: tells them to steer on the average heading of their near neighbors
  • cohesion, says steer towards the average position of their neighbors, which is sort of a long range attraction rule
  • separation, don’t hit each other

This will turn difference into sameness and will reveal Concealed Concepts.

Watch 2: Converging on Abstractions

Increase isolation of the thing we want to vary. With the hope that eventually the code will open for the change we want to make.

Discover hidden abstractions instead of look at the problem and make them up.

We went from this code:


  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, " +
      "no more bottles of beer.\n" +
      "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, " +
      "1 bottle of beer.\n" +
      "Take it down and pass it around, " +
      "no more bottles of beer on the wall.\n"
    when 2
      "2 bottles of beer on the wall, "
      "2 bottles of beer.\n " +
      "Take one down and pass it around," +
      "1 bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, " +
      "#{number} bottles of beer.\n " +
      "Take one down and pass it around, " +
      "#{number - 1} bottles of beer on the wall.\n"
    end
  end

To this one, always following flocking rules for finding hidden abstracions, select the things that are most alike, find the smallest difference between them and make the simplest change to remove the difference:


  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, " +
      "no more bottles of beer.\n" +
      "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, " +
      "1 bottle of beer.\n" +
      "Take it down and pass it around," +
      "no more bottles of beer on the wall.\n"
    else
      "#{number} #{container(number)} of beer on the wall, " +
      "#{number} bottles of beer.\n" +
      "Take one down and pass it around, " +
      "#{number - 1} bottles of beer on the wall.\n"
    end
  end

  def container(number)
    if number == 1
      "bottle"
    else
      "bottles"
    end
  end

Dry Out Shameless Green

In this block we finished DRYing out bottles Shameless Green via the Flocking Rules. We coded for 30 minutes with the aim to DRYing out the #verse.

After that time we watched the solution videos, replicate the code and discussed it with the group.

This is the code we started with, after chopping off the case statement for number 2 all tests run on green:

def song
    verses(99, 0)
  end

  def verses(upper, lower)
    upper.downto(lower).map { |i| verse(i) }.join("\n")
  end

  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, " +
      "no more bottles of beer.\n" +
      "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, " +
      "1 bottle of beer.\n" +
      "Take it down and pass it around, " +
      "no more bottles of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, " +
      "#{number} bottles of beer.\n" +
      "Take one down and pass it around, " +
      "#{number-1} #{container(number-1)} of beer on the wall.\n"
    end
  end

  def container(number)
    if number == 1
      "bottle"
    else
      "bottles"
    end
  end
end

Watch 1: Replacing Difference With Sameness

It’s time to pick a pair to work on, keeping in mind go for the code that is pretty similar to each other, in other words, less differences and work towards reducing the differences.

In this case is the branch 1 and the else branch.

Their difference rely on "1 bottle" vs "#{number}", "it" vs "one" and "no more" vs "#{number-1}"

This change will make our code slower and more abstract, but he have to do this in order to make our code look the same.

The cost of this change is execution time, but keeping both cases will cost more due to maintenance (2 very similar branches).

def verse(number)
  case number
  when 0
    "No more bottles of beer on the wall, " +
    "no more bottles of beer.\n" +
    "Go to the store and buy some more, " +
    "99 bottles of beer on the wall.\n"
  when 1
    "#{number} #{container(number)} of beer on the wall, " +
    "#{number} #{container(number)} of beer.\n" +
    "Take it down and pass it around, " +
    "no more bottles of beer on the wall.\n"
  else
    "#{number} #{container(number)} of beer on the wall, " +
    "#{number} #{container(number)} of beer.\n" +
    "Take one down and pass it around, " +
    "#{number - 1} #{container(number - 1)} of beer on the wall.\n"
  end
end

When following the flocking rules trying to turn difference into sameness, resist the urge to volunteer changes. Limit your changes to only what the recipe calls for and see what happens

Watch 2: Equivocating About Names

The official definition of equivocate is to use ambiguous language so as to conceal the truth or avoid committing oneself.

Sometimes we just don’t know and we have to do the best we can with the information we have at the time.

We are now tackling the "it" vs "one" case, therefore we need to name the concept, create a method or function to be responsible for it, and then use it, send the message in the place of this difference.

We are looking for that method name where def thing is many layers of abstraciton away.

When naming, we have 3 rules:

Naming Rule Pros Cons
Time-boxed naming - Set a time limit and use a thesaurus to find the best name within that time Time limited approach prevents overthinking You'll never know less than you know right now, so a better name may emerge later; good enough names may not motivate future improvements
Intentionally bad placeholder - Pick the worst possible name (like "foo") knowing you'll rename it later Very fast; saves time and money The name is terrible
Ask someone good at naming - Find a person skilled at naming and describe your problem to them Access to expertise; the act of describing the problem may help you discover a good name yourself Self explanatory

In this case "it" and "one" are pronouns. It feels too far from the domain of the 99 Bottles song, but it also feels more correct than anything else I can think of.

Let’s use pronoun by first defining the method, then making it return the else branch. After that add the pronoun method in the actual code, run tests, then add a default argument, then we add another branch to cover the 1 branch

Now that else branch and 1 branch are identical and as we are getting used to this recipe the coding part becomes easier, however the naming part is the challenging.

  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, " +
      "no more bottles of beer.\n" +
      "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    when 1
      "#{number} #{container(number)} of beer on the wall, " +
      "#{number} #{container(number)} of beer.\n" +
      "Take #{pronoun(number)} down and pass it around, " +
      "no more bottles of beer on the wall.\n"
    else
      "#{number} #{container(number)} of beer on the wall, " +
      "#{number} #{container(number)} of beer.\n" +
      "Take #{pronoun(number)} down and pass it around, " +
      "#{number - 1} #{container(number - 1)} of beer on the wall.\n"
    end
  end

  def container(number)
  ...
  end

  def pronoun(number)
    if number == 1
      "it"
    else
      "one"
    end
  end

Watch 3: Deriving Names From Responsibilities

We finished last lesson by picking def pronoun(number) for the "it" and "one" and it was difficult to come up with that name, it’s even more complex when we don’t know the domain.

We’ll continue with the same flocking rules as previously, pick the code that are similar, remove differences, create a new method to inject in that space and remove unused code.

A good trick for naming is “What is the responsibility of the method or function that I’m trying to create to replace this difference?”

At least for now, the good enough name for the change on "no more" words is going to be "quantity".

Watch 4: Choosing Meaningful Defaults

In this section we add the "def quantity" method and play with a default argument "def quantity(number=0)"

When you can, use the else branch first.

def verse(number)
  case number
  when 0
    "No more bottles of beer on the wall, " +
    "no more bottles of beer.\n" +
    "Go to the store and buy some more, " +
    "99 bottles of beer on the wall.\n"
  else
    "#{number} #{container(number)} of beer on the wall, " +
    "#{number} #{container(number)} of beer.\n" +
    "Take #{pronoun(number)} down and pass it around, " +
    "#{quantity(number - 1)} #{container(number - 1)} of beer on the wall.\n"
  end
end

def quantity(number=0)
  if number == 0
    "no more"
  else
    number
  end
end

def container(number)

def pronoun(number)

Watch 5: Seeking Stable Landing Points

The section emphasizes the importance of consistency in code style to reduce mental load and business costs. Sandi Metz explains that consistent, similarly structured methods make code easier to read and maintain. She advises teams to adopt and follow a style guide, even if imperfect, because any consistent style is better than none.

If teams can’t agree, rotate disputed styles for a month; if seniors resist, let them keep their own area but require adherence to team style elsewhere. Over time, consistency benefits everyone by highlighting real differences in code and lowering long-term costs.

Watch 6: Obeying the Liskov Substitution Principle

The point of Liskov is that objects have to be what they say they are. They have to behave like you expect. They can’t do anything that forces folks that interact with them to check what kind of a thing they are in order to know how to talk to them.

Please notice the consistency we have in the new methods, all of them have if statements, and receive an argument number however we are doing a kind of a duck type. 🦆

Example:

# Define the "duck type" role: every Contract must respond to `name`
class PrivateContract
  def initialize(person_name)
    @person_name = person_name
  end

  def name
    @person_name
  end
end

class CommercialContract
  def initialize(business_name)
    @business_name = business_name
  end

  def name
    @business_name
  end
end

# Code that works with any kind of contract
def print_contract_name(contract)
  puts "Contract with: #{contract.name}"
end

# Example usage
private_contract   = PrivateContract.new("John Doe")
commercial_contract = CommercialContract.new("Acme Corporation")

print_contract_name(private_contract)
# => Contract with: John Doe

print_contract_name(commercial_contract)
# => Contract with: Acme Corporation

In the example above, we unified the API so that both private and commercial contracts respond to the same message: .name

Here is the code by fixing it from the quantity method to respond to .capitalize

  def verse(number)
    case number
    when 0
      "#{quantity(number).capitalize} bottles of beer on the wall, " +
      "no more bottles of beer.\n" +
      "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    else
      "#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
      "#{number} #{container(number)} of beer.\n" +
      "Take #{pronoun(number)} down and pass it around, " +
      "#{quantity(number - 1)} #{container(number - 1)} of beer on the wall.\n"
    end
  end

  def quantity(number=0)
    if number == 0
      "no more"
    else
      number.to_s
    end
  end

  def container(number)

  def pronoun(number)

Even in a dynamically-typed language like Ruby, you should never check an object’s type just to decide what message to send. Instead, define a clear role (a duck type) and make sure every object that plays that role conforms to its API.

Watch 6: Taking Bigger Steps

So we’ve been through this pattern a couple of times where we turned small differences into methods, so we could send messages to make things the same.

We grabbed that shape (verse method and 4 case statements) and moved it down into these new methods that we’re creating.

def verse(number)
  case number
  when 0
    "#{quantity(number).capitalize} bottles of beer on the wall, " +
    "#{quantity(number)} #{container(number)} of beer.\n" +
    action(number) +
    "99 bottles of beer on the wall.\n"
  else
    "#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
    "#{quantity(number)} #{container(number)} of beer.\n" +
    action(number) +
    "#{quantity(number - 1)} #{container(number - 1)} of beer on the wall.\n"
  end
end

def action(number)
  if number == 0
    "Go to the store and buy some more, "
  else
    "Take #{pronoun(number)} down and pass it around, "
  end
end

def quantity(number)

def container(number)

def pronoun(number)

Watchn 7: Discovering Deeper Abstractions

We’ve almost arrived to have same code on both branches, except the last line of verse method.

In our first apporach we proposed adding a new branch to if statemnt

def verse(number)
  case number
  when 0
    "#{quantity(number).capitalize} bottles of beer on the wall, " +
    "#{quantity(number)} #{container(number)} of beer.\n" +
    action(number) +
    "#{quantity(number - 1)} bottles of beer on the wall.\n"
  else
    "#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
    "#{quantity(number)} #{container(number)} of beer.\n" +
    action(number) +
    "#{quantity(number - 1)} #{container(number - 1)} of beer on the wall.\n"
  end
end

def quantity(number)
  if number == -1 # 👈
    99
  elsif number == 0
      "no more"
  else
    number.to_s
  end
end

With this change the tests are green however "-1" isn’t a valid value in the context of the song, which has to make you wonder whether things are working by accident and not really working by design.

We’d better ask ourselves, what is the responsibility of the quantity method?

quantity is responsible for knowing what to sing in place of a number. If there are 50, you’re going to sing “50”, if there’s 0, you’re going to sing “no more”. This is the method that represents the mapping between the value of a number and the string that you sing in its place.

As the song progresses, the verse number gets decremented, except when we reach 0, it wraps back around to the top and starts over again with 99.

When you’re confused, very often a good strategy is: don’t try to solve the whole problem straight away. If you can nibble away at it, solving the simple parts of the problem might make the hard ones easier.

And here we already have a rule for what to do when we’re confused; it’s to try to make things more alike, even if not yet identical, using code that we’ve already written.

You can think of this next verse as the successive verse. If I ask you for the successor of the letter B, you would tell me C, you wouldn’t tell me A. We think of A as the predecessor and B as a successor. I kind of like the word successor here, but we have to agree that successor means next and not necessarily next higher.

  def verse(number)
    case number
    when 0
      "#{quantity(number).capitalize} bottles of beer on the wall, " +
      "#{quantity(number)} #{container(number)} of beer.\n" +
      action(number) +
      "#{quantity(succesor(number))} #{container(number)} of beer on the wall.\n"
    else
      "#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
      "#{quantity(number)} #{container(number)} of beer.\n" +
      action(number) +
      "#{quantity(number - 1)} #{container(number - 1)} of beer on the wall.\n"
    end
  end

  def succesor(number)
    if number == 0
      "99"
    else
      number - 1
    end
  end

  def action(number)

  def quantity(number)

  def container(number)

  def pronoun(number)

Watch 8: Depending on Abstractions

Abstractions are beautiful things. They allow you to consolidate the implementation details for an idea in your code in a single place so that everybody can use it if they know its name. They give a name to those things so that you can have a conversation with people using this shortcut language, instead of having to describe the whole thing.

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)
    "#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
    "#{quantity(number)} #{container(number)} of beer.\n" +
    action(number) +
    "#{quantity(succesor(number))} #{container(succesor(number))} of beer on the wall.\n"
  end

  def succesor(number)
    if number == 0
      "99"
    else
      number - 1
    end
  end

  def action(number)
    if number == 0
      "Go to the store and buy some more, "
    else
      "Take #{pronoun(number)} down and pass it around, "
    end
  end

  def quantity(number=0)
    if number == 0
      "no more"
    else
      number.to_s
    end
  end

  def container(number)
    if number == 1
      "bottle"
    else
      "bottles"
    end
  end

  def pronoun(number)
    if number == 1
      "it"
    else
      "one"
    end
  end
end

We use the Flocking Rules to convert four concrete verse_templates into a single more abstract verse method. And along the way, we found a bunch of smaller, internal abstractions and we’ve created methods for them and named them.

It’s important to ask whether this new code actually improves upon the Shameless Green variant from which we started. Most programmers do think it’s better, but you may be sad to find out that static analysis tools will score it worse.

We turned one conditional into many and add 55% more code.

The counterbalance of that is that there’s a lot of value in this code.

What we have now, that we didn’t use to have, is a bunch of identified, named concepts. We know that 99 bottles contains a container and a pronoun and a quantity and an action, and even a successor.

This block finished the refactoring that began in previous blocks. It iteratively followed the Flocking Rules to remove differences in the verse method, and as a result unearthed abstractions that were deeply hidden within the 99 Bottles song.

It illustrated the power of the Flocking Rules to uncover sophisticated concepts, even those which cast only dim shadows in the existing code. You don’t have to understand the entire problem in order to find and express the correct abstractions—you merely apply these rules, repeatedly, and abstractions will naturally appear.

One final thought before moving on. Consider this question: If several different programmers started from Shameless Green and refactored the verse method according to the Flocking Rules, what would the resulting code look like? If you’ve guessed that everyone’s code would be identical, excepting the names used for the concepts, you’d be right. This has enormous value.

Now we’ll return to the “six-pack” problem.