The title of this tutorial is a bit misleading because the Swift programming language doesn't support abstract classes. Fortunately, there are workarounds. In this tutorial, we take a look at two alternatives to the abstract class pattern.
Abstract Classes
How It Works
The first workaround attempts to mimic the abstract class pattern. An example best illustrates this solution.
class Animal {
func sound() {}
}
class Cat: Animal {
override func sound() {
print("miauw")
}
}
class Dog: Animal {
override func sound() {
print("woof")
}
}
This is a good start. The override
keyword is required since we're overriding a method implemented by the superclass. The Animal
class isn't abstract, though. How do we force Cat
and Dog
to implement the sound()
method. This is a possible solution.
class Animal {
func sound() {
fatalError("Subclasses need to implement the `sound()` method.")
}
}
If the sound()
method of the Animal
class is invoked, a fatal error is thrown. How does this work? And how does that mimic the abstract class pattern? Let's look at another example.
class Cat: Animal {
override func sound() {
print("miauw")
}
}
class Dog: Animal {
}
let kitty = Cat()
kitty.sound()
let bobby = Dog()
bobby.sound()
Swift uses dynamic dispatch to determine which sound()
method to invoke. What does that mean? In the updated implementation of the Dog
class, the sound()
method is remove. The subclass does not override the sound()
method of its parent class, Animal
. This is a red flag for the abstract class pattern.
If we instantiate an instance of the Cat
class and invoke the sound()
method on kitty
, the runtime inspects the class hierarchy to determine which sound()
method to invoke. If the Cat
class has an implementation of the sound()
method, then that implementation is used since it overrides the implementation of the superclass.
The same applies to the Dog
class. The difference is that the Dog
class doesn't implement the sound()
method. This means the sound()
method of the parent class is invoked instead. The result is a fatal error.
Downsides
An important downside is that this solution attempts to mimic the abstract class pattern. Abstract classes aren't supported in Swift and this solution is nothing more than an attempt to patch the absence of abstract classes in Swift.
A more important downside is that a missing implementation isn't detected at compile time. Which method is invoked is decided at runtime through dynamic dispatch. This is a major limitation of this solution.
Protocols
How It Works
Fortunately, Swift offers a much better alternative to the above solution, protocols. Protocols are nothing new if you're familiar with Objective-C. To illustrate this solution, I'd like to refactor the Animal
, Cat
, and Dog
example with protocols.
We start by defining a protocol, Animal
. The protocol defines a method, sound()
. Remember that every method and property of a Swift protocol is required by default.
protocol Animal {
func sound()
}
In the next step, we create the Cat
and Dog
classes. Both classes conform to the Animal
protocol.
class Cat: Animal {
}
class Dog: Animal {
}
If we don't implement the sound()
method in the Cat
and Dog
classes, the compiler throws an error at compile time.
We can get rid of these errors by implementing the sound()
method in Cat
and Dog
.
class Cat: Animal {
func sound() {
print("miauw")
}
}
class Dog: Animal {
func sound() {
print("woof")
}
}
Advantages
The advantage of protocols is that the compiler notifies us at compile time if the protocol hasn't been implemented correctly. Another advantage is that both classes and structures can conform to a protocol. This isn't true in Objective-C. Protocols bypass type restrictions. Any type can conform to the Animal
protocol.
Protocol Extensions
In Swift 2, Apple introduced protocol extensions. They make protocols more powerful and versatile. With protocol extensions, developers can extend the functionality of a protocol by adding methods and computed properties. In the next example, we extend the Animal
protocol by adding the maximumAge
computed property and the feed()
method.
extension Animal {
var maximumAge: Int {
return 20
}
func feed() {
print("eating")
}
}
With protocol extensions, developers can define behavior on protocols instead of on the types that conform to the protocol.
The functions and computed properties defined by a protocol extension do not need to be implemented by the types that conform to the protocol. This makes protocol extensions great for providing default implementations.
class Cat: Animal {
func sound() {
print("miauw")
}
}
class Dog: Animal {
func sound() {
print("woof")
}
var maximumAge: Int {
return 25
}
}
let cat = Cat()
cat.maximumAge // 20
cat.feed()
let dog = Dog()
dog.maximumAge // 25
dog.feed()
What's Next?
While protocols are a great alternative to the abstract class pattern, it's important to remember that protocols don't attempt to mimic abstract classes. Protocol extensions are much more powerful than what we covered in this tutorial. We only scratched the surface. Questions? Leave them in the comments below or reach out to me on Twitter.