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.

Networking Layer is commonly implemented as a singleton

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:

Steps involved in a network call

Put in this way, a network call implies, at least, three steps:

  1. Create a request: this means setting the URL, method, parameters (either in URL path or http body) and HTTP headers.
  2. 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).
  3. 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.

Network layer should be implemented using, at least, three different components.

Those objects are the following:

  1. 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, one Request object.
  2. NetworkDispatcher: A NetworkDispatcher is an object that is responsible of getting a Request and 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 NetworkDispatcher can be replaced for a MockNetworkDispatcher that doesn’t actually make any network request, and instead getting the response from a JSON file, leading to a naturally testable architecture.
  3. NetworkTask: A NetworkTask is 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 Input type and returning a Output type, either in a synchronous, or asynchronous way. You can implement a Task 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:

Task based network layer

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.