Inheritance can make code fragile and inflexible, while composition is more flexible. The document discusses favoring composition over inheritance and provides examples. It shows how inheritance between an Order class and CampaignOrder class can break if Order is refactored. Using composition, different order types implement the same interface without relying on each other's implementations. For beverages, inheritance led to an explosion of subclasses, while composition created reusable wrapper classes. Overall, the document advocates favoring composition over inheritance for increased stability and flexibility.
2. Why favor composition over inheritance?
• Inheritance is often overused; composition is a better alternative in
many cases
• Inheritance can make your code fragile
• Inheritance is inflexible
2
4. Example: piece of code used by a cafe
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);
}
}
4
5. New requirement: discount campaign
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;
}
}
5
6. Refactoring of Order can break CampaignOrder
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 here:
// subTotal = subTotal.add(beverage.price());
add(beverage);
}
...
}
• This refactoring has broken CampaignOrder;
now CampaignOrder#numberOfBeverages gets incremented twice for every addAll() call
• The problem: CampaignOrder relies on an implementation detail of its super class
• Using inheritance this way should be avoided because that introduces fragility to the codebase
6
7. Better approach: composition (1/2)
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);
}
}
7
8. Better approach: composition (2/2)
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()); }
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;
}
}
• No refactoring can break CampaignOrder as long as classses keep the contract of the public methods
• CampaignOrder no longer relies on any implementation details of any class
• Composition reduces chances of unforeseen breakage; it will make a codebase more stable 8
10. Example: piece of code used by a cafe
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"; }
}
10
11. New requirement: multiple condiments
• Too many subclasses; we will need to create many subclasses every time we introduce a new condiment
• What if we want to reuse code which is responsible for a condiment for another beverage (e.g. a Tea class) ?
• Inheritance is not flexible enough for this use case
11
12. Better approach: composition (1/3)
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"; }
} 12
14. Better approach: composition (3/3)
@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");
}
• Introducing a new condiment doesn't impact any other class (there will be no class explosion)
• Those wrapper classes are highly reusable; they can be reused for anything which implements Beverage
• Much more flexible than inheritance for this use case 14
15. Conclusion
• Improper use of inheritance can make your code fragile and inflexible
• Using inheritance just for code reuse can lead to an unforeseen
problem in the future
• When you are tempted to use inheritance, using composition instead
can be a good idea
• Further reading:
• Head First Design Patterns
• Design Patterns: Elements of Reusable Object-Oriented Software
15