Backchannel is out, and so is its SDK. I wanted Backchannel’s code to be as polished as possible, to help build trust with other developers. Having no categories, no dependencies, and no weird tricks like swizzling is essential for building that trust, but, ultimately, the code needs to be easily readable.
One way that Backchannel’s SDK maintains its readability is through simplicity. No class in Backchannel is longer than 250 lines of code. When I was newer at programming, I thought that Objective-C’s nature made it hard to write short classes, but as I’ve gotten more experience, I’ve found that the problem was me, rather than the language. True, UIKit doesn’t do you any favors when it comes to creating simplicity, and that’s why you have to enforce it yourself.
Ben Orenstein, in his Simple Rules for New Programmers, says
Just as your classes should contain tiny methods, so should your system be built of tiny classes.
The critical epiphany for me was that if the same amount of code is spread among much smaller classes, there will have to be a lot more classes.
So, how does Backchannel maintain its small class sizes? A lot of classes. If I could come up with a name for it, I made it into a new class. If it was reused code, I figured out a name for it and made it into a new class. Since the code is open-source, I want to touch on some specific techniques here.
Backchannel’s SDK is essentially a whole app that lives inside your app, so the techniques here are as applicable for a regular app as they are for an SDK.
First, and most important, I stuck to a consistent pattern of code. In a normal app, your app delegate creates a UIWindow
and sets up your root view controller. From there, each view controller pushes on more view controllers as needed. Instead, in the Backchannel SDK, I used Coordinators to manage the high-level flow of each task, and pushed extraneous code I could down to children of the view controller. This helped take as much stuff as possible out of the view controller. Taking authentication as an example, this involved using:
Coordinators to handle app flow and data manipulation (BAKAuthenticationCoordinator.m)
Requests to the API as separate classes (BAKCreateAccountRequest.m, BAKSignInRequest.m)
View controllers that handled data presentation and corralling user input (BAKAuthenticationViewController.m)
Views that handled allocating subviews and managing their layout (BAKAuthenticationForm.m)
Layout objects for calculating the layouts (BAKAuthenticationFormLayout.m)
By breaking each of these things down further and further, when a new change came in, it was obvious where to add the code to start working on that feature. Good naming also enables other developers to take a look at the file names and guess where they might want to start editing.
Line length isn’t a perfect metric for complexity (there are others), but it makes a great first approximation, since it’s so easy to determine. One of the most complicated classes in the Backchannel SDK is the message creation coordinator, with three different initializers and the task of coordinating between many small pieces. It’s no coincidence that it’s also one of the longest files. It’s also a ripe target for refactoring and breaking up.
Managing complexity by adding classes is the key here. The second technique I want to touch on is using different classes for similar concepts that are used in different ways. For example, the Backchannel SDK includes both BAKDraft
and BAKMessage
.
Even though they both have many similar properties, like a text body
and an array of attachments
, they serve different purposes: BAKDraft
is used as a data clump for the data that the BAKMessageFormViewController
generates, and BAKMessage
is the message’s representation after it’s created in the API and includes information about display metrics and user permissions. BAKMessage
has room to grow to handle new things from the API, whereas BAKDraft
might expand into a persistent format for saving drafts that the user is working on.
These two objects are nominally the same thing, and it might have been possible to use BAKMessage
in the place of BAKDraft
. Nevertheless, the two concepts fill different roles, have different capabilities and benefit from being separate. Shoehorning them into the same class would be ugly, inelegant, and restrictive.
Finally, there are concepts that you know will need to be encapsulated and separated into their own space, but don’t lend themselves easily to a name. Without a good name, it’s hard to tell what should be in the class and what shouldn’t. In those cases, I find that the best way to develop the identity for the class is not through its name, but rather through its members.
For example, in Backchannel, images upload in the background while the user types out the rest of their message. If the user taps the “Post” button while images are still uploading, posting is delayed until all of the images have uploaded. This definitely sounds like a bit of state I want to encapsulate, potentially with a dispatch_group_t
. I built an object around this dispatch group, and it eventually became BAKAttachmentUploader.
I think that name is ultimately pretty weak (a rough rule for me is if the class name ends in -er, it’s not the best name). The class’s name doesn’t really reveal what its interface should be. However, I knew that my class would have a method like - (void)waitForAttachmentUploads:((^)())block
and it identity would revolve around the dispatch group, and so a new object was born.
There’s lots of little objects (they might even be enums or structs in Swift) in the Backchannel SDK that perform little tasks into which I won’t go into depth here. (Some interesting ones to explore are BAKMessageCreator
, BAKAuthenticationData
, or BAKErrorPresenter
.) While practicing the technique of making more little objects than you’re used to, it might be worthwhile to play a game with yourself to find out what the smallest interesting object you can make is. If it encapsulates a useful piece of logic or data, you might find that you like having it around.
To find the 20 .m
files with the longest length, you can use the following bash command from inside your iOS project repo:
find . -name "*.m" -type f -print0 | xargs -0 wc -l | sort -rn | head -n 20
Pick one of those files and refactor it until it’s not in the list top 20 anymore. Continue until you’re happy.