dom lizarraga

    dominiclizarraga@hotmail.com

    POOD Session 4: Cure Primitive Obsession with Extract Class

    11 minutes to read

    Session 4: Cure Primitive Obsession with Extract Class

    Date: August 30, 2025

    This blog post consists in one part:

    Key Concepts

    • Primitive obsession
    • Extract Class refactoring pattern
    • Naming classes after what they are vs. what they do
    • Modeling abstract concepts as objects

    Cure Primitive Obsession with Extract Class

    Watch 1: Selecting the Target Code Smell

    In this lesson we have more code, more abstraction which means more levels of indirection. It’s not really more complex and mini-differences are hidden in these abstractions.

    We’re midway through refactoring. Seeing the code become more complicated is normal. We can learn more in the next talk. All the little things talk.

    Refactoring recipes don’t promise to make the code simpler, they promise to help you make a very planned change and if fail revert it.

    The code is not open yet, and we chose the code duplication as code smell to solve and we followed flocking rules for identifying abstractions.

    We need to make a decision if we continue with our current code and if we revert it and pick a different code smell.

    We’ve got to ask ourselves, did the refactor we implement, isolate more the parts I want to vary? ("six-pack")

    And the answer is yes, quatity(number) and container(number)

    If we were to vary container or quantity now, we’re dealing with a smaller amount of code.

    Sandi recommended to move on and pick another code smell.

    We had to pick the next code smell and Sandi helped us with the following questions:

    • Do any of the methods have the same shape?

    • Are there any arguments that have the same name?

    • If you were to put the private keyword in here, where would it go?

    • If this were 2 classes, were would you split it?

    • The method that have arguments, do they depend on the argument itself or the whole class?

    Almost all questions are YES.

    Something that caught my attention was how the number argument started as a verse in song and verses(upper, lower) methods but in verse(number) it becomes a bottle number, which is confusing, same argument name for two different concepts.

    A worse problem is when we use the different names for the same concept.

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

    Let’s evaluate the container(number):

    You passed it an argument, and that argument is so impaired that it does not come with its own behavior. You force it to know that it should test it for some values, and you force it to know what behavior to supply, based on the value that it tested for.

    You force it into the conditional. You forced it to know the different values that it might test in the conditional. And you forced it to know the behavior that applied to each of those. This is not OO.

    We should be able to call number.container at this point we cannot. The code needs a smarter number.

    Watch 2: Extracting Classes

    A number of methods take the same argument (number). Most of them have the same shape. They contain a conditional, they would be considered private, and they depend more on the argument that got passed than the class as a whole.

    These things suggest the Primitive Obsession code smell.

    The cure for Primitive Obsession is to extract a class.

    We’ll “wrap a coat of behavior around a primitive,” we’re taking something simple like a number or a string and giving it a bit of intelligence.

    Now, since we’re going to create a class, the first thing we do is name it. What is this thing? Example: it’s a bottle. It’s blue, 22 ounces, made of plastic, and holds water. But remember what we write in code isn’t the bottle itself. It’s a representation of all bottles.

    Think of a boolean, like true, you can’t pick one up or see it. It’s not physical; it’s an idea. In Ruby, true is just a variable that points to an instance of TrueClass.

    That’s the beauty of object-oriented programming: it lets you build worlds where ideas are as real as physical objects. When you start modeling concepts the same way you model things, you’ve crossed an important milestone as an OO programmer. Those hidden, abstract ideas, the ones that live in the relationships between real objects, are where the most powerful abstractions are found.

    Another example: in an Event Management System, perhaps you have buyers and tickets and there’s a bunch of interactions between them. You have purchases and refunds and discounts and you could resell your tickets.

    Now, all of the logic to do those actions, those operations, could exist in either ticket or buyer, but it’s better if they get modeled as their own separate things. If you do that, and put the logic in its own object, you create objects that are easier to understand, easier to change, easier to vary, and easier to test.

    And since tests are just a form of reuse, a test is often the first reuse of an object. If you can test it, if it’s easy to test, it means it will be also easy to reuse.

    Here is another example to illustrate what primitive obsession code smell is:

    # BEFORE: Primitive Obsession
    class Order
      def initialize(country_code)
        @country_code = country_code
      end
    
      def tax_rate
        if @country_code == "US"
          0.07
        elsif @country_code == "MX"
          0.16
        else
          0.20
        end
      end
    
      def currency
        if @country_code == "US"
          "USD"
        elsif @country_code == "MX"
          "MXN"
        else
          "EUR"
        end
      end
    end
    
    # AFTER: Extract Class to cure Primitive Obsession
    class Order
      def initialize(country)
        @country = country
      end
    
      def tax_rate
        @country.tax_rate
      end
    
      def currency
        @country.currency
      end
    end
    
    class Country
      attr_reader :code
    
      def initialize(code)
        @code = code
      end
    
      def tax_rate
        case code
        when "US" then 0.07
        when "MX" then 0.16
        else 0.20
        end
      end
    
      def currency
        case code
        when "US" then "USD"
        when "MX" then "MXN"
        else "EUR"
        end
      end
    end
    

    When naming instance methods, we asserted that methods ought to be named one level of abstraction higher than what they do, but the rule is different for classes. Classes ought to be named after what they are, and therefore this new class is a BottleNumber.

    The class Bottles is now free of conditionals and BottleNumber only deals with number nothing about the Song and this is how this the code we finished with during this session.

    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(successor(number))} #{container(successor(number))} of beer on the wall.\n"
      end
    
      def quantity(number)
        BottleNumber.new(number).quantity
      end
    
      def container(number)
        BottleNumber.new(number).container
      end
    
      def action(number)
        BottleNumber.new(number).action
      end
    
      def successor(number)
        BottleNumber.new(number).successor
      end
    end
    
    class BottleNumber
      attr_reader :number
    
      def initialize(number)
        @number = number
      end
    
      def quantity
        if number == 0
          "no more"
        else
          number.to_s
        end
      end
    
      def container
        if number == 1
          "bottle"
        else
          "bottles"
        end
      end
    
      def action
        if number == 0
          "Go to the store and buy some more"
        else
          "Take #{pronoun} down and pass it around"
        end
      end
    
      def pronoun
        if number == 1
          "it"
        else
          "one"
        end
      end
    
      def successor
        if number == 0
          99
        else
          number - 1
        end
      end
    end