Handler-based Development
Lately I’ve been writing a Swift module for a project at work. I decided to branch out a little bit by utilizing a different design philosophy than I’ve used in the past. While Apple sits around and pretends that protocol based development is actually something new and exciting, I’ve been much more interested in the functional abilities that Swift seems to endow us with. To that end, I decided to escew protocols and instead define the dependencies in my objects using handlers1 instead. This means instead of defining a protocol and requiring an object that conforms to this protocol, I simply require the specific behavior itself. An example of a simple handler might be:
typealias GetUserForID = (userID: String) -> User
A protocol, instead, might look something like this:
protocol UserRetrieval {
func getUserFor(id: String) -> User
}
At first glance these really look similar, except perhaps the the protocol is a little more verbose, which might not actually be a bad thing. I’ve noticed a trend with Swift devs (myself included) to be overly concise with their code which disturbs me because in the end it simply creates short code that’s impossible to read.
One important distinction here is that you’re not passing in objects into the class, but actual functions/closures instead. You can end up with some long init’s when there’s a lot of dependency instead of passing in a single object that conforms to the protocol. For many, this will be a big turn off. However, it has some pretty neat benefits. Here’s some things I noticed:
- I really like writing classes where I define the behavior (as defined by handlers) I need to depend on rather than the objects I need to use, even if those objects were to be defined by protocols. The dependency to behavior makes much more sense to me than dependency on objects.
- Using handlers gives you much more flexibility than protocols, unless you create a separate protocol for each separate behavior you want but that’s even more messy than using handlers. When using a protocol, you must pass in an object that conforms to all the protocol’s methods, whereas the handlers can be “handled” by any object or any combinations of objects or no object at all (a closure).
- Protocols force a dependency graph of objects. Handlers force a dependency graph of behavior.
- When using protocols, you have to consider and predict what the dependency object graph is like in order to choose how you need to reference the objects for memory. Should you be holding a strong or weak reference? What if there is an interdependency and the object passed also depends on you? When using handlers this is not even a consideration. It’s the job of the constructor to worry about memory management and that can change for different constructors. This is not possible using protocols… or at the very least would be insanely clunky.
- Protocols don’t allow you to mix and match behavior, and I’ve found the ability to do that very useful. I have several different objects that need to rely on with different sets of behavioral dependencies. If I create a protocol for each one, I will end up with an object that has to conform to a bunch of protocols, many of which will have overlapping behavior. Personally, I think that’s even more confusing than creating the handlers. I could create a single protocol that defines all the behavior, but that will expose all behavior to any objects that use it, which “muddies the water”. If you see an object has a reference to
TLRequestCoordinator
protocol, you know it can access any of the functions it has, even those that have nothing to do with the object you’re trying to create. However a handler,typealias GetUserFromServerByID = (userID: String, cb: User -> Void) -> Void
, exposes only the dependencies you need. MaybeTLRequestCoordinator
handles that, maybe not, but it doesn’t matter. What matters it that you need to get a user asynchronously from another object and you don’t actually care who does it. - This is weird, but surprisingly effective: Defining too many handlers is a pain because they all need to be passed into the
init
. Defining a protocol with too many functions is surprisingly easy. I’ve found that using handlers has a side effect of causing you to create smaller classes that work together rather than a large class with a lot of functionality. - Using handlers is a god-send for unit testing. There’s absolutely no need to create mock objects at all. You don’t even need to mock the protocols. You simply assign the behavior. You also can much more easily change that behavior as you test, rather than rely on a static implementation of a mock.
- I’m discovering that this is really bad:
typealias GetUser = String -> User
. Yes, it is concise but it’s really easy to forget what exactly it’s supposed to mean. This is better:typealias GetUserObjectByID = (userID: String) -> User
and gets better if you comment it. Unfortunately, I opted for concise in the module and I’ve been thinking about going back and refactoring my handler signatures. Once I had finished creating all my objects I need to write a constructor to put them together. It hit home for me when I kept forgetting what I actually intended each handler to do and I had to go look through my own implementation to figure it out. That’s bad. So documenting and adding parameters to the handler definitions is really important. - Having a constructor is now a thing. It’s not just your
init
functions chained together anymore. It actually takes thought to put together the object graph and it’s explicit, instead of being a by product of the object definitions themselves. - Interdependency must be dealt with.
ObjectA
defines a handler thatObjectB
satisfies. ButObjectB
also defined a handler thatObjectA
satisfies. Well damn, you can’t create either object because they must have each other’s handlers passed in their respectiveinit
. I haven’t figure out the best approach to handling this “chicken & egg” problem yet. I ended up creating a separate class that references both objects and itself has functions that satisfy both objects, which really just pass the call along. It passes it’s own functions as the handlers. I may try using closures to handle this in the future, but I like that using a separate class is very explicit about the dependency behavior, even if it is a little indirect. - A lot of objects use the same handler signature. I noticed this and decided to place all my handler definitions in a single file called
Handlers.swift
. I’m still not sure if that’s a good idea. I’m considering moving them back into their respective files (where the dependency actually is). Even if there’s code duplication it will be easier to read. What I don’t know is if Swift is strict on the parameter names. For instance, is it possible to pass in the same handler for(userID: String) -> User
and(id: String) -> User
? I need to test this, because if I cannot it will cause problems when if I define two handlers in two different files slightly differently that can be handled the same.
So far, I’m really liking this approach but for me the verdict is still out. I’ve used handlers plenty in the past (with blocks in objc) but I’ve never actually tried a design pattern based around it. I still have yet to see if there are any unforeseen landminds I’ve yet to trigger, but so far it’s working out well.
-
Or callbacks, functors, whatever… I’m going to stick with the term “Handler” because it seems to implicate the dependency, like I’m depending on someone else to “handle” the dependent behavior I need. If you want, feel free to copy this and do a search & replace for callback or functor or another term you feel more comfortable with. ↩