How can protocols and extensions help structure code?

This blog explains how you can use protocols to structure your code when multiple objects have the same set of functions.
A protocol (referred to as an interface in some other languages) is a set of requirements that any object must fulfil if it wants to conform to that protocol. Typically, this is a set of functions, but can also include property definitions.
When structuring your code, there are times when you want multiple objects to have the same set of functions. This might be because you're swapping between different storage mechanisms, or it might be because you want a live implementation for your app and a dummy implementation suitable for unit tests. Whatever the reason, this is where protocols are useful.
The iOS platform makes significant use of protocols throughout, but to help explain how they work, let’s take this simple example:
struct Record { let id: Int // some data record } protocol DataManager { func get(id: Int) -> Record? func getAll() -> [Record] } struct SqliteDataManager: DataManager { func get(id: Int) -> Record? { // Retrieve from database } func getAll() -> [Record] { // Retrieve from database } } struct InMemoryDataManager: DataManager { var data = [Record]() func get(id: Int) -> Record? { return data.first { $0.id == id } } func getAll() -> [Record] { return data } }
First, we’ve created a simple struct that will model a record in our database. Then we’ve defined our DataManager protocol with two functions, allowing us to retrieve either a single record by ID, or to load all records. On its own this doesn’t do anything; it’s simply a contract that needs to be adhered to by any object that wants to be a “Data Manager”. We’ve then created two implementations of this protocol, one that requires a lot more work to actually be usable, probably using an existing SQLite library to help with the actual database integration, and the other that is a good starting point for something that’s suitable for using in unit tests, where everything is stored in memory and each instance of InMemoryDataManager will be entirely independent.
Naming a protocol
As with most things in software development, naming is a key consideration. As you can see with the example above, I like to name the protocol with a clean name that describes the behaviour without exposing any implementation details. Using this approach means that, if you want to migrate from SQLite to another database technology, you can simply create a new implementation of the protocol, and the rest of your code can continue to reference DataManager.
Is there an cleaner way to implement a protocol?
Another feature of Swift that works well with protocols is extensions. An extension is simply a block of code that extends another class. They can be used as a simple way of organising your code, or they can be used to add behavior to a class that doesn't originate from your code.
When implementing a protocol, I generally like to put it in an extension. This makes it easier to see which functions relate to the protocol and makes the code easier to read.
Adapting the example above, instead of defining SqliteManager and its conformance to DataManager in one line, we can split it up:
struct SqliteDataManager { // other implementation details } extension SqliteDataManager: DataManager { func get(id: Int) -> Record? { // Retrieve from database } func getAll() -> [Record] { // Retrieve from database } }
It’s harder to see the benefit in such a simple example, and there may be times when it actually makes more sense not to do it this way. A better example may be adding a UIKit conformance to a view controller. I personally like to use the main class definition for the main life cycle methods and move everything else into extensions, which gives a coherent grouping to the functions within your class:
class MyViewController: UIViewController { override func viewDidLoad() { // TODO } override func viewWillAppear(_ animated: Bool) { // TODO } } extension MyViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // TODO } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { // TODO } }
Extending other classes
One final example of using both protocols and extensions, which also demonstrates extending a system class. Let’s take UserDefaults. The behaviour available through this class isn’t defined in a protocol, so let’s create our own protocol to match the signature of UserDefaults:
protocol LocalStorage { func object(forKey defaultName: String) -> Any? func set(_ value: Any?, forKey defaultName: String) func removeObject(forKey defaultName: String) func string(forKey defaultName: String) -> String? func array(forKey defaultName: String) -> [Any]? func dictionary(forKey defaultName: String) -> [String: Any]? func integer(forKey defaultName: String) -> Int func float(forKey defaultName: String) -> Float func double(forKey defaultName: String) -> Double func bool(forKey defaultName: String) -> Bool func set(_ value: Int, forKey defaultName: String) func set(_ value: Float, forKey defaultName: String) func set(_ value: Double, forKey defaultName: String) func set(_ value: Bool, forKey defaultName: String) }
With this in place, we can now extend UserDefaults to make it conform to LocalStorage with very little code:
extension UserDefaults: LocalStorage {}
No implementation code is needed because UserDefaults already has implementations of these functions. Now, instead of referring directly to UserDefaults in our code, we can inject an instance of LocalStorage instead. With this change, an instance of UserDefaults can be passed in for the live code, but an in-memory version can be created, the same as we did with InMemoryDataManager above, for unit tests.
In summary
Protocols are an extremely powerful feature of Swift. When used incorrectly they can cause your code to be overly complicated and confusing, but when used correctly they can give a structure and meaning to your code. Hopefully this post gives you confidence to start using them in your projects.
Looking for something else?
Search over 400 blog posts from our team
Want to hear more?
Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!