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:
After a while, we realize we like danger and freedom, so we create a Motorbike class too:
Because petrol tanks aren't infinite, we add a .refuel() method to the Car and the Motorbike classes:
But that’s a duplication, so we move .refuel() into a shared Vehicle class.
Cars & motorbikes tend to breakdown over time, so we create a Mechanic Robot to maintain them:
We like to show off our vehicles, a CleanerRobot will help keep them looking factory fresh:
Since .findVehicle() is now duplicated between MechanicRobot and CleanerRobot we create a Robot superclass:
This is what our complete system looks like:
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:
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:
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:
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.
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.
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 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:
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.