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.
A 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
Request
object 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, oneRequest
object. - NetworkDispatcher: A
NetworkDispatcher
is an object that is responsible of getting aRequest
and return aResponse
. 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, thisNetworkDispatcher
can be replaced for aMockNetworkDispatcher
that doesn’t actually make any network request, and instead getting the response from a JSON file, leading to a naturally testable architecture. - NetworkTask: A
NetworkTask
is a subclass of the generic classTask
. A task, as I will explain better in a moment, is a generic class that is responsible of getting anInput
type and returning aOutput
type, either in a synchronous, or asynchronous way. You can implement aTask
using 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
A Request
is an object that is responsible of configuring everything that may be necessary to create a URLRequest
.
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 GetAllUsersRequest
or LoginRequest
.
Implementing a NetworkDispatcher
The 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.
A 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.
Isolating Tasks
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 Promise
, Signal
, 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.
Every 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 Observable
.
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.
Reviewing architecture
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:
Conclusion
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 ViewModel
, Presenter
, Interactor
, 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.