The drawbacks of Object Oriented Programming and how to overcome them in Swift

Object oriented programming (OOP) is widely used iOS development and most job descriptions list OOP as a requirement. Swift must support OOP because it is backwards compatiable with Objective-C, however Swift also supports functional and protocol oritented programming. Let's explore the limitations of OOP, and how to solve them in Swift using POP.

In this example we're designing a game, and we need a Car:

class Car {
func drive()
}
view raw CarObject.swift hosted with ❤ by GitHub

After a while, we realize we like danger and freedom, so we create a Motorbike class too:

class Motorbike {
func ride()
}

Because petrol tanks aren't infinite, we add a .refuel() method to the Car and the Motorbike classes:

class Car {
func refuel()
func drive()
}
class Motorbike {
func refuel()
func ride()
}
view raw Refuel1.swift hosted with ❤ by GitHub

But that’s a duplication, so we move .refuel() into a shared Vehicle class.

class Vehicle {
func refuel()
}
class Car: Vehicle {
func drive()
}
class Motorbike: Vehicle {
func ride()
}
view raw Refuel2.swift hosted with ❤ by GitHub

Cars & motorbikes tend to breakdown over time, so we create a Mechanic Robot to maintain them:

class MechanicRobot {
func findVehicle()
func maintainVehicle()
}

We like to show off our vehicles, a CleanerRobot will help keep them looking factory fresh:

class CleanerRobot {
func findVehicle()
func cleanVehicle()
}

Since .findVehicle() is now duplicated between MechanicRobot and CleanerRobot we create a Robot superclass:

class Robot {
func findVehicle()
}
class Mechanic: Robot {
func maintainVehicle()
}
class Cleaner: Robot {
func cleanVehicle()
}
view raw Robot.swift hosted with ❤ by GitHub

This is what our complete system looks like:

class Vehicle {
func refuel()
}
class Car: Vehicle {
func drive()
}
class Motorbike: Vehicle {
func ride()
}
class Robot {
func findVehicle()
}
class Mechanic: Robot {
func maintainVehicle()
}
class Cleaner: Robot {
func cleanVehicle()
}

A couple of months of development go by, and your garage has become a mature, stable system, with a couple of Lambos and Ducatis driving around.

It's usually at this point, when everything is looking good, that the project manager will sit you down and say:

“Our customers want to be more hands-on with the car-washing process, they want to add a CleanerCar, which they can .drive() and .refuel() themselves and use to .cleanVehicle(), but it should not be able to .findVehicle() by itself."

And now, we’re screwed. We simply cannot fit the CleanerCar nicely into our inheritance hierarchy!

We could create a new parent object, where you put all the functionality that is shared:

class GlobalObject {
func cleanVehicle()
}
class Vehicle: GlobalObject {
func refuel()
}
class Car: Vehicle {
func drive()
}
class CleanerCar: Car {}
class Motorbike: Vehicle {
func ride()
}
class Robot: GlobalObject {
func findVehicle()
}
class Mechanic: Robot {
func maintainVehicle()
}
class Cleaner: Robot {}

This gives us the CleanerCar we've been asked for, which can .drive(), .refuel() and .cleanVehicle(), but not .findVehicle(). However, it also means our other objects will have a ton of functionality they don't use. E.g. our Motorbike and Mechanic can now .cleanVehicle()!

This strategy results in the classic Gorilla/Banana problem: You request a banana, but you end up with a gorilla holding the banana and the entire jungle with it too.

The other suboptimal solution is to duplicate functionality:

class Vehicle {
func refuel()
}
class Car: Vehicle {
func drive()
}
class CleanerCar: Car {
func cleanVehicle() //DUPLICATE
}
class Motorbike: Vehicle {
func ride()
}
class Robot {
func findVehicle()
}
class Mechanic: Robot {
func maintainVehicle()
}
class Cleaner: Robot {
func cleanVehicle() //DUPLICATE
}

This isn't as horrid as our previous attempt, but it still introduces duplicate code which brings additional complications such as having to maintain the same code in two different locations.

As good developers we practice DRY (don't repeat yourself). If the same code is being copy-pasted to a few different locations, that's a sure-sign that something's wrong.

So what can we do that doesn't result in objects with redundant functionality or in code duplication?

Protocol Oriented Programming

If OOP is when you design your types around what they are, then our object above were Vehicles, Cars, Motorbikes, Robots etc.

If we turn this around and ask what our objects can do, then we might have a Cleaner, Maintainer, VehicleFinder, Drivable, Rideable, Refuelable. Which, abstractly could look like this:

Car = Refuelable + Drivable;
Motorbike = Refuelable + Rideable;
CleanerCar = Refuelable + Driveable + Cleaner;
MechanicRobot = VehicleFinder + Maintainer;
CleanerRobot = VehicleFinder + Cleaner;

Here's what the protocols might look like. Each once is highly specific and lightweight. Unlike inheritable classes which often end up bloated. The sole reason this is possible is because multiple protocols can be stacked, progressively adding functionality to a type.

protocol Refuelable {
func refuel()
}
protocol Drivable {
func drive()
}
protocol Rideable {
func ride()
}
protocol Cleaner {
func clean()
}
protocol Maintainer {
func maintain()
}
protocol VehicleFinder {
func findVehicle()
}

Now let's combine our protocols into useful structs that can do multiple things. The beauty of protocols is they they allows us to compose a Type from one or more instances, stacking together to provide the desired functionality. Whereas Objects are limited to inheriting their functionality from just one base class. Favouring composition over inheritance is an often-stated principle of programming, such as in the influential book Design Patterns.

struct Car: Refuelable, Drivable {}
struct Motorbike: Refuelable, Rideable {}
struct CleanerCar: Refuelable, Drivable, Cleaner {}
struct MechanicRobot: VehicleFinder, Maintainer {}
struct CleanerRobot: VehicleFinder, Cleaner {}
view raw Structs.swift hosted with ❤ by GitHub

Great! Now we have types with all the expected functionality, no duplicate code and no extra, unneeded functionality!

However, if we try to run the code above, Xcode will throw an error - Type 'Car' does not conform to protocol 'Refuelable' this is because protocols let you describe what methods something should have, but don’t provide the code inside. So we would have to code the functions for all the types individually, unless we use Protocol Extensions.

Protocol Extensions

Protocol Extensions allow us to provide default code inside our methods.

We could declare the method .drive() separately inside every struct which conforms to the Drivable protocol. However this may lead to duplicate code if the actions we want to perform are equivalent.

In this case we would implement a Protocol Extension which lets us provides a default declaration. Every struct which conforms to Drivable will fire the method contained within our extension, upon calling .drive(), by default.

If we want any struct to do perform a different action to the default, this can be accomplished by defining the .drive() method within the struct itself. This is the same as using the override keyword within an inherited class type.

In the snippet below both type Car and CustomCar conform to Drivable and Refuelable. However, only CustomCar actually declares any methods itself.

Because of our Protocol Extensions both types will fire the exact same .refuel() method, Car will fire the default .drive() method and CustomCar will fire it's own custom .drive() method:

protocol Refuelable {
func refuel()
}
protocol Drivable {
func drive()
}
struct Car: Refuelable, Drivable {}
//If we just include everything above this comment, the code will not compile.
//We need the default implementations below:
extension Refuelable {
func refuel() {
//Default refuel code here
}
}
extension Drivable {
func drive() {
//Default drive code
print("Default drive speed is 30mph")
}
}
struct CustomCar: Refuelable, Drivable {
func drive() { //Declaring this function within a struct that conforms to "Drivable" is equivalent to "override" for an inherited class type
print("The CustomCar drives at 100mph")
}
}
let car = Car()
car.drive() // Prints "Default drive speed is 30mph"
let customCar = CustomCar()
customCar.drive() // Prints "The CustomCar drives at 100mph

Conclusion

The issue with inheritance in OOP is that we're asked to predict the future with the knowledge we have now. As we all know, good coding practice is to make things as flexible, modular and expandable as possible. Defining a rigid, fixed hierarchy right at the start of our project inevitably leads to a situation down the line where we've backed ourselves into a corner requiring a lot of inelegant spaghetti code to get out of it.

Using protocols, & composition, on the other hand, is more flexible, powerful, and it’s very easy to do.

Want more? Here's a fantastic video from WWDC about using POP over OOP.