When coming from an Object-Oriented Programming language like ObjC, inheritance is often used to share code between multiple classes. But that solution is not always the best, and have some issues.
In todayâs article, weâll see how Swiftâs Protocol Extensions and their usage as âMixinsâ can change the deal.
TL;DR: You can download the Swift Playground containing all the code of that article here.
The problem with Inheritance
Say you have an app with a lot of UIViewController
classes that share the same behavior, for example they all have a Burger Menu. You donât want to reimplement the Burger Menu logic (setting up the leftBarButtonItem
, opening and closing the menu when the button is tapped, etc) in every View Controllers of your app of course.
Well the solution is easy, youâll just create a CommonViewController
, subclass of UIViewController
that implement all those behaviors, and then make all your ViewControllers inherit from that CommonViewController
instead of inheriting from UIViewController
directly, right? That way, theyâll all inherit those methods and behave the same, no need to reimplement all the things every time.
class CommonViewController: UIViewController {
func setupBurgerMenu() { ⌠}
func onBurgerMenuTapped() { ⌠}
var burgerMenuIsOpen: Bool {
didSet { ⌠}
}
}
class MyViewController: CommonViewController {
func viewDidLoad() {
super.viewDidLoad()
setupBurgerMenu()
}
}
But then later during the development phase, you realize that you need an UITableViewController
or UICollectionViewController
⌠Damn, canât use that CommonViewController
, because it inherits UIViewController
, not UITableViewController
!
What would you do, make a CommonTableViewController
that implement the same things as CommonViewController
but inherit from UITableViewController
instead? Thatâd be a lot of code duplication and quite a bad design.
Composition to the rescue
Of course, the typical and rightful answer is this:
Prefer Composition over Inheritance.
That means that insteed of making use of inheritance, weâll make our UIViewController
contain / be composed of inner classes that provide the behavior.
In our case, we could imagine a BurgerMenuManager
class that provides all the needed methods to setup the burger menu icon and interact with it, and our various UIViewControllers
will then have a property holding a BurgerMenuManager
and can use it to interact with the Burger Menu:
class BurgerMenuManager {
func setupBurgerMenu() { ⌠}
func onBurgerMenuTapped() { burgerMenuIsOpen = !burgerMenuisOpen }
func burgerMenuIsOpen: Bool { didSet { ⌠} }
}
class MyViewController: UIViewController {
var menuManager: BurgerMenuManager()
func viewDidLoad() {
super.viewDidLoad()
menuManager.setupBurgerMenu()
}
}
class MyOtherViewController: UITableViewController {
var menuManager: BurgerMenuManager()
func viewDidLoad() {
super.viewDidLoad()
menuManager.setupBurgerMenu()
}
}
But you can see how that can become cumbersome. And you need to reference the intermediate object menuManager
each time explicitly.
Multiple inheritance
Another problem with inheritance is the fact that a lot of Object-Oriented languages donât allow multiple inheritance (for a good reason, especially because of the diamond problem).
The means that a class canât inherit multiple superclasses.
Letâs say youâre implementing model classes representing sci-fi characters. You will obviously have to represent DocEmmettBrown
, DoctorWho
& TimeLord
, IronMan
, Superman
⌠so how do they relate each others? Some can time-travel, some can space-travel, some can do both, some can fly and some canât, some are humans and some arenâtâŚ
class IronMan
and class Superman
can both fly, so we can imagine a class Flyer
superclass which provides the implementation of the func fly()
method. But IronMan
and DocEmmettBrown
are both human, so we can also imagine a class Human
superclass, while Superman
and TimeLord
will be subclasses of class Alien
. Wait⌠so IronMan
both inherit from Flyer
and from Human
? Thatâs not possible in Swift (and neither in a lot of OOP languages).
Should we choose one over the other? But if we make IronMan
a subclass of Human
then what about the implementation of the func fly()
method? We canât obviously implement it in Human
because not every human can fly, but Superman
will need that method too, and we donât want to duplicate it.
So, we could use composition there, like make class SuperMan
be composed of a var flyingEngine: Flyer
property.
But having to write superman.flyingEngine.fly()
instead of just superman.fly()
is not that pretty.
Mixins & Traits
Thatâs where the concept of Mixins & Traits1 comes into play.
- With Inheritance, you define what your classes are. For example every
Dog
is anAnimal
. - With Traits, you define what classes can do. For example, every
Animal
caneat()
, but Humans can eat too, and Doctor Who can eat fish fingers and custard, even if heâs Gallifreyan and not Human nor Animal.
So with Traits, the important stuff is not so much what they are, but what they can do.
While Inheritance let you describe what an object is, Traits let you describe what an object can do.
And best of all, a class can adopt multiple Traits
, as it can do multiple things, while it can only be one thing (inherit only one superclass).
So how does this apply to Swift?
Protocols with Default Implementation
In Swift 2.0, when you define a protocol
, you can give default implementations to some or all methods of that protocol, using an extension
of that protocol. Hereâs what it looks like:
protocol Flyer {
func fly()
}
extension Flyer {
func fly() {
print("I believe I can flyyyyy âŹ")
}
}
Given that, when you create a class or struct that conforms to that Flyer
protocol, it gets an implementation of the fly()
method for free!
Thatâs still a default implementation so youâre free to redefine that method if you need to, but if you donât youâll still have the default one:
class SuperMan: Flyer {
// we don't implement fly() there so we get the default implementation and hear Clark sing
}
class IronMan: Flyer {
// but we can also give a specific implementation if needs be
func fly() {
thrusters.start()
}
}
That feature of Protocols with Default Implementation is great for many things, one being of course as you guessed to bring the âTraitsâ concept to Swift.
One identity, multiple abilities
The great thing about Traits is that they donât depend on the identity of the object you apply them on. They donât care what the class is and what it inherits from: they just define some functions on that class.
That solves our problem of Doctor Who being both a Time Traveler and an Alien while Dr Emmett Brown being a Time Traveler and a Human. Or IronMan being a Human who can fly while Superman being an Alien who can fly.
What you are does not define what you can do.
So letâs implement our model classes by taking advantage of Traits now.
First, letâs define the various Traits:
protocol Flyer {
func fly()
}
protocol TimeTraveler {
var currentDate: NSDate { get set }
mutating func travelTo(date: NSDate)
}
Then letâs give them some default implementations:
extension Flyer {
func fly() {
print("I believe I can flyyyyy âŹ")
}
}
extension TimeTraveler {
mutating func travelTo(date: NSDate) {
currentDate = date
}
}
At that point, weâll still use inheritance to define the identities of our characters (what they are), so letâs have some parent classes:
class Character {
var name: String
init(name: String) {
self.name = name
}
}
class Human: Character {
var countryOfOrigin: String?
init(name: String, countryOfOrigin: String? = nil) {
self.countryOfOrigin = countryOfOrigin
super.init(name: name)
}
}
class Alien: Character {
let species: String
init(name: String, species: String) {
self.species = species
super.init(name: name)
}
}
And now we can define our characters by both their identity (via inheritance) and abilities (traits / protocol conformance):
class TimeLord: Alien, TimeTraveler {
var currentDate = NSDate()
init() {
super.init(name: "I'm the Doctor", species: "Gallifreyan")
}
}
class DocEmmettBrown: Human, TimeTraveler {
var currentDate = NSDate()
init() {
super.init(name: "Emmett Brown", countryOfOrigin: "USA")
}
}
class Superman: Alien, Flyer {
init() {
super.init(name: "Clark Kent", species: "Kryptonian")
}
}
class IronMan: Human, Flyer {
init() {
super.init(name: "Tony Stark", countryOfOrigin: "USA")
}
}
Now both Superman
and IronMan
use the same implementation of fly()
even if they inherit from a different superclass (Alien
for one, Human
for the other), and both doctors know how to Time Travel even if one is Human and the other is from Gallifrey:
let tony = IronMan()
tony.fly() // prints "I believe I can flyyyyy âŹ"
tony.name // returns "Tony Stark"
let clark = Superman()
clark.fly() // prints "I believe I can flyyyyy âŹ"
clark.species // returns "Kryptonian"
var docBrown = DocEmmettBrown()
docBrown.travelTo(NSDate(timeIntervalSince1970: 499161600))
docBrown.name // "Emmett Brown"
docBrown.countryOfOrigin // "USA"
docBrown.currentDate // Oct 26, 1985, 9:00 AM
var doctorWho = TimeLord()
doctorWho.travelTo(NSDate(timeIntervalSince1970: 1303484520))
doctorWho.species // "Gallifreyan"
doctorWho.currentDate // Apr 22, 2011, 5:02 PM
An adventure in Space and Time
Now letâs introduce a new Space Travel ability/trait:
protocol SpaceTraveler {
func travelTo(location: String)
}
And give it a default implementaton:
extension SpaceTraveler {
func travelTo(location: String) {
print("Let's go to \(location)!")
}
}
We can then use Swiftâs extensions
to add conformance to a protocol to an existing class, so letâs add those abilities to the characters we already have defined. If we omit that time when IronMan went to the portal above New York and briefely flew into space, then only The Doctor and Superman can actually Space Travel:
extension TimeLord: SpaceTraveler {}
extension Superman: SpaceTraveler {}
Yes, thatâs all it takes to add this ability/trait to the existing classes! And just like that, they can now travelTo()
any place! Pretty neat, right?
doctorWho.travelTo("Trenzalore") // prints "Let's go to Trenzalore!"
Letâs invite more people to the party!
Now letâs introduce some more people to the mix:
// Come along, Pond!
let amy = Human(name: "Amelia Pond", countryOfOrigin: "UK")
// Damn, isn't she not a Time and Space Traveler too? Which doesn't make her a TimeLord, though
class Astraunaut: Human, SpaceTraveler {}
let neilArmstrong = Astraunaut(name: "Neil Armstrong", countryOfOrigin: "USA")
let laika = Astraunaut(name: "LaĂŻka", countryOfOrigin: "Russia")
// Wait, LaĂŻka is a Dog, right?
class MilleniumFalconPilot: Human, SpaceTraveler {}
let hanSolo = MilleniumFalconPilot(name: "Han Solo")
let chewbacca = MilleniumFalconPilot(name: "Chewie")
// Wait, isn't MilleniumFalconPilot defined as "Human"?!
class Spock: Alien, SpaceTraveler {
init() {
super.init(name: "Spock", species: "Vulcan")
// Woops not 100% right
}
}
Ok, Huston, we have a problem here. Laika is not a Human, neither is Chewie, and Spock is half Human, half Vulcan, so those definitions are quite wrong.
You see whatâs the problem here? Weâve taken for granted that Human
and Alien
are identities, and weâve been bitten by the inheritance again as some classes were forced to be of some Type / inherit from some parent class, whereas in fact itâs not always the reality, especially in Sci-Fi.
Thatâs also why using Protocols in Swift, and Protocols Default Implementations, can help removing those constraints imposed on your classes by inheritance.
If Human
and Alien
were protocols
instead of classes
, weâd have a lot of benefits:
- We could define a
MilleniumFalconPilot
type without forcing it to be aHuman
and thus allowing Chewie to drive it - We could define LaĂŻka as an
Astronaut
even if sheâs notHuman
- We could define
Spock
as being bothHuman
andAlien
- We could even totally get rid of inheritance in our case, and define our types as
structs
instead ofclasses
. Astruct
doesnât support inheritance, but can still conform to as many protocols as you want.
Protocol everywhere!
So, one solution to this is to make everything a Protocol and get rid of the inheritance completely. After all, we donât care what our characters are, what define heroes is the abilities they have!
Iâve included a Swift Playground that you can download here that contains the code shown in that blog post, and demonstrate in Page 2 of the Playground a solution with everything made a Protocol and Structs, with no inheritance at all. Donât hesitate to take a look!
Of course that doesnât mean that you have to get rid of inheritance at all cost (donât listen too much to that Dalek, they lack feelings after all đ). Inheritance is still useful and can still make sense â e.g. it still feels logical that a UILabel
is a subclass of UIView
. But that gives you a taste of what Mixins & Protocols with Default Implementations can offer.
Conclusion
When practicing Swift, youâll realize that itâs really a Protocols-Oriented language, and that using a Protocol is more common and more powerful in Swift that it was in Objective-C. After all, protocols like Equatable
, CustomStringConvertible
and any protocol in -able
in the Swift Standard Library can actually be seen as Mixins!
With Swift Protocols and Protocols Default Implementations, you can implement Mixins & Traits, but you can also implement something similar to Abstract Classes2 and more, and make your code way more flexible.
The Mixins & Traits approach allows you to describe types by what they can do rather than what they are, and more importantly, to opt-in capabilities into your types. Itâs like doing your shopping and pick the capabilities you want for your type, whatever the class they inherit, if any.
Back at the first example, you can for example create a protocol BurgerMenuManager
with a default implementation, then simply make your View Controllers (whether they are UIViewController
, UITableViewController
or whatnot) conform to that protocol so it will automatically gain those abilities and features from BurgerMenuManager
for free, without worrying about the parent class of the UIViewController
!
Thereâs a lot more to say about Protocol Extensions, and Iâm tempted to continue that article to tell you a lot more about them, as they can improve your code in a lot more ways. But hey, that post is already long enough, and letâs keep some more for future blog posts, hoping to so see you there!
In the meantime, long live and prosper, and Geronimo! đ
-
Iâm not going to enter in the details of the difference between the concept of âMixinâ and âTraitsâ here. Letâs say for the sake of simplicity that these two are similar and those two words can be used interchangeably in the context of this article. ↩
-
That will probably be a topic for a future blog post. ↩