Protocol Oriented Programming in the Real World
This weekend, I rewrote most of Locksmith, my library for using the iOS keychain, to be protocol-oriented. This was a fascinating process, and one that isnât yet finished.
Why?
The primary reason I tried out a protocol-oriented approach was to manage complexity in a type-safe way.
The big problem with the iOS keychain is the sheer complexity of it. There are five types of keychain items: generic passwords, internet passwords, certificates, keys, and identities. For each of these, you have four operations: create, read, update, and delete. Thatâs 20 operations, all with different attributes that need to be set and things that can go wrong.
The way I thought about it, we have customization on two axes: types of items, and operations on items.
To accomplish this, the actual Cocoa implementation uses a bunch of string constants and difficult to remember key/value combinations. This works, but itâs not user-friendly. You donât know whatâs gone wrong and why, and you never know the expected type of something without diving for the docs.
With Locksmith, I wanted to have the same power as the Cocoa implementation, but in a way that made use of the type system and felt Swift-native.
What do you get out of protocols?
A couple of awesome things emerge when you focus on protocols
- you can add functionality in new dimensions to existing types
- you can easily adapt a rapidly growing API
- you can decouple certain parts of your API for flexibility and testing
Dimensions
Say we have a Twitter account, which is a struct. And letâs say that we want to be able to save the username and password for that struct to the keychain.
In the past, we mightâve added a method to this struct to save it to the keychain.
This is okay, but thereâs a lot we donât know about a given TwitterAccount
from its declaration. Plus, weâre a bit limited by what that library provides to us.
But with Locksmithâs new protocol oriented design, we get some nice functionality for free.
And with the method we get from Createable
, we can save our item
Oh, and guess what⦠if we conform to Readable
and Deleteable
on that struct (which have no additional requirements), then we can use readFromKeychain()
and deleteFromKeychain
without adding any other code!
Thatâs really awesome. It almost feels like using mixins or stylesheets in CSS.
Consider this as well: in the first instance, we conformed to two protocols. One for Createable
, which works for anything that can provide data
, and one for GenericPassword
, which works for anything that belongs to a service
and has an account
.
Two axes of customization.
Imagine a 4Ã5 chessboard, where for each square on the bottom you have one of our actions: create, read, update, and delete. On the vertical, youâve got a square for each type of item: generic password, internet password, certificate, key, and identity.
20 different permutations of closely-related stuff is tough to model. But with protocols and protocol extensions, it becomes so much easier. With protocol extensions, protocols add functionality in new dimensions.
Thatâs cool, but itâs probably not clear just how cool that really is. Let me tell you one more thing.
Weâve seen that generic passwords have a service
and an account
. Those are required. What I didnât show you was that generic passwords can also have creationDate
, modificationDate
, description
, comment
, creator
, label
, type
, isInvisible
, and isNegative
. (And some of them donât have the type youâd expect!)
Letâs extend our TwitterAccount
to use some of these.
Too easy! We just added the properties onto our type.
But wait⦠if we wanted to use the old static func approach, how would we do that?
You might create methods that have optional or default arguments, but that explodes with complexity really quickly.
Or you could pass around a dictionary and check against some agreed-upon keys, but then you donât get the type system working for you. Youâd never know what was required and what was optional, and itâs impossible to know for sure at compile time.
Protocol oriented programming is the best approach I can think of to deal with this complexity and configurability.
When I was first implementing this, I realised we werenât actually returning anything useful from our readFromKeychain()
. We were giving people a [String: AnyObject]
, which is barely a step up from where we started!
We needed a nice type for returning data, but we also had to communicate to users that the metadata returned from a GenericPassword
operation wouldnât be the same as that returned from an InternetPassword
operationâand they had to be able to know that at compile time.
Sounds like a huge pain.
Having protocols all the way down made it super easy to mix together the right type to return to usersâwe developed a ResultType
, and created a couple of other protocols (GenericPasswordResultType
and InternetPasswordResultType
) that shared certain properties, but also provided their own unique properties.
Plus, having this ResultType
meant that we could provide actual types for the metadata we get back from the keychainâif the user saves an NSDate
as metadata, theyâre going to get an NSDate
back. Wonderfulâand almost impossible without protocols.
Testing
The WWDC session on protocol oriented programming mentioned testing, but it was a point that I didnât notice at the time.
Initially, I found that similar operations needed slightly different ways of actually performing the request (SecItemAdd
vs SecItemCopyMatching
, etc.). To help with code reuse, I introduced a closure on the root protocol: var performRequestClosure: (requestReference: CFDictionaryRef, inout result: AnyObject?) -> (OSStatus) { get }
. Hairy.
This started as an internal implementation detail, and became a super useful aspect of the library.
First, it provides another point of customization, where users can change how or where certain items will be stored. If someone wants to store their type somewhere other than the iOS keychain, itâs really easy for them to do that. I use this internally in Locksmith, so that we can customize the options for a request in shared code, and then perform the request in code unique to the implementer. Very useful.
Second, and following from the first point, we can override performRequestClosure
to get access to the serialized request, and make sure weâve added our attributes properly.
We implement validation of the request to be performed in the closureâwe take the requestReference
, convert it to an NSDictionary, and check that all of the required attributes have been set.
That means we can easily and thoroughly test that all of the desired properties have been set, but without using mocks and relying on internal implementation!
Wrap up
Thereâs a lot to be liked about protocol oriented programming, but the big thing for me is the feeling that I still have a tonne to learn. This was my first crack at it, so if you have feedback or suggestions, feel free to contact me on Twitter.
This was a very design-focused post. If you want to see how itâs actually implemented, check out the Locksmith Github repository.
ð Vanilla â hide icons from your Mac menu bar for free
ð Rocket â super-fast emoji shortcuts everywhere on Mac⦠:clap: â ð
â³ Horo â the best free timer app for Mac
ð FastFolderFinder â a lightning-fast launchbar app for folders and apps
ð Kubernetes â my book on Kubernetes for web app developers
ð Emoji Bullet List â easily emojify your bullet point lists (like this one!)
Jump on my email list to get sent the stuff thatâs too raunchy for the blog.
(Seriously though, itâs an occasional update on apps Iâve built and posts Iâve written recently.)