Welcome to the third in a series of posts in which we will answer the questions:

  • How does complexity sneak into software?
  • How can we recognize it before it becomes painful?
  • And, how can we remove it permanently?

Here's a link to the previous post: As a patron, I want coffee


With our superior coffee machines now taking business away from the competition, research tells us there's an opportunity to disrupt the tea market.

So, what does it take to steep tea?

  • Grab a cup
  • Heat some water
  • Put a tea bag into the cup
  • And, pour the hot water into the cup

Setting Up

Our machine already does most of what we need. In fact, it actually does more than we need. We won't need the grinder, for example. But, there is one new method to add to the driver:

dispense_tea_bag()

So, we need to extend our mock driver with the new method:

class MockDriver
  ...
  
  def dispense_tea_bag; end
end

The Test

Now we're ready to write a test that makes sure we're using the machine in the proper order to steep tea:

example "As a patron, I want tea" do
  # ARRANGE
  driver = instance_double(
    MockDriver,
    dispense_cup: nil,
    dispense_tea_bag: nil,
    dispense_water: nil,
    engage_boiler: nil,
    empty_boiler: nil
  )
  machine = CoffeeMachine.new(driver: driver)

  # ACT
  machine.vend(beverage: :tea)

  # ASSERT
  expect(driver).to have_received(:dispense_cup).ordered
  expect(driver).to have_received(:dispense_water).ordered
  expect(driver).to have_received(:engage_boiler).with(seconds: 30).ordered
  expect(driver).to have_received(:dispense_tea_bag).ordered
  expect(driver).to have_received(:empty_boiler).ordered
end

The Code

With the test in place, we're able to add tea to vend method:

class CoffeeMachine
  attr_reader :driver

  def initialize(driver:)
    @driver = driver
  end

  def vend(beverage: :coffee)
    if beverage == :tea
      dispense_cup
      heat_water
      dispense_tea_bag
      dispense_hot_water
    else
      dispense_cup
      heat_water
      grind_coffee_beans
      dispense_hot_water  
      dispose_grounds
    end
  end
  
  private
  
  def dispense_cup
    driver.dispense_cup
  end

  def heat_water
    driver.dispense_water
    driver.engage_boiler(seconds: 30)
  end

  def grind_coffee_beans
    driver.dispense_coffee_beans
    driver.engage_grinder(seconds: 15)
    driver.engage_filter
    driver.empty_grinder
  end
    
  def dispense_hot_water
    driver.empty_boiler
  end
    
  def dispose_grounds
    driver.empty_filter
  end

  def dispense_tea_bag
    driver.dispense_tea_bag
  end
end

Two tests passing! Yay! But, we introduced some duplication in the #vend method. A common pattern at this point would be to DRY this code up.

def vend(beverage: :coffee)
  dispense_cup
  heat_water
  
  if beverage == :tea
    dispense_tea_bag
  else
    grind_coffee_beans
  end

  dispense_hot_water

  dispose_grounds if beverage == :coffee
end

For the record, here's the Flog score for the #vend method at various points in time:

 5.5 - Serves coffee only
11.8 - Serves coffee or tea with duplication
 8.5 - Serves coffee or tea without duplication

Why did the source code become more complex with the addition of tea? Complexity increased because we added a conditional and more branches (method calls) to the method.

Why did the source code become less complex when we removed the duplication? We removed enough branches from the code to offset the conditional that we added for disposing of the grounds.

Flog Scores

But wait! Is 8.5 a good Flog score? It's better than 11.8, for sure — higher numbers indicate higher complexity. But, how do we know a good score from a bad one?

Well, way back in 2008, Jake Scruggs (author of the methric_fu gem) proposed these numbers:

Score      Means
---------  ---------------------------------------- 
0-10       Awesome
11-20      Good enough
21-40      Might need refactoring
41-60      Possible to justify
61-100     Danger
100-200    Whoop, whoop, whoop
200 +      Someone please think of the children

We use these numbers in our own work. They help us know when it's time to pause and reflect on a method before adding more code to it. In fact, we even incorporated them into our Ruby Flog VS Code extension.

Image of Ruby Flog VS Code Extension in action.

Conclusion

Alright! We've completed two user stories and are already disrupting multiple vending machine markets. We should reach unicorn status any day now!

So, that's it for our third post in the series. In our next post, we'll tackle yet another user story: As a patron, I might want condiments.