Working with Swift: Adopt a Protocol or Pass a Function?

Without fail, any time Rob Napier (@cocoaphony) speaks or writes, I gain insight into new and deeper ways to solve problems with Swift.

In January 2016, he gave a talk at dotSwift, and I wanted to record my thoughts on something he said that made a lot of sense when it comes to the topic of, “Should I create and adopt a protocol for this Type I’m creating, or should I just pass it a function instead?”

Two insights

During the talk, he compared some scenarios that were meant to help determine when to create a protocol, or when doing so would be overly complex, and passing a function might be the simpler thing to do instead.

Two phrases caught my attention:

A protocol is really just a promise to implement some functions, and a struct is mostly just a bundle of functions that implement the promise.

and

I can pass you an object that promises a function, or, I could just pass you the function.

[mind blown]

So in other words, there are times when, rather than going through the formality of…

  • Creating a protocol defining one or more functions that should be implemented
  • Creating a Type that adopts that protocol to promise that “I (as a class/struct/enum) will implement this/these function(s)”
  • Creating an instance of that Type
  • Passing off the instance to another Type that needs to call that promised function

…life might be simpler and code might be more clear and more concise when you just… pass the function instead!

When to pass a function

Rob gave us a couple or three rules of thumb. Not hard-and-fast, “It should always be this way”, but just some guiding thoughts to filter our decision-making about our architecture.

When it comes to finding opportunities that lend themselves to going the “just pass the function” route, consider the following:

  • If you’re creating a Type that depends on a single piece of functionality (a single function), maybe try depending on / passing just the function, rather than creating a protocol.
  • If you’re creating a Type that depends on more than a single function, but the nature of the dependent relationship is short-lived, maybe try depending on / passing just the function. How do you know if it’s short-lived? Ask, “How many times am I going to call the function(s) that I depend on? Once, and then I’m done? Or multiple times throughout the application life-cycle?” If it’s a once and done kind of relationship, much like a callback, then perhaps just depending on and passing the function, rather than creating a protocol, is the simpler route.

When to use a protocol

For some rules of thumb when it comes to choosing a protocol over just passing a function, you might consider:

  • If you’re creating a Type that depends on 3 or more related functions, wrapping those functions up in a protocol might be cleaner and more clear.
  • If you’re creating a Type that depends on some functions for a long period of time, consider a protocol. Long-lived relationships are better-described in a protocol. Think of something like a table view’s data source. This is a good example of when to use a protocol to describe the dependency and the relationship, because as data changes, the table view will need to constantly call into those protocol methods to refresh itself.

How to depend on a function

In order to fully grasp how to go the “just depend on / pass the function” route, you need to have an understanding of how function Types are described in Swift. With this knowledge, you’re set to do a couple of things:

1 – Create a property on the Type you’re implementing that is of some function Type. For example:

 1struct Vehicle<Fuel> {
 2    let move: (Fuel) -> Void
 3}
 4
 5// Fuel Types
 6struct Gas {}
 7struct RocketFuel {}
 8
 9let car = Vehicle<Gas>(move: { _ in print("use gasoline to move") })
10
11let rocket = Vehicle<RocketFuel>(move: { _ in print("use rocket fuel to move") })
12
13car.move(Gas())
14rocket.move(RocketFuel())

A full explanation of indicating function Types can be found by reviewing my guide on Swift Functions as Types

2 – Declare that such-and-such parameter on a function within your Type must be a function Type. For example:

1func getData(completion: (NSData) -> Void) {
2    let data: NSData = // do something to go get data
3
4    // call completion handler when getting data is done
5    completion(data)
6}

The above is an example of a callback scenario, which I give full treatment in Fundamentals of Callbacks for Swift Developers

comments powered by Disqus