Interfaces vs Inheritance in Swift

January 10, 2016

Interfaces vs Inheritance in Swift

I suppose it is tempting, if the only tool you have is a hammer, to treat everything as if it were a nail.

— Abraham Maslow

When creating software, it’s useful to understand a wide range of design principles. Understanding how to design a system with the most appropriate principle can save countless hours of development and headache.

The first principle I’d like to talk about is Interfaces vs Inheritance, also known as Composition Over Inheritance:

What is inheritance?

Inheritance is when a class inherits state and/or behavior from a parent class. Here’s an example in Swift:

class Dog {
    func bark() {
        print("Bark!")
    }
}
 
class GermanShephard: Dog {
}
 
let myDog = GermanShephard()
myDog.bark() // prints "Bark!"

In this example, the GermanShephard class inherits from the Dog class. Because of this, GermanShephard gains all of the properties and methods from Dog, and can call bark().

Inheritance is extremely useful for modeling hierarchies like this one. Still, inheritance is not without its limitations. Take this example:

class Dog {
    func bark() {
        print("Bark!")
    }
}
 
class GermanShephard: Dog {
    func sniffDrugs() {
        if drugs {
            bark()
        }
    }
}
 
class BelgianMalinois: Dog {
    func sniffDrugs() {
        if drugs {
            bark()
        }
    }
}
 
class Poodle: Dog {
}

In this example, we have two breeds of dog that are able to sniff drugs: the German Shephard and the Belgian Malinois. Unfortunately, they both provide implementations for sniffDrugs(), so there’s some code duplication here.

At first glance, it looks like you can reduce the code duplication by moving sniffDrugs() to the parent Dog class. Unfortunately, that would mean the Poodle class inherits sniffDrugs() as well, which doesn’t make sense, as poodles aren’t drug-sniffing dogs (at least for this example).

Another solution might be to create a level directly below Dog that splits the dogs into two families: drug-sniffing dogs and non-drug-sniffing dogs:

class Dog {
    func bark() {
        print("Bark!")
    }
}
 
class DrugSniffingDog: Dog {
    func sniffDrugs() {
        if drugs {
            bark()
        }
    }
}
 
class GermanShephard: DrugSniffingDog {
}
 
class BelgianMalinois: DrugSniffingDog {
}
 
class Poodle: Dog {
}

This fixes the code duplication, but adds a bit of complexity to our model.

Now let’s say you need to represent dogs that can and can’t swim. For the sake of this example, let’s also say that German Shephards can’t swim (despite what this adorable video shows).

To represent this in our system, we may create something like this:

class Dog {
    func bark() {
        print("Bark!")
    }
}
 
class DrugSniffingDog: Dog {
    func sniffDrugs() {
        if drugs {
            bark()
        }
    }
}
 
class GermanShephard: DrugSniffingDog {
}
 
class BelgianMalinois: DrugSniffingDog {
    func swim() {
        print("Splash!")
    }
}
 
class Poodle: Dog {
    func swim() {
        print("Splash!")
    }
}
 

Did you notice the same code duplication we saw above? That’s because both the Belgian Malinois and the Poodle can swim. Unfortunately, we can’t solve this in the same way as above, because BelgianMalinois can’t inherit from both DrugSniffingDog and a new SwimmingDog parent class.

We can, however, model this with protocols in Swift.

How can protocols provide a better abstraction?

A protocol in Swift defines methods or properties that a class can then adopt. Here’s an example:

protocol Barker {
    func bark()
}
 
class GermanShephard: Barker {
    func bark() {
        print("Bark")
    }
}
 
class BelgianMalinois: Barker {
    func bark() {
        print("Bark!")
    }
}

The GermanShephard and BelgianMalinois class both adopt the Barkable protocol, meaning they must both provide implementations for bark().

As of Swift 2.0, we can now remove the code duplication by providing a default implementation for bark() using a protocol extension:

protocol Barker {
    func bark()
}
 
extension Barker {
    func bark() {
        print("Bark!")
    }
}
 
class GermanShephard: Barker {
}
 
class BelgianMalinois: Barker {
}
 
let myDog = GermanShephard()
myDog.bark() // prints “Bark!”

This gives both the GermanShephard and BelgianMalinois the ability to bark(). If we want the GermanShephard to bark differently from the BelgianMalinois, we would just need to override the bark() method in GermanShephard.

And, because classes can adopt multiple protocols, we can model the above barking, drug-sniffing, swimming model like so:

protocol Barker {
    func bark()
}
 
protocol Swimmer {
    func swim()
}
 
protocol DrugSniffer {
    func sniffDrugs()
}
 
extension Barker {
    func bark() {
        print("Bark!")
    }
}
 
extension Swimmer {
    func swim() {
        print("Splash!")
    }
}
 
extension DrugSniffer {
    func sniffDrugs() {
        print("I found drugs!");
    }
}
 
class GermanShephard: Barker, DrugSniffer {
}
 
class BelgianMalinois: Barker, Swimmer, DrugSniffer {
}
 
class Poodle: Barker, Swimmer {
}
 
let myDog = BelgianMalinois()
myDog.bark() // prints "Bark!"
myDog.swim() // prints "Splash!"
myDog.sniffDrugs() // prints "I found drugs!"
 

Now we can easily represent dogs with different abilities without getting stuck in a rigid hierarchy. Not only that, but you can reuse the Swimmer (and maybe DrugSniffer?) protocols for other classes, like Human.

Conclusion

When designing a system, it’s important to pick the right design principle for your model. In many circumstances, it makes sense to prefer composition over inheritance.


Further Reading