Kohei Nozaki's blog 

Favor composition over inheritance


Posted on Friday Aug 14, 2020 at 04:52PM in Technology


In this entry, I’ll discuss problems of inheritance, which is often overused in software written in an object oriented programming language, and how we can do better with composition, which is usually a much better alternative to inheritance.

Inheritance can make your code fragile

Let’s think about some piece of software used by some cafe. It contains the following classes:

1e95a7ea 3a54 4fcc ad6b 798b5b3d1ebf

There is an interface called Beverage, which can return the price and description of a beverage. Most probably there are Coffee or Tea classes that implement the interface. And there is a class called Order, where you can add Beverage objects to it in order to calculate the grand total on the bill for a customer of the cafe. The code of these looks like this:

interface Beverage {
    BigDecimal price();
    String description();
}

class Order {

    private static final BigDecimal TAX_RATE = new BigDecimal("0.1");
    private BigDecimal subTotal = BigDecimal.ZERO;

    void add(Beverage beverage) {
        subTotal = subTotal.add(beverage.price());
    }

    void addAll(Collection<? extends Beverage> beverages) {
        for (Beverage beverage : beverages)
            subTotal = subTotal.add(beverage.price());
    }

    BigDecimal grandTotal() {
        BigDecimal tax = subTotal.multiply(TAX_RATE);
        return subTotal.add(tax);
    }
}

Now, let’s consider the following scenario: the owner of the cafe wants to start a campaign to boost their sales. The idea of the campaign is that if a customer orders more than 2 beverages at the same time, he/she will get 20% discount from the grand total. In order to implement this requirement, a programmer comes up with the CampaignOrder class which extends the Order class. It looks like the following:

class CampaignOrder extends Order {

    private static final BigDecimal DISCOUNT_RATE = new BigDecimal("0.2");
    private int numberOfBeverages;

    @Override
    void add(Beverage beverage) {
        super.add(beverage);
        numberOfBeverages++;
    }

    @Override
    void addAll(Collection<? extends Beverage> beverages) {
        super.addAll(beverages);
        numberOfBeverages += beverages.size();
    }

    @Override
    BigDecimal grandTotal() {
        BigDecimal grandTotal = super.grandTotal();
        if (numberOfBeverages > 2) {
            BigDecimal discount = grandTotal.multiply(DISCOUNT_RATE);
            grandTotal = grandTotal.subtract(discount);
        }
        return grandTotal;
    }
}

It captures add() and addAll() method calls, forwards them to its superclass and keeps track of the number of the beverages which are added in the numberOfBeverages variable. And it also captures grandTotal() method calls, forwards them to its superclass and applies the 20% discount to the grand total the superclass calculated depending on the numberOfBeverages variable.

It might look reasonable as it reuses the Order class effectively so that it won’t introduce any duplicate code. But this approach can lead to an unforseen issue due to the fact that the CampaignOrder class relies on a hidden behavior of the Order class, which is that the add() method and the addAll() method work independently. Consider that at some point some other programmer has done some quick refactoring in the addAll() method:

class Order {
    ...
    void add(Beverage beverage) {
        subTotal = subTotal.add(beverage.price());
    }

    void addAll(Collection<? extends Beverage> beverages) {
        for (Beverage beverage : beverages)
            // Someone has done refactoring. Original code was:
            // subTotal = subTotal.add(beverage.price());
            // Now:
            add(beverage);
    }
    ...
}

It hasn’t broken anything in terms of the functionality of the Order class but unfortunately it has just broken the CampaignOrder class. Remember the implementation of the CampaignOrder class which captures both of the add() and the addAll() method calls and counts the number of the beverage objects it receives. Due to the fact that now the Order class calls the add() method from the addAll() method, whenever the addAll() method of the CampaignOrder class gets called, the number of the beverage objects gets counted twice in the CampaignOrder class. In other words, now the following test case fails:

@ExtendWith(MockitoExtension.class)
class CampaignOrderTest {

    @Mock
    Beverage coffee, tea;
    CampaignOrder sut = new CampaignOrder();

    @Test
    void addAll() {
        when(coffee.price()).thenReturn(new BigDecimal("2.0"));
        when(tea.price()).thenReturn(new BigDecimal("3.0"));

        sut.addAll(Arrays.asList(coffee, tea));

        // It fails after the refactoring. Now grandTotal() returns 4.4
        // The discount is applied unexpectedly since the logic which counts beverages is broken
        // Now it's counted as 4, which is over the threshold of the discount
        assertThat(sut.grandTotal()).isEqualByComparingTo("5.5");
    }
}

The person who has done this refactoring should not be blamed. In fact this person removed a duplicate piece of code, which is a good thing, and it’s not easy to catch such an error. The real problem here is the wrong use of inheritance, which is writing a subclass that relies on an implementation detail of its super class. That introduces fragility to the codebase. Hence using inheritance this way should be avoided.

And also there can be another problematic case where a new method has been added to the Order class. If the new method can be used for adding a beverage, we need to make sure that the CampaignOrder class captures method calls to the new method, but chances are we would not even notice that there was a subclass which we might have to change.

What could have been done instead of inheritance then? The most obvious approach is using composition instead. Let’s rework the class hierarchy and make it like this:

1d07a6e0 893d 4104 bfdb 177221cd449b

The implementation:

interface Order {
    void add(Beverage beverage);
    void addAll(Collection<? extends Beverage> beverages);
    BigDecimal grandTotal();
}

class RegularOrder implements Order {

    private static final BigDecimal TAX_RATE = new BigDecimal("0.1");
    private BigDecimal subTotal = BigDecimal.ZERO;

    @Override
    public void add(Beverage beverage) {
        subTotal = subTotal.add(beverage.price());
    }

    @Override
    public void addAll(Collection<? extends Beverage> beverages) {
        for (Beverage beverage : beverages)
            subTotal = subTotal.add(beverage.price());
    }

    @Override
    public BigDecimal grandTotal() {
        BigDecimal tax = subTotal.multiply(TAX_RATE);
        return subTotal.add(tax);
    }
}

class CampaignOrder implements Order {

    private static final BigDecimal DISCOUNT_RATE = new BigDecimal("0.2");
    private int numberOfBeverages;

    private final Order delegate;

    CampaignOrder() {
        this(new RegularOrder());
    }

    private CampaignOrder(Order delegate) {
        this.delegate = delegate;
    }

    @Override
    public void add(Beverage beverage) {
        delegate.add(beverage);
        numberOfBeverages++;
    }

    @Override
    public void addAll(Collection<? extends Beverage> beverages) {
        delegate.addAll(beverages);
        numberOfBeverages += beverages.size();
    }

    @Override
    public BigDecimal grandTotal() {
        BigDecimal grandTotal = delegate.grandTotal();
        if (numberOfBeverages > 2) {
            BigDecimal discount = grandTotal.multiply(DISCOUNT_RATE);
            grandTotal = grandTotal.subtract(discount);
        }
        return grandTotal;
    }
}

Now the Order class has become an interface which has 2 implementations. One is the RegularOrder class, which was formerly the Order class, and the other one is the CampaignOrder class. An instance of the CampaignOrder class has a reference to a RegularOrder instance in order to reuse its functionality, but the CampaignOrder class doesn’t have any superclass anymore. With the new design, no refactoring of the RegularCampaign class can break the CampaignOrder class as long as the RegularOrder class keeps the contract of the public methods.

One important difference from the inheritance approach is that now the CampaignOrder class no longer relies on any of the implementation details of the RegularOrder class. What it relies on now is the behavior of the public methods of the RegularOrder class, which are all defined in the Order interface.

The good thing about sticking with this approach is that as long as a class keeps its functionality on the public interface level the same, it can change its internal structure without you worrying about breaking something accidentally. It greatly reduces chances of unforseen breakage. Therefore, it will make your codebase more stable. Also, with having the interface, when a new method has been added to the interface, it will make the compilation of its implementors fail since the implementation is missing. With that, unlike with the inheritance solution, we can notice that we have to add the missing implementation to its implementors immediately.

Inheritance is inflexible

Now let’s think about some details of the Beverage interface in the codebase. It has an implemention class called Coffee. The customer can add a condiment such as milk, whip or sugar into it if they want to. For some reason, the original programmer implemented this requirement using inheritance:

faea3261 015d 43d8 be9a 69c44d88c388
interface Beverage {
    BigDecimal price();
    String description();
}

class Coffee implements Beverage {
    @Override public BigDecimal price() { return new BigDecimal("1.99"); }
    @Override public String description() { return "Coffee"; }
}

class CoffeeWithMilk extends Coffee {
    @Override public BigDecimal price() { return super.price().add(new BigDecimal("0.10")); }
    @Override public String description() { return super.description() + ", Milk"; }
}

class CoffeeWithWhip extends Coffee {
    @Override public BigDecimal price() { return super.price().add(new BigDecimal("0.15")); }
    @Override public String description() { return super.description() + ", Whip"; }
}

class CoffeeWithSugar extends Coffee {
    @Override public BigDecimal price() { super.price().add(new BigDecimal("0.05")); }
    @Override public String description() { return super.description() + ", Sugar"; }
}

Now we’ve got a new requirement to implement, which is that a customer should be able to add multiple condiments into coffee. Let’s see if we can do that with inheritance. It would look like the following:

2d51746f 83fd 43ba b0c5 3628272b7d88

We ended up creating a lot of subclasses for all of the combinations. That will do for the time being but think about the future expansion. We will need to create many subclasses everytime we introduce a new condiment. And what if we want to reuse code which is responsible for a condiment for another beverage class, say, a Tea class? It’s not clear if we can do that in a reasonable way with this approach.

Let’s see if we could do better with composition. It would look like the following:

9b0b3122 c78b 4f45 b358 b30e682519dc

The implementation (the Beverage interface and the Coffee class are the same as the ones we have seen before):

class MilkWrapper implements Beverage {
    private final Beverage delegate;

    MilkWrapper(Beverage delegate) { this.delegate = delegate; }

    @Override public BigDecimal price() { return delegate.price().add(new BigDecimal("0.10")); }
    @Override public String description() { return delegate.description() + ", Milk"; }
}

class WhipWrapper implements Beverage {
    private final Beverage delegate;

    WhipWrapper(Beverage delegate) { this.delegate = delegate; }

    @Override public BigDecimal price() { return delegate.price().add(new BigDecimal("0.15")); }
    @Override public String description() { return delegate.description() + ", Whip"; }
}

class SugarWrapper implements Beverage {
    private final Beverage delegate;

    SugarWrapper(Beverage delegate) { this.delegate = delegate; }

    @Override public BigDecimal price() { return delegate.price().add(new BigDecimal("0.05")); }
    @Override public String description() { return delegate.description() + ", Sugar"; }
}

In the new implementation, the classes responsible for condiments hold a reference to a Beverage object instead of extending the Coffee class. They can still reuse the code of the Coffee class but through the Beverage interface. Instead of creating an instance of a subclass of the Coffee class, you need to provide a Coffee instance to the constructor of one of the wrapper classes. Let’s do it like this:

@Test
void coffeeWithMilk() {
    Beverage coffeeWithMilk = new MilkWrapper(new Coffee());
    assertThat(coffeeWithMilk.description()).isEqualTo("Coffee, Milk");
    assertThat(coffeeWithMilk.price()).isEqualByComparingTo("2.09");
}

@Test
void coffeeWithWhip() {
    Beverage coffeeWithWhip = new WhipWrapper(new Coffee());
    assertThat(coffeeWithWhip.description()).isEqualTo("Coffee, Whip");
    assertThat(coffeeWithWhip.price()).isEqualByComparingTo("2.14");
}

@Test
void coffeeWithSugar() {
    Beverage coffeeWithSugar = new SugarWrapper(new Coffee());
    assertThat(coffeeWithSugar.description()).isEqualTo("Coffee, Sugar");
    assertThat(coffeeWithSugar.price()).isEqualByComparingTo("2.04");
}

Now, let’s see how we can implement the new requirement about multiple condiments with the new design. In fact, no change is needed in the Coffee class and the other classes in the diagram. We can do that like this:

@Test
void coffeeWithMilkAndWhip() {
    Beverage coffee = new Coffee();
    coffee = new MilkWrapper(coffee);
    coffee = new WhipWrapper(coffee);

    assertThat(coffee.description()).isEqualTo("Coffee, Milk, Whip");
    assertThat(coffee.price()).isEqualByComparingTo("2.24");
}

@Test
void coffeeWithMilkAndSugar() {
    Beverage coffee = new Coffee();
    coffee = new MilkWrapper(coffee);
    coffee = new SugarWrapper(coffee);

    assertThat(coffee.description()).isEqualTo("Coffee, Milk, Sugar");
    assertThat(coffee.price()).isEqualByComparingTo("2.14");
}

@Test
void coffeeWithMilkAndWhipAndSugar() {
    Beverage coffee = new Coffee();
    coffee = new MilkWrapper(coffee);
    coffee = new WhipWrapper(coffee);
    coffee = new SugarWrapper(coffee);

    assertThat(coffee.description()).isEqualTo("Coffee, Milk, Whip, Sugar");
    assertThat(coffee.price()).isEqualByComparingTo("2.29");
}

First, we create an instance of the Coffee class and then wrap it with the wrapper classes as needed. This is much more flexible than the old approach which required having subclasses for all of the combinations. We can even combine one particular condiment multiple times, which would have been much harder with the inheritance approach:

@Test
void coffeeWithTripleWhip() {
    Beverage coffee = new Coffee();
    coffee = new WhipWrapper(coffee);
    coffee = new WhipWrapper(coffee);
    coffee = new WhipWrapper(coffee);

    assertThat(coffee.description()).isEqualTo("Coffee, Whip, Whip, Whip");
    assertThat(coffee.price()).isEqualByComparingTo("2.44");
}

This design is also better in terms of maintainability. Introducing a new condiment doesn’t impact any other class in the diagram (i.e. there will be no class explosion). Those wrapper classes are highly reusable because they can be reused for anything which implements the Beverage interface. Also, they don’t depend on anything but the Beverage interface. They solely rely on the public methods in the Beverage interface. There will be no breakage as long as the contract of the Beverage interface stays the same.

Conclusion

We’ve seen how improper use of inheritance can make your code fragile and inflexible. Using inheritance just for code reuse can lead to an unforseen problem in the future. And inheritance is not flexible as you might expect. When you are tempted to use inheritance, I recommend considering if you can do it using composition instead. In my experience, inheritance is not the best option for most of such cases.

I would also like to mention that composition opens up a whole new world of clever design ideas in an object oriented programming language. If you want to learn more about it, I recommend checking out the following books:



No one has commented yet.

Leave a Comment

HTML Syntax: NOT allowed