Architect Your iOS App for Easy Backend Replacement - Part I: Functional Domain Design

Datetime:2016-08-22 23:25:26          Topic: Software Architecture  IOS Development           Share

Using a third party backend service (BaaS) for your iOS app’s backend can be risky, as you have no control over a third party. They can shut down your backend and put you in a tough spot. But there are cases when you need to use third party backend services. How to mitigate the risk of the third party pulling the plug on your backend? The solution, of course, is to architect your iOS app so that you can easily switch backends when required.

Making your app modular enough for quick backend replacement is not always an easy task. Fortunately, we can use a few techniques to attain more modularity. In this post, I want to show how I’ve been doing it lately, by using a combination of ideas from Domain Driven Design (DDD) and functional programming, inspired by Debasish Ghosh ‘s book Functional and Reactive Domain Modelling .

The following techniques can help you make your app modular enough so that you can easily switch backends, or even use several different backends at the same time. For example, if you’re in the process of migrating backend servers, and you need to connect to two different servers to get your data. Or, if no backend is available at all, but you have local JSON files that you want to use temporarily.

Representing Backends With Repositories

The first DDD concept I want to talk about is called the Repository Pattern. Repositories are wrapper classes (constrained by protocols) that perform read and write operations on a backend. Repositories are beneficial because they are implementation-independent contracts for accessing your backend . A repository can be implemented with a local database or as a web service, but the protocol for the repository doesn’t change. So if you switch backends, you don’t have to change the protocol — you merely create a new repository implementation that conforms to the same protocol as the old repository. The contract should remain the same. And your app doesn’t break!

Let’s consider an example of a coffee shop taking orders. The following OrderRepository protocol exposes method signatures for querying, storing, and deleting orders:

protocol OrderRepository {

    func query(orderId:Int) --> Order

    func update(orderId:Int, order:Order) --> Bool

    func write(orders:[Order]) --> Bool

    func delete(order:[Order]) --> Bool

}

Notice that OrderRepository serves as an implementation-independent contract. Our orders may be stored on an AWS cloud service, on our own servers, or on a local mock JSON store. All of these implementations can satisfy the OrderRepository protocol.

If it turns out that we decide to switch from the current web service to a different one — for example, a backend based on Firebase — then we can just write a new implementation and call it something like OrderFirebaseRepository , and make it conform to the OrderRepository protocol.

Representing Behaviours With Services

Repositories offer implementation-independent abstractions for our backends. But in any app we also have to perform behaviors on the data. Sometimes we need to retrieve data from different sources and then transform them into proper model objects before we can store them. And often we have to objects interact with other objects to produce new objects.

Where to put these behaviors for optimal modularity? We want to keep our views and view controllers thin, so we won’t put them there. And we don’t want to put them inside model classes, as we’d rather use models as mere data containers without coupling them with behaviors. Instead, we’ll put these behavior methods inside in specialized modules, called services .

A service module consists of functions (or methods). Each method expresses a distinct behaviour. For example, suppose we had a class called OrderService , with a method called createOrder which is responsible for creating a new coffee shop order:

func createOrder(orderInfo:Dictionary

     ) -> Order { let order = Order(orderInfo) OrderAWSRepository.sharedInstance.write([order]) return order }

And here is an addItem method that adds an item to an existing order:

func addItem(orderId:Int, item:Item) -> Order {
    let currentOrder = OrderAWSRepository.sharedInstance.query(orderId)
    let newOrder = Order(currentOrder.items + item)
    if OrderAWSRepository.sharedInstance.update(orderId, newOrder) {
        return newOrder 
    }
    return currentOrder
}

As you can see, the basic strategy here is that the methods call the backend which is represented by a singleton repository object.

Using Dependency Injection to Avoid Singletons

I just mentioned that I used a singleton ( OrderRepository.sharedInstance ) for the repository object. That was not good, and I apologize for doing so. Singletons are not a good fit with the repository pattern. This is because singletons couple our service methods with our repositories.

Why is this bad? Suppose we want to test some of our service methods with different repositories, like a mock repository that stores data locally. Or suppose we're in the process of migrating backends and we need two different repository implementations at any given time -- one AWS implementation, and one Firebase implementation. Or maybe we have to call the web service when we're online, but call a local datastore when offline. With our singleton-based approach, we can't mix and match repositories too easily -- each method already references a particular repository.

A better solution would be if the caller to the service can pass in a repository as an argument, so that the service is not coupled with any particular repository. In other words, we can use dependency injection .

Let's look at how dependency injection can help us in this case. Here is how a MovieService class may look like, with an "init" that allows the repository object to be injected into the class:

Now our methods inside the class can refer to the injected repository object:

func createOrder(orderInfo:Dictionary

     ) -> Order { let order = Order(orderInfo) self.repo.write([order]) return order }

Gaining More Modularity Without Classes

One problem with the class-based dependency injection we've done above is that we've introduced another kind of coupling -- the coupling of services with repositories.

It would be better if our services were standalone modules that don't depend on repositories. Then whenever we want to swap backends, we don't have to modify our service modules.

To de-couple our service modules from our repositories, let's try this approach:

  • Stop using classes for our service modules, and instead make our service modules a bunch of protocols with corresponding protocol extensions.
  • Put our behavior functions inside the protocol extensions.
  • Inject the repository dependencies at the function-level rather than class-level.

If we follow the above two guidelines, our createOrder function would just be a standalone function implemented inside a protocol extension, and it would look something like this:

func createOrder(orderInfo:Dictionary

     , repo:OrderRepository) { let order = Order(orderInfo) repo.write([order]) }

Notice that createOrder only depends on its arguments. The backend dependency (i.e. the repository) is injected at the function level, so there is no need to reference an object from an upper-level scope, like a class.

Let's get into a real world scenario for and see how this would play out. A customer is at our coffee shop and orders a latte. She then decides to order a slice of as well. Then she cancels the cake:

let repo = OrderAWSRepository()
let order = createOrder([121:"latte"], repo) 
let orderModified = addItem([521:"cake"], repo)
let orderModifiedAgain = removeItem([121:"cake"], repo)

Did we just throw away OOP for procedural programming? It seems like we're now stuck with verbose procedural code!

Fortunately, we can get back the abstraction of OOP without the coupling. To do this, we'll leverage Swift's functional programming-based features, like currying and partial application . We'll explore those concepts in the next post





About List