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.