The title of this episode is a bit misleading because the Swift programming language has no support for abstract classes. There are a few workarounds, though. In this episode, we take a look at two alternatives to the abstract class pattern in Swift.
Abstract Classes
How It Works
The first workaround attempts to mimic the abstract class pattern. Let's take a look at an example.
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 override 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 are required to override the `sound()` method.")
}
}
If the sound()
method of the Animal
class is invoked, a fatal error is thrown. How does that work and how does that mimic the abstract class pattern? Let's look at another example.
class Animal {
func sound() {
fatalError("Subclasses are required to override the `sound()` method.")
}
}
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? Notice that we removed the sound()
method from the Dog
class. The Dog
subclass doesn't override the sound()
method of its parent class, Animal
. That 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 implements the sound()
method, then that implementation is used since it overrides the implementation of the superclass. That is textbook class inheritance.
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
The first downside is that this solution attempts to mimic the abstract class pattern. Abstract classes are not supported in Swift and this solution is nothing more than an attempt to patch the absence of abstract classes in Swift.
The second, more important, downside is that a missing implementation isn't picked up by the compiler at compile time. Which method is invoked is decided at runtime through dynamic dispatch. That is a major risk and limitation of this solution.
Protocols
How It Works
The good news is that Swift offers a much better alternative to the above solution, protocols. Protocols and protocol-oriented programming (POP) are not new if you are familiar with Objective-C. Let me illustrate how it works by applying protocol-oriented programming to the Animal
, Cat
, and Dog
example. It is time to refactor.
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()
}
The next step is implementing the Cat
and Dog
classes. Both classes conform to the Animal
protocol.
protocol Animal {
func sound()
}
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. That is exactly what we want. The compiler should warn us at compile time if the class don't conform to the protocol.
We can get rid of these errors by implementing the sound()
method in Cat
and Dog
.
protocol Animal {
func sound()
}
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 a type doesn't conform to the protocol. Another advantage is that both reference types and value types can conform to a protocol. This isn't true in Objective-C. Protocols bypass type restrictions. Any type can conform to the Animal
protocol, including structs and enums.
Let's apply that to the example. Cat
and Dog
don't need to be declared as classes. We can declare them as structs instead. Value types are an important concept to understand in Swift and I always default to a value type unless I have a good reason to use a reference type.
protocol Animal {
func sound()
}
struct Cat: Animal {
func sound() {
print("miauw")
}
}
struct Dog: Animal {
func sound() {
print("woof")
}
}
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 this example, we extend the Animal
protocol by adding the maximumAge
computed property and the feed()
method.
protocol Animal {
func sound()
}
extension Animal {
var maximumAge: Int {
20
}
func feed() {
print("eating")
}
}
struct Cat: Animal {
func sound() {
print("miauw")
}
}
struct Dog: Animal {
func sound() {
print("woof")
}
}
With protocol extensions, you can add implementations, such as methods and computed properties, to conforming types without having to make changes to the conforming types.
The methods and computed properties defined in a protocol extension don't need to be implemented by the types that conform to the protocol. This makes protocol extensions great for providing default implementations.
struct Cat: Animal {
func sound() {
print("miauw")
}
}
struct Dog: Animal {
func sound() {
print("woof")
}
var maximumAge: Int {
25
}
}
let cat = Cat()
cat.maximumAge // 20
cat.feed()
let dog = Dog()
dog.maximumAge // 25
dog.feed()
Notice that Dog
implements the maximumAge
computed property while Cat
doesn't.
What's Next?
While protocols are a great alternative to the abstract class pattern, it is important to understand and remember that protocols don't attempt to mimic abstract classes.
In this episode, we briefly touched on protocol extensions. They make protocols and protocol-oriented programming more powerful and versatile. We only scratched the surface, though. There is much more to explore.