Protocol Oriented Programming: A Swift Solution to OOP Headaches
Why is OOP so Popular?
Object Oriented Programming (OOP) was the classic strategy to write modular code for decades, and with good reason.
- Reusability: Well-written objects are stand-alone entities that can be plugged into other programs. If I implement an optimized tree, I can use it in any part of an existing project, or in a brand new one, without writing more code.
- Skip the details: Another programmer doesn’t need to know your object’s implementation. If I see a “Sort” function, I can use without worrying what the code looks like.
- Error-Proofing: We can hide access to functions or variables in a class, which prevents other developers, that are less familiar with our code, from create bugs. Others see only what they need to see.
- Make Big Projects Manageable: Big projects get complicated. OOP lets us break a large problem into sub problems — each of which are handled by different objects. If Nabil, Julia, and Alex are making a space invaders game. Nabil can build an object to represent Aliens, Julia can build a GameBoard object that keeps track of my score, number of lives, and when the game starts/finishes, and Alex can make objects representing my tank and its missiles. In end, we compose these objects to build a functioning game.
- Updating Code: With modular code we can write easily add to, update, or remove specific components as time goes on. e.g. In a building made of bricks, you can swap out a cracked brick with a new one instead of replacing entire sections of the building.
Out With the Old
Though very handy, OOP comes with its fair share of issues:
- “GOD” objects, Massive View Controller: To maintain modularity programmers use the one class, one purpose principal. However bloated viewControllers and models are common by-products of popular swift design patterns (MVC, MVVM). Passing around chunky objects slows your code, and makes modularity difficult as you build out. If an object holds too much of the functionality you need, new code you write will end up depending on it too.
- Multiple Ownership: Lets suppose you and a fellow engineer (Nick), are working on separate ViewControllers that reference the same BankAccount object. If you add credit in firstViewController while Nick removes credit in secondViewController, both of you will end up with an unexpected result. Classes are passed by reference, so changing the values in one area of your project will cause changes to that object’s values in every other place. This makes it difficult to test the isolated effect of your functions on a given object.
- The Subclassing Problem: A Problem so big is deserves its own section
The Subclassing Problem and Why You Can’t Have the Cheeseburger you Want
Your stomach grumbles, so you decide to check out the old “OOP burgers” joint, where they’re famous for their cheeseburger. The cheeseburger comes with Pickles, Tomatoes, Onions, and Mushrooms. But you’re allergic to Mushrooms! So you ask the waiter to substitute the mushrooms for Kale — you LOVE Kale. The waiter shakes his head and says it’s impossible. He tells you the only way you can have Kale is to get a CheeseBurger with all its standard ingredients, with the kale will get placed on top of everything else…Bummer.
OOP driven inheritance often gives subclasses more baggage than we desire. Lets first take a look at how inheritance can be used effectively, then take a look at how this can lead us into problems. Finally I’ll introduce you to Protocol Oriented Programming: A “Swifty” way to achieve everything OOP offers, while skipping many of its issues.
Here’s an example of a typical inheritance pattern:
This is effective use of inheritance because a
SportsCar will utilize everything in
Vehicle, plus some extra features that should only be in sports vehicles. But hey, we’re in the age the electric vehicle, so lets create a subclass for that too!
This is an example of poor inheritance.
We get extraneous features,
gasTank that are irrelevant in an
ElectricVehicle class because
- Electric vehicles don’t use these components by design. Having them in an electric vehicle class is unintuitive.
- For a developer using your electric vehicle object, it’s really confusing to see functions that modify obsolete features — Why are they here? Its unnecessary bloating.
Now lets say I want to add a
superCharging feature to
ElectricVehicle. This feature will get inherited (along with
gasTank), by every class that subclasses
ElectricVehicle. But not everything that needs inheritance from
superCharging! Suppose 8/10 electric vehicles use supercharging — our choices are to either implement supercharging in 8 subclasses of
ElectricVehicle or bloat two subclasses with code for supercharging that will not get used.
This problem only gets worse as we continue down subclass down the chain.
In bigger projects, we get massive inheritance structures that go several layers deep:
These structures are prone to causing architectural headaches. For example, What if we wanted to add a penguin class to the following structure?
Penguins don’t fly. So we’d have to create a two new classes “Flying Birds” and “Flightless Birds” and reorganize all of our birds under these classes. Wait, what about birds that can swim? The headache goes on…
How do we prevent the subclassing problem without knowing all the classes we are going to add ahead of time?
Enter Protocol Oriented Programming
Swift is a language designed to give power to value types. The standard library is designed with just 4 classes, and 95 instances of structs and enums.
Protocol Oriented Programming (POP) is an architecture encourages developers to leverage the functionality packed into value types: Use structs, enums, and protocols in place of additional classes.
Why is this helpful?
Lets go back to our Vehicle problem.
We have features that need to be shared between electric and gas vehicles (such as the ability to
park), and others that belong exclusively to one group (filling gas, charging batteries).
Lets build protocols for each of these feature sets!
Now we can put functions exclusively where needed, while getting the added flexibility of conforming very disparate classes to the same protocol. Before the
fillable() functionality was confined to the
Vehicle class and classes that inherited from it. But if I wanted to create an unrelated type now, let’s say a
Bucket struct, I can conform that to the fillable protocol as well!
Another bonus is that we can use
Structs in place of classes. Previously we used classes in order to take advantage of inheritance, but that is no longer necessary because we get the desired functionality through protocols.
So we’ve solved one problem, but it appears we’ve created another. When conforming to a protocol in swift, each function from that protocol must be implemented to satisfy the compiler. How do we get around this without repeating code?
Protocol Extensions to the Rescue!
Using a protocol extension, I can indicate a class or set of classes for which functions of this protocol will have a default implementation. Now in any
ElectricVehicle class, or any of its subclasses, I can call
recharge() without implementing it again. However, if I want to change how
recharge() behaves in one of my new classes, I can
override this implementation inside the file of the new object.
Alternatively I can have multiple structs adhere to the protocol extension:
Or even create a new protocol that composes several others:
Protocol Oriented Programming and UIKit
Because swift relies heavily on its interactions with UIKit, which is built entirely with classes, we still need to deal with Objects when using POP.
To learn more about this I highly suggest you check out Andrew Mastuchak’s talk on using value oriented programming in swift, where he discusses the benefits of having thick logic layer composed of value types, that interacts with thin action (User Interface) layer composed of classes.
To learn more about using POP to vastly improve your interactions with UIKit, I highly suggest you check out Natasha The Robot’s Realm talk on practical POP.
TLDR; The Benefits of Using POP
- Abstracting code into Protocols creates reusable code that you can add and remove without worrying about restructuring your inheritance
- Single Ownership: Structs are passed by value. The copy of values that you manipulate in a struct will not be affected by activity happening elsewhere in you code. Headaches with threading (Multiple Read/Write problems) and multiple ownership become a thing of the past
- Completely unrelated types can share overlapping functionality
- Clear labels. Looking at a class declaration and the protocols it conforms to, I can quickly gather what a particular class/struct can accomplish.
- Inheritance is Bad: Code Reuse Part 1: http://blogs.perl.org/users/sid_burn/2014/03/inheritance-is-bad-code-reuse-part-1.html
- If You’re Subclassing, You’re Doing it Wrong: https://krakendev.io/blog/subclassing-can-suck-and-heres-why
- Practical POP: https://realm.io/news/appbuilders-natasha-muraschev-practical-protocol-oriented-programming/
- WWDC — Protocol Oriented Programming in Swift: https://www.youtube.com/watch?v=g2LwFZatfTI