Repository Design Pattern in Swift
What problem does it solve?
If you need to query your model objects from different locations in your code over and over again, a repository can be really helpful to provide a single-entry point to work with your models and remove duplicate query code. You can take it even further and use it with protocols, this way you can easily switch out implementations (for example for unit tests) or you can use it with generics to make a more *drum roll*generic abstraction. In this article I will cover all these cases.
Sketching the scene.
Let’s say you have some code that fetches data from an API and maps this to model objects. In this example I’ll fetch a list of articles from a server.
This may look a bit funky, but it’s just RxSwift, using Moya as the networking abstraction layer, but that doesn’t really matter to understand what’s happening. The way how you retrieve your data is totally up to you.
This piece of code does
- A GET request to the server
- Maps the returned JSON to an array of Article objects
- The closure gets called when all the work is done.
Why do we need a repository?
Well at the moment we don’t. If you only call the API once in your entire code base, adding a repository might be overkill (or as some may say over-engineering).
Ok … but when is a repository object convenient to use?
Let’s say your codebase starts to grow, and you need to write the code to fetch the articles over and over again. You might say “let’s copy the code and paste it wherever you need to fetch all the articles.”
No harm done, nobody died. Right?
At that moment a big red alarm should start flashing in your brains. 🚨
A repository is just an object that encapsulates all the code to query your models in one place, so you have a single-point of entry if you want e.g. get all the articles.
Let’s create a repository object that provides a public API to get the articles.
Now we can call this method and we don’t have to worry about what happens behind the scenes to get the actual articles.
Just call the method, and you get the articles. Nice, right?
But wait, there is more!
Handle all article interactions
We can use the repository to add more methods to interact with our model object. Most times you want to do CRUD (create, read, update, delete) operations on your model. Well, just add the logic for these operations in the repository.
This make a nice API to use throughout your code, without having to repeat the same code over and over again.
In practice, the use of a repository would look like this.
Quite nice and readable, right? But, wait it gets even better.
In the previous code, I always used the example of ‘getting data from an API’. But what if you need to add support to load data from a local JSON file instead of an online source.
Well if you create a protocol that lists the method names, you can create an implementation for the online API and one to get the data offline.
This could look like this.
A protocol just says ‘if you conform to me, you need to have these methods signatures, but I don’t care about the actual implementation!’
So that’s great, you can create a WebArticleRepository and a LocalArticleRepository. They’ll both have all the methods that are listed in the protocol, but you can write 2 totally different implementations.
Power-up: Unit Testing
The use of protocols is also really convenient when you want to unit test your code, because you can just create another object that implements the repository protocol, but instead returns mock data.
If you use this together with dependency injection, it makes it really easy to test a specific object.
Let’s say you have a view model, and the view model gets its data via a repository.
If you want to test the view model, you’re stuck with the articles that will be fetched from the web.
This is actually not what we want. We want our test to be deterministic as much as possible. In this case, the articles retrieved from the web could change over time, there could be no internet connection at the time the tests run, the server could be down, … these are all possible scenarios in which our tests would fail, because they are out of our control. And when we test, we want/need to be in control.
Luckily it’s actually really simple to solve this.
Hello, dependency injection.
You just need to set the articleRepo property via the initializer. The default case, will be the one that you want for your production code and when you write a unit test, you can swap out the repository with your mock version.
But maybe you’re thinking, well what about the types? A WebArticleRepository is not a MockArticleRepository, so will the compiler not complain? Well, not if you use the protocol as a type. This way we let the compiler know, allow everything as long as it conforms to the ArticleRepository protocol (which both the Web and MockArticleRepository do).
The final code would look like this.
And in your unit test you could swap it out like this.
Now you have full control over what data your repository returns.
Super power-up: generics
You could take this even further, by using generics. If you think about it, most repository always have the same operations
- get all the things
- get some of the things
- insert some things
- delete thing
- update a thing
Well the only thing that is different is the word ‘thing’, so this might be an excellent candidate to use a protocol with generics. It might sound complicated, but actually it’s quite simple to do.
First we’ll rename the protocol to Repository, to make it more … generic 😏.
And then we’ll remove all the Article types, and replace them by the magic T. But the letter T is just a replacement for … anything that we want it to be. We just need to mark T as the associated type of the protocol.
So now we can use this protocol for any model object we have.
1. Article repository
The compiler will infer the type of T to Article, because by implementing the methods, we have specified what T is. In this case an Article object.
2. User Repository
I hope you enjoyed the article and if you have any questions or remarks, just ask them below or reach out to me on Twitter and let’s have a chat.