Keeping External Dependencies On The Outer Layer
I’m going to lay out a technique that I use to keep external dependencies on the outer layer of the apps that I work on.
Before we begin, this method was by no means invented by me, parts of it are quite old, in fact. It’s just one of those things I wish I had started doing earlier so I thought I’d share.
The Problem
A common thing to see at the top of a UIViewController;
Starts off ok. Importing Foundation and UIKit sounds reasonable enough. But that’s when things take a dive. If you import big external libraries like Core Data and Google Analytics, you’re going to have a problem later on if, for example, you found out one of the libraries surreptitiously captures your user’s data without you or your users knowing and you want to rip it out quickly. Ripping it out is typically going to involve re-writing code in multiple view controllers, and cutting off the tentacles can prove a nightmare.
One way to combat this is to use a monolithic UIViewController base class that has methods for doing common tasks related to (in this example) persistence or analytics. For me, that’s a no-no. Mainly because I don’t really want a greasy slide of inheritance throughout my app and all the baggage that comes with it, and nor do I believe it belongs in a view controller.
A Potential Solution
This is where leaning heavily on protocols can lead to a big win. First let’s declare a few domain objects that are specific to our app and only uses primitive types.
Then we’re going to declare a protocol that can handle common tasks related to the dependency you wish to use. For now we’ll concentrate on a persistence layer.
Next, we declare a repository-style struct that connects our inner classes (eg. view controllers) to our database (outer layer) and has some common queries we might want to use.
Now, we bring in the dependency. For example, if we wanted to use CoreData we’d declare a new struct which adheres to Database.
As you can see we’re using some models here that are specific to Core Data (NSManagedObject). You can make your life easier by providing the mapping to your domain layer objects in that class via way of a protocol method along the lines of func domainObject() -> Person which simply maps the Core Data properties to a new instance of your domain layer model equivalent.
Technically, you should now be able to pass your domain objects around your inner layers without worrying about how and where they are stored. At the call site it would simply look a little like this;
If you now wanted to switch to a Realm Database you’d just need to do the following:
-
Create a new RealmDatabase object that adheres to Database
-
Create a new RealmPerson object that maps to and from Person
-
Update the DatabaseRepository initialiser to use RealmDatabase() instead of CoreDataDatabase()
And that’s it! Your commit would be tiny and (in this case) only add a 2 new files, even though you’ve just switched your (typically deep-rooted) persistence layer to another provider.
Other Use Cases
There are several ways to use this pattern, but it basically comes down to being able to abstract up a layer from your dependencies so that the implementation is generic enough to work with multiple libraries (that do the same thing). Here are a few other examples;
All of these examples can be easily implemented by all sorts of libraries, but it avoids having 3rd party code and models in your domain layer and makes switching one out an absolute breeze.
For example, in my last app I use a repository to sit between my core app layer and HealthKit. That way, if I ever wanted to switch to another source of health data, I could do so easily. Also, my domain layer objects don’t have the 100s of properties that HealthKit’s do, instead, my models just have the properties I need.
Conclusion
There are obvious benefits to this approach for testing, as well as some advanced implementations where the repo/manager holds multiple instances of a protocol type. Handy if, for example, you’re using multiple analytics providers, or uploading files to multiple places (eg. Amazon and Firebase).
Writing all this down makes it sounds like I’m pointing out the bleeding obvious, but hopefully it’ll be useful to someone. And as usual, the implementation of this pattern sounds great until you actually come to do it and meet a giant road block. It’s happened to me countless times, but for the sole reason of being able to rip out dependencies at a days notice makes it worth pursuing.
I’d love to hear from you if you like what I’ve done, or likewise if you don’t. Or if you have any questions I’m always happy to help and you can find me on email or Twitter.