Isolating tasks in Swift, or how to create a testable networking layer.
There are a lot of shiny iOS architectures that are getting more and more hype in the last few years. As they are all valid and have good and bad parts, they all address the same thing: separate the business logic from the presentation. Today, I’m going to write about a simple concept that is applicable to your next project’s architecture, whichever architecture you are planning to use.
A pretty common networking layer
To explain my point, I will first talk a bit about how network layers are often implemented.
I have seen a lot of different network layers. In most of them, there is a NetworkManager, ConnectionManager, or something like those. There is a single class that contains every single API call in the app. While that’s valid and works fine, it fails in a core concept in software design: Single Responsibility.
ConnectionManager contains too many responsibilities to be considered a good practice. Moreover, it is often implemented as a singleton. And I don’t say singletons are necessarily bad, but they can’t be injected as dependencies, and can’t be easily mocked when testing.
This is a very frequent practice. I’ve also seen this in MVVM, or MVP architectures.
A different approach
A data access layer can be implemented in a different way. Let describe the process in networking fetching:
Put in this way, a network call implies, at least, three steps:
- Create a request: this means setting the URL, method, parameters (either in URL path or http body) and HTTP headers.
- Dispatch request: This is a very, very important step. The request that was created and configured in the previous step must be dispatched using URLSession, or a layer over that (for example, Alamofire).
- Get and parse response: It’s important that this step should be implemented separated from the previous two. This is where you should validate JSON or XML response and parse that into a valid Entity (or Model if you prefer to say).
If you really want your architecture to be clean and testable, those three steps should be implemented in different objects.
Those objects are the following:
- Request: A
Requestobject has everything needed for configuring a network request. It is a struct, or class that is responsible for configuring a single network request. And this is very important: One network request, one
- NetworkDispatcher: A
NetworkDispatcheris an object that is responsible of getting a
Requestand return a
Response. It should be implemented as a protocol. You should code against that protocol and not against a concrete class (or struct), and it should never, ever, be implemented as a singleton. If you do that, this
NetworkDispatchercan be replaced for a
MockNetworkDispatcherthat doesn’t actually make any network request, and instead getting the response from a JSON file, leading to a naturally testable architecture.
- NetworkTask: A
NetworkTaskis a subclass of the generic class
Task. A task, as I will explain better in a moment, is a generic class that is responsible of getting an
Inputtype and returning a
Outputtype, either in a synchronous, or asynchronous way. You can implement a
Taskusing RxSwift, ReactiveCocoa, Hydra, Microfutures, FOTask, or simply using closures. It’s up to you. The important part here is the concept, not the implementation details.
Implementing a Request
Request is an object that is responsible of configuring everything that may be necessary to create a
An example of a network request may be like this:
As simple as it is. The important part here is that every
Request is implemented as a separate object. Of course, this can be also implemented as an enum, like Moya promotes, it depends on what you like more. I personally prefer using a more object oriented style and implementing a
BaseRequest class, and subclasses like
AuthenticatedRequest and more specific requests like
Implementing a NetworkDispatcher
NetworkDispatcher is the component that is responsible of dispatching network requests.
Note: hereinafter, I will be using
RxSwift in my examples, but you are free to implement them the way you prefer.
NetworkDispatcher has a single responsibility: dispatching
Request objects and returning responses.
The cool thing about using a protocol instead of a specific implementation here is that a protocol based implementation is easily interchangeable. You can have a
MockNetworkDispatcher, which doesn’t actually perform any “network” operation, and instead returns a response from a JSON file, making testing much easier.
Tasks are simple objects that are responsible of performing a single logic operation. It may be getting users from the backend, logging in or registering a user, for example. Tasks can be synchronous or asynchronous, but that should be transparent from the client side. I like using the convenient abstraction that is RxSwift’s
Observable but a
Future or a simple callback can be enough.
A simple implementation of
Task can be as follows:
I use an object oriented style, but you are free to do some cool stuff here like associated types and type erasure, that may work well too. I prefer using an object oriented style because it seems less hacky and it’s simpler to implement.
Task needs two generic parameters: a
Input type, and an
Output one. A
Task performs an operation that includes receiving an
Input object and returning an
Output, maybe using an abstraction over it, like
We need to specialize
Task to perform networking operations.
As you can see, a
NetworkTask needs two generic types, an
Input and an
Output, it’s worth noting that the
Input must be a
Request object. A
NetworkTask should be only instantiated with a
NetworkDispatcher so you can easily pass a
MockNetworkDispatcher when you want to test.
Implementing business logic in this way helps to reduce coupling without introducing complexity and increasing testability.
This idea can be shown in a diagram as follows:
Isolating business logic operations in separated objects is a good practice, as it allows creating a more testable architecture. It reduces complexity and is totally independent of which architecture you are using. This can be used behind a
Store, or whatever you are using to separate business logic from presentation logic.
I hope this will help you as much as me. If you have a doubt or know a better way to do this, please, leave your comment below.