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