Post

The Composite Reuse Principle - Composition over Inheritance

Discover the Composite Reuse Principle in Swift! Composition over inheritance ... with pirates!

The Composite Reuse Principle - Composition over Inheritance

Introduction

Ahoy and welcome to Swift for Pirates!

Today, we want to talk about the Composite Reuse Principle. It states that it’s best to prefer composition over inheritance when you want to reuse code to avoid duplication. Or, to be more precise, to prefer object composition over class inheritance.

This is a universal principle. It’s not specific to Swift. You can apply it to any object-oriented programming language, such as Java, C++, Python, or C#. But of course we’re speaking Swift here at Swift for Pirates.

The Problem: Duplicated Code

Let’s say we have three types of ships.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PirateShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func plunder() {
        print("Plundering")
    }
}

class MerchantShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

class PirateHunterShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func arrestPirates() {
        print("Game over!")
    }
}

As you see, we have some duplication here. All three ships have the same sail method, and two of them have shootCannon. Let’s try to improve this by using class inheritance.

Inheritance

So let’s define a superclass Ship.

1
2
3
4
5
class Ship {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }
}

Now all ships can inherit the sail method by subclassing Ship. So they don’t have to duplicate the method anymore.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class PirateShip: Ship {
    func shootCannon(_ direction: String) {
        print("Shooting cannon to the (direction). Boom!")
    }

    func plunder() {
        print("Plundering")
    }
}

class MerchantShip: Ship {
    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

class PirateHunterShip: Ship {
    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func arrestPirates() {
        print("Game over!")
    }
}

The method shootCannon is still duplicated. So let’s create another subclass of Ship.

1
2
3
4
5
class ArmedShip: Ship {
    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }
}

Now all armed ships can inherit this method instead of duplicating it.

1
2
3
4
5
6
7
8
9
10
11
class PirateShip: ArmedShip {
    func plunder() {
        print("Plundering")
    }
}

class PirateHunterShip: ArmedShip {
    func arrestPirates() {
        print("Game over!")
    }
}

This works great so far. We’ve eliminated all duplication. This is what our inheritance hierarchy looks like:

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Ship
    class ArmedShip
    class PirateShip
    class PirateHunterShip
    class MerchantShip

    Ship <|-- ArmedShip
    ArmedShip <|-- PirateShip
    ArmedShip <|-- PirateHunterShip
    Ship <|-- MerchantShip

But now, with all those pirates haunting the seas these days, more and more merchants choose to arm their ships so they can defend themselves.

A Limitation

These are the methods we need for an armed merchant ship:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ArmedMerchantShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

But now we have a lot of duplication again.

If we inherit from ArmedShip, we can get rid of the duplicated sail and shootCannon methods.

1
2
3
4
5
6
7
8
9
class ArmedMerchantShip: ArmedShip {
    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

Or we could inherit from MerchantShip and remove the duplicated sail, load and unload methods. In that case, we’d have to implement shootCannon again, which is not part of MerchantShip.

1
2
3
4
5
class ArmedMerchantShip: MerchantShip {
    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }
}

To eliminate all duplication, we’d have to inherit from both ArmedShip and MerchantShip, which is not possible (at least in Swift, for good reasons). So, with class inheritance, we’re stuck here.

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Ship
    class ArmedShip
    class MerchantShip
    class ArmedMerchantShip

    Ship <|-- ArmedShip
    Ship <|-- MerchantShip
    ArmedShip <|-- ArmedMerchantShip : inherit?
    MerchantShip <|-- ArmedMerchantShip : inherit? 

That’s one limitation of inheritance. It’s not that flexible.

Strong Coupling

Inheritance is described as an is-a relationship. A MerchantShip is a Ship. A PirateShip is an ArmedShip. It’s all or nothing. If you decide to inherit from a class, not only do you inherit all properties and behavior of that class, but you become a kind of that class. That’s the strongest form of coupling.

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Ship
    class ArmedShip
    class PirateShip
    class PirateHunterShip
    class MerchantShip

    Ship <|-- ArmedShip: is a
    ArmedShip <|-- PirateShip: is an
    ArmedShip <|-- PirateHunterShip: is an
    Ship <|-- MerchantShip: is a

Composition

Now let’s try to solve the same problem with composition instead of inheritance. Let’s start with our three ships again. Remember we wanted to eliminate the duplication here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class PirateShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func plunder() {
        print("Plundering")
    }
}

class MerchantShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

class PirateHunterShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func arrestPirates() {
        print("Game over!")
    }
}

Small Components

In order to work with composition, we need to think small. For example, who or what is it that can shoot, is it really the whole ship? No, it’s only the cannon. So let’s define a tiny class Cannon that only does this one thing.

1
2
3
4
5
class Cannon {
    func shoot(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }
}

What do we need to sail? A mast, a sail, a helm, a rudder, but we need all of these together. Let’s call it SailingEquipment.

1
2
3
4
5
class SailingEquipment {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }
}

Now, who does the plundering? Is it the ship itself? No, let’s say that’s the BoardingCrew.

1
2
3
4
5
class BoardingCrew {
    func plunder() {
        print("Plundering")
    }
}

What does a ship need to be able to load and unload cargo? It needs a Hold.

1
2
3
4
5
6
7
8
9
class Hold {
    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

And finally, who is arresting pirates? Let’s call them the NavalOfficers.

1
2
3
4
5
class NavalOfficers {
    func arrestPirates() {
        print("Game over!")
    }
}

And now we have all these tiny components and can compose them to our liking.

Solving the Problem

A PirateShip should be able to sail, shoot, and plunder. So it needs SailingEquipment, a Cannon, and a BoardingCrew. Then we can use the sailing equipment to sail, the cannon to shoot, and the boarding crew to plunder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PirateShip {
    let sailingEquipment: SailingEquipment
    let cannon: Cannon
    let boardingCrew: BoardingCrew

    init(sailingEquipment: SailingEquipment, cannon: Cannon, boardingCrew: BoardingCrew) {
        self.sailingEquipment = sailingEquipment
        self.cannon = cannon
        self.boardingCrew = boardingCrew
    }

    func attack(_ direction: String) {
        sailingEquipment.sail(direction)
        cannon.shoot(direction)
        boardingCrew.plunder()
    }
}

Remember that I said that inheritance is called an is-a relationship? With composition, we have a has-a relationship. A PirateShip has a cannon it can use to shoot. It has a boarding crew that can be sent out to plunder.

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Cannon:::picked
    class SailingEquipment:::picked
    class BoardingCrew:::picked
    class PirateShip
    style PirateShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38

    Cannon <-- PirateShip: has a
    SailingEquipment <-- PirateShip: has a
    BoardingCrew <-- PirateShip: has a

Next, let’s do the same with the MerchantShip. The MerchantShip now has a hold to be able load and unload cargo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MerchantShip {
    let sailingEquipment: SailingEquipment
    let hold: Hold

    init(sailingEquipment: SailingEquipment, hold: Hold) {
        self.sailingEquipment = sailingEquipment
        self.hold = hold
    }

    func exchange(_ oldCargo: String, with newCargo: String) {
        hold.unload(oldCargo)
        hold.load(newCargo)
    }
}

And now the PirateHunterShip.

1
2
3
4
5
6
7
8
9
10
11
class PirateHunterShip {
    let sailingEquipment: SailingEquipment
    let cannon: Cannon
    let navalOfficers: NavalOfficers

    init(sailingEquipment: SailingEquipment, cannon: Cannon, navalOfficers: NavalOfficers) {
        self.sailingEquipment = sailingEquipment
        self.cannon = cannon
        self.navalOfficers = navalOfficers
    }
}

You get the idea.

Surpassing the Limitation

But now let’s bring in our problem case again, the ArmedMerchantShip. Remember, this one needs to be able to sail, shoot, load, and unload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ArmedMerchantShip {
    func sail(_ direction: String) {
        print("Sailing \(direction)")
    }

    func shootCannon(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }

    func load(_ cargo: String) {
        print("Loading \(cargo)")
    }

    func unload(_ cargo: String) {
        print("Unloading \(cargo)")
    }
}

This seemed impossible to solve with inheritance. But with our composition approach, it’s easy! We just need sailing equipment, a cannon, and a hold.

1
2
3
4
5
6
7
8
9
10
11
class ArmedMerchantShip {
    let sailingEquipment: SailingEquipment
    let cannon: Cannon
    let hold: Hold

    init(sailingEquipment: SailingEquipment, cannon: Cannon, hold: Hold) {
        self.sailingEquipment = sailingEquipment
        self.cannon = cannon
        self.hold = hold
    }
}

Problem solved. So composition is much more flexible than inheritance. Every class can simply pick the functionality it needs.

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Cannon:::picked
    class SailingEquipment:::picked
    class BoardingCrew:::picked
    class Hold
    class NavalOfficers
    class PirateShip
    style PirateShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38

    Cannon <-- PirateShip
    SailingEquipment <-- PirateShip
    BoardingCrew <-- PirateShip

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Cannon
    class SailingEquipment:::picked
    class BoardingCrew
    class Hold:::picked
    class NavalOfficers
    class MerchantShip
    style MerchantShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38

    SailingEquipment <-- MerchantShip
    Hold <-- MerchantShip

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Cannon:::picked
    class SailingEquipment:::picked
    class BoardingCrew
    class Hold
    class NavalOfficers:::picked
    class PirateHunterShip
    style PirateHunterShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38

    Cannon <-- PirateHunterShip
    SailingEquipment <-- PirateHunterShip
    NavalOfficers <-- PirateHunterShip

---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class Cannon:::picked
    class SailingEquipment:::picked
    class BoardingCrew
    class Hold:::picked
    class NavalOfficers
    class ArmedMerchantShip
    style ArmedMerchantShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38

    Cannon <-- ArmedMerchantShip
    SailingEquipment <-- ArmedMerchantShip
    Hold <-- ArmedMerchantShip

Going Even Further: Protocols

And here comes the next step. You can make this even more composable by using protocols instead of concrete types. For example, let’s make Cannon a protocol instead of a concrete class.

1
2
3
protocol Cannon {
    func shoot(_ direction: String)
}

Now our ships can use different kinds of cannons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BasicCannon: Cannon {
    func shoot(_ direction: String) {
        print("Shooting cannon to the \(direction). Boom!")
    }
}

class LongRangeCannon: Cannon {
    func shoot(_ direction: String) {
        print("Shooting cannon FAR to the \(direction). Boom!")
    }
}

class LoudCannon: Cannon {
    func shoot(_ direction: String) {
        print("Shooting cannon to the \(direction). BOOOOM!")
    }
}

Let’s create an armed merchant ship with an extra loud cannon to scare away the pirates.

1
2
3
4
5
let loudMerchant = ArmedMerchantShip(
    sailingEquipment: SailingEquipment(), 
    cannon: LoudCannon(), 
    hold: Hold()
)
---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class _Cannon_:::abstraction
    class SailingEquipment:::picked
    class Hold:::picked
    class ArmedMerchantShip
    style ArmedMerchantShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38
    classDef abstraction stroke:#CC7A38,stroke-width:2,stroke-dasharray: 5 5

    _Cannon_ <-- ArmedMerchantShip
    SailingEquipment <-- ArmedMerchantShip
    Hold <-- ArmedMerchantShip
    LoudCannon ..|> _Cannon_

And here is an extra dangerous pirate hunter with a long range cannon.

1
2
3
4
5
let dangerousHunter = PirateHunterShip(
    sailingEquipment: SailingEquipment(), 
    cannon: LongRangeCannon(), 
    navalOfficers: NavalOfficers()
)
---
config:
    class:
        hideEmptyMembersBox: true
---
classDiagram
    class _Cannon_:::abstraction
    class SailingEquipment:::picked
    class NavalOfficers:::picked
    class PirateHunterShip
    style PirateHunterShip fill:#CC7A38,color:white
    classDef picked stroke:#CC7A38
    classDef abstraction stroke:#CC7A38,stroke-width:2,stroke-dasharray: 5 5

    _Cannon_ <-- PirateHunterShip
    SailingEquipment <-- PirateHunterShip
    NavalOfficers <-- PirateHunterShip
    LongRangeCannon ..|> _Cannon_

And so we can compose our objects whichever way we like and create components with different behavior even at runtime. There are a lot more ways of composition, this was just a basic example.

Let’s compare our two candidates in some key aspects.

A Comparison

InheritanceComposition
Forms “is-a” relationshipsForms “has-a” relationships
Tight coupling to superclassLoose coupling when using abstractions like protocols
StaticDynamic
RigidFlexible
Fixed at compile-timeDynamically created at runtime
Hard-codes relationships between classesDynamically creates relationships between objects/instances
Hard to change and maintain (potential cascading changes when changing superclass)Easy to change and maintain
Complex hierarchies can be difficult to reason aboutSmall, single-responsibility components are easier to understand
Harder to testEasy to test small components in isolation
Limited reusabilityEasy to reuse small components

Wrapping It Up

So if your goal is to reuse or share code, composition is generally a better choice than class inheritance.

A better use of inheritance is extending the functionality of existing classes, like inheriting from UIViewController and overriding its lifecycle methods.
Inheritance can be the better solution when an is-a relationship makes sense.

Both composition and inheritance are tools in your programming toolbox, so pick whichever tool best solves the problem at hand. You can also combine both approaches.

There is more to be said on this topic. If you’re interested, I’ll put some recommendations below.

Fair winds and goodbye!


This post is licensed under CC BY 4.0 by the author.