DEV Community

Cover image for Solving the Beverages Prices Refactoring kata (1)
Manuel Rivero
Manuel Rivero

Posted on • Edited on • Originally published at codesai.com

Solving the Beverages Prices Refactoring kata (1)

Introduction.

We are going to show a possible solution to the Beverages Prices Refactoring kata that we developed recently with some people from Women Tech Makers Barcelona with whom I'm doing Codesai's Practice Program twice a month.

The Beverages Prices Refactoring kata shows an example of inheritance gone astray. The initial code computes the price of the different beverages that are sold in a coffee house. There are some supplements that can be added to those beverages. Each supplement increases the price a bit. Not all combinations of drinks and supplements are possible.

As part of the kata, we are asked to add an optional cinnamon supplement that costs 0.05€ to all our existing catalog of beverages. We are also advised to refactor the initial code a bit before introducing the new feature. Let's see why.

The initial code.

To get an idea of the kind of problem we are facing, we'll have a look at the code. There are 8 files: a Beverage interface and 7 classes, one for each type of beverage and one for each allowed combination of beverages and supplements.

Image Initial code files

Initial code.

A closer look reveals that the initial design uses inheritance and polymorphism to enable the differences in computing the price of each allowed combination of beverage and supplements. This is the inheritance hierarchy:

Image Inheritance hierarchy in the initial code

Class diagram showing the inheritance hierarchy in the initial code.

If that diagram is not enough to scare you, have a quick look at the unit tests of the code:

package unit_tests;

import beverages.*;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;

public class BeveragesPricingTest {
  private static final double PRECISION = 0.001;

  @Test
  public void computes_coffee_price() {
    Beverage coffee = new Coffee();
    assertThat(coffee.price(), is(closeTo(1.20, PRECISION)));
  }

  @Test
  public void computes_tea_price() {
    Beverage tea = new Tea();
    assertThat(tea.price(), is(closeTo(1.50, PRECISION)));
  }

  @Test
  public void computes_hot_chocolate_price() {
    Beverage hotChocolate = new HotChocolate();
    assertThat(hotChocolate.price(), is(closeTo(1.45, PRECISION)));
  }

  @Test
  public void computes_tea_with_milk_price() {
    Tea teaWithMilk = new TeaWithMilk();
    assertThat(teaWithMilk.price(), is(closeTo(1.60, PRECISION)));
  }

  @Test
  public void computes_coffee_with_milk_price() {
    Coffee coffeeWithMilk = new CoffeeWithMilk();
    assertThat(coffeeWithMilk.price(), is(closeTo(1.30, PRECISION)));
  }

  @Test
  public void computes_coffee_with_milk_and_cream_price() {
    Coffee coffeeWithMilkAndCream = new CoffeeWithMilkAndCream();
    assertThat(coffeeWithMilkAndCream.price(), is(closeTo(1.45, PRECISION)));
  }

  @Test
  public void computes_hot_chocolate_with_cream_price() {
    HotChocolateWithCream hotChocolateWithCream = new HotChocolateWithCream();
    assertThat(hotChocolateWithCream.price(),  is(closeTo(1.60, PRECISION)));
  }
}
Enter fullscreen mode Exit fullscreen mode

First, we make the change easy[1].

Given the current design, if we decided to add the new feature straight away, we would end up with 14 classes (2 times the initial number of classes). If you think about it, this would happen for each new supplement we were required to add: we would be forced to double the number of classes. Adding n supplements more would mean multiplying the initial number of classes by 2n.

This exponential growth in the number of classes is a typical symptom of a code smell called Combinatorial Explosion[2]. In this particular case the problem is caused by using inheritance to represent the pricing of beverages plus supplements.

In order to introduce the new cinnamon supplement, we thought sensible to do a bit of preparatory refactoring first in order to remove the Combinatorial Explosion code smell. The recommended refactoring for this code smell is Replace Inheritance with Delegation which leads to a code that uses composition instead of inheritance to avoid the combinatorial explosion. If all the variants[3] (supplements) keep the same interface, we'd be creating an example of the decorator design pattern[4].

Image Class diagram for the decorator design pattern

Class diagram for the decorator design pattern[5].

The decorator pattern provides an alternative to subclassing for extending behavior. It involves a set of decorator classes that wrap concrete components and keep the same interface that the concrete components. A decorator changes the behavior of a wrapped component by adding new functionality before and/or after delegating to the concrete component.

Applying the decorator pattern design to compute the pricing of beverages plus supplements, the beverages would correspond to the concrete components, tea, coffee and hot chocolate; whereas the supplements, milk and cream would correspond to the decorators.

Image New design class diagram using the decorator design pattern

Design using the decorator design pattern to compute the pricing of beverages plus supplements.

We can obtain the behavior for a given combination of supplements and beverage by composing supplements (decorators) with a beverage (base component).

For instance, if we had the following WithMilk decorator for the milk supplement pricing,

package coffee_shop.menu.supplements;

import coffee_shop.Beverage;

public class WithMilk implements Beverage {
    private final Beverage beverage;

    public WithMilk(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double price() {
        return beverage.price() + 0.10;
    }
}
Enter fullscreen mode Exit fullscreen mode

we would compose it with a Tea instance to create the behavior that computes the price of a tea with milk:

public class BeveragesPricingTest {
  ///... 

  @Test
  public void computes_tea_with_milk_price() {
    Beverage teaWithMilk = new WithMilk(new Tea());
    assertThat(teaWithMilk.price(), is(closeTo(1.60, PRECISION)));
  }

  ///... 
}
Enter fullscreen mode Exit fullscreen mode

A nice thing about decorators is that, since they have the same interface as the component they wrap, they are transparent for the client code[6] which never has to know that it's dealing with a decorator. This allows us to pass them around in place of the wrapped object which makes it possible to compose behaviors using as many decorators as we like. The following example shows how to compose the behavior for computing the price of a coffee with milk and cream[7].

public class BeveragesPricingTest {
  // ...

  @Test
  public void computes_coffee_with_milk_and_cream_price() {
    Beverage coffeeWithMilkAndCream = new WithMilk(new WithCream(new Coffee()));
    assertThat(coffeeWithMilkAndCream.price(), is(closeTo(1.45, PRECISION)));
  }

  //...
}
Enter fullscreen mode Exit fullscreen mode

After applying the Replace Inheritance with Delegation refactoring we get to a design that uses composition instead of inheritance to create all the combinations of supplements and beverages. This fixes the Combinatorial Explosion code smell.

Image files after refactoring introducing the decorator design pattern

Code after refactoring introducing the decorator design pattern.

You can have a look at the rest of the test after this refactoring in this gist.

Then, we make the easy change.

Once we had the new design based in composition instead of inheritance in place, adding the requested feature is as easy as creating a new decorator that represents the cinnamon supplement pricing:

package beverages.supplements;

import beverages.Beverage;

public class WithCinnamon implements Beverage {
    private Beverage beverage;

    public WithCinnamon(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double price() {
        return beverage.price() + 0.05;
    }
}
Enter fullscreen mode Exit fullscreen mode

Image code after adding the new feature

Code after adding the new feature.

Remember that using the initial design adding this feature would have involved multiplying by two the number of classes.

What have we gained and lost with this refactoring?

After the refactoring we have a new design that uses the decorator design pattern. This is a flexible alternative design to subclassing for extending behavior that allows us to add behavior dynamically to the objects wrapped by the decorators.

Thanks to this runtime flexibility we managed to fix the Combinatorial Explosion code smell and that made it easier to add the new feature. Now, instead of multiplying the number of classes by two, adding a new supplement only involves adding one new decorator class that represents the new supplement pricing. This new design makes the client code open-closed to the axis of change of adding new supplements.

On the flip side, we have introduced some complexity[8] related to creating the different compositions of decorators and components. At the moment this complexity is being managed by the client code (notice the chains of news in the tests snippets above).

There's also something else that we have lost in the process. In the initial design only some combinations of beverages and supplements were allowed. This fact was encoded in the initial inheritance hierarchy. Now with our decorators we can dynamically add any possible combination of beverages and supplements.

All in all, we think that the refactoring leaves us in a better spot because we'll be likely required to add new supplements, and there are usual improvements we can make to the design to isolate the client code from the kind of complexity we have introduced.

Conclusion.

We have shown an example of preparatory refactoring to make easier the addition of a new feature, and learned about the Combinatorial Explosion code smell and how to fix it using the decorator design pattern to get a new design in which we have protected the client code[9] against variations involving new supplements.

In a future post we will show how to encapsulate the creation of the different compositions of decorators and components using builders and/or factories to hide that complexity from client code, and show how we can limit again the allowed combinations that are part of the menu.

Acknowledgements.

I’d like to thank the WTM study group, and especially Inma Navas for solving this kata with me.

Thanks to my Codesai colleagues and Inma Navas for reading the initial drafts and giving me feedback and and to Lisa Fotios for her picture.

Notes.

[1] This and the next header come from Kent Beck's quote:

"For each desired change, make the change easy (warning: this may be hard), then make the easy change"

[2] You can find this code smell described in Bill Wake's wonderful Refactoring Workbook.

[3] In this context variant means a variation in behavior. For instance, each derived class in a hierarchy is a variant of the base class.

[4] Have a look at the chapter devoted to the decorator design pattern in the great Head First Design Patterns. It's the most didactic and fun explanation of the pattern I've ever found. This kata is heavily inspired in the example used in that chapter to explain the pattern.

[5] We could have also solved this problem composing functions instead of objects, but we wanted to practice with objects in this solution of the kata. That might be an interesting exercise for another practice session.

[6] In this context client code means code that uses objects implementing the interface that both the components and decorators implement, which in the kata would correspond to the Beverage interface.

[7] Also known as a "cortado leche y leche" in Gran Canaria :)

[8] Complexity is most often the price we pay for flexibility. That's why we should always assess if the gains are worth the price.

[9] Protected variations is another way to refer to the open-closed principle. I particularly prefer that way to refer to this design principle because I think it is expressed in a way that relates less to object orientation. Have a look at Craig Larman's great article about it: Protected Variation: The Importance of Being Closed

References.

Top comments (0)