Since winter is coming, food thoughts begin to rise into a delicious fervor. Do I like food? Oh, yes! Yes, I do. Yet, reflections on the How to Isolate Client-Server Interaction Logic in iOS Applications need to happen. So, what’re you gonna do, right?
We’ll set this blog post on fire with food and iOS architecture geekiness. So now, hear me out. This blog post combines the desire for food and reflections about the iOS article into a tasty combination. Even if you don’t absolutely love this approach, there’s always this funny two minute video:
Since I am a skinny Finney and get full quickly, no food is mentioned at the south end of this blog post as opposed to up here in the north.
iOS Architecture Menu
Taking what the article offers and my own background, here are some typical things you’ll often see on the iOS architecture menu whether in formal object form, or via Protocol Oriented Programming mixins / traits, or just thrown in there spaghetti code style:
- View Controllers – Ideally kept from becoming massive cookie monsters with too much code in them
- Presenters, Model Views, or Presentation Models. In general, they have presentation logic separate from the Model objects and views.
- Services – Work with different shared parts of the app, put things together, and interact with the backend server through Client code
- Managers – Code that is in charge of app wide resource management similar to an Alamofire SessionManager or a file manager.
- Translators – Convert incoming or outgoing yummy information to/from the backend servers
- Server Error side dishes – Often in the form of enumerations, these are translated from raw server errors into something tasty for the rest of the code to chew on.
- Model objects – Although not a horrible hodgepodge salad, we’ll lump together both Domain Objects and data Model structures into the word Model.
- “Client” code – Code at the system boundary of the architecture used to talk with the backend server. It’s the last bit of non-library code before data leaves or enters the iPhone.
So the goodies are View Controllers, Presenters, Services, Managers, Translators, Server Errors, Models, and Client code. Since the word Client is overused, let’s expand on that.
The article refers to the Client as:
Sends requests to backend servers and receives responses
Tastily specified in a simple yet sweet way. If outlined, the quoted goals of the Client from the “Bridging the Gap Between the Application and Backend Server” section are:
- intermediary between the application and the backend server
- know nothing about the data models and their structures
- be responsible for invoking specific URLs with provided parameters
- returning incoming JSON data parsed as JSON objects
That’s quite a mouthful! If we digest that a bit, we see their overall point. Near the oven door, the Client code is at the system boundary of the architecture. It doesn’t reference the data models, so it’s free from those code dependencies. Lip smacking good!
Visual Noise In The Client Example Code
Although I understand why the author is not using an example which depends on a library like Alamofire, the visual noise is rough when looking at the WebClient code. Specifically, the load method is the issue. With four parameters and one of them being a closure, it leaves a bad taste in my mouth:
func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask?
I had to reread it a few times to visually parse it apart. I prefer the CRUD & Authorization approach used by Alamofire where method and path are captured as part of the request. Uncle Bob Martin had a good rule of thumb guide about the number of parameters. All that said, I understand why he coded it that way.
With three parameters, our code looks similar to the following:
func makeRequest(_ request: URLRequestConvertible, success: @escaping ResponseClosure, failure: @escaping (_ httpStatusErrorCode: Int) -> Void)
ResponseClosure would actually be a type alias such as
(Whatever)ResponseClosure depending on what the server responds with. It should be noted that our
failure parameters are two of the three parameters. In his
load method, the success and failure are both together in a completion block. To compare apples to apples, we have request and success/failure. Compare that to the
load method’s request-path, method, params, and success/failure.
Applying the Client Approach To My World
This article makes me think. I love it like chocolate. One thing I saw which initially got me excited was that the
ServiceError is created at the Client level as opposed to a higher level. In his
WebClient.load method example, he covers the
ServiceError cases of
custom error, and an
other kind of error. That was exciting because I thought we could potentially simplify our own code by eliminating a level of code. That idea burst into flames like stinky burnt popcorn.
At first, I thought the code I’m working on handled the errors at a higher level than the Client level. However, his WebClient code is what our code conceptually started with. Over time, we realized we had to create a layer higher to handle translating HTTP status codes into the appropriate kind of enumeration Error case specific to the server domain we’re hitting as well as handle other things. We call that layer of objects the Gateway level.
I say server domain because we used to hit multiple servers, but now we don’t. Since the idea of the Gateway has changed from server to server domains, it has gotten confusing. Keeping track of what server domain behind the API is handling a particular request is not straightforward. With a minor exception, the server functionality is hidden behind one API now. Yet, we have to keep the Gateway objects as part of the architecture unless we find homes for all of the following:
- Translation of HTTP status codes into error enumeration cases
- Transforming our JSON response dictionaries into Model objects
- Response caching
Finding Homes For Gateway Object Code
I think it’s possible to find homes for the code that is in the Gateway layer now. It would be great to get rid of that layer if possible. There’s a ton of pass through code in it. Following some approaches of the article, the following changes to our architecture approach would happen at a minimum:
- HTTP status code transformation logic could happen in
Errorextensions similar to what he did in the article with
ServiceErrorand its extension that contains the
- Transformation of JSON dictionaries could happen in the constructor (
init) methods of the Model objects.
- Caching could potentially be handled a level higher than the gateway. Further investigation is needed for that.
Beyond the Networking Module
Since we touched on parts of the world I am working on, let’s go over what my world looks like in general. Although there is a food fight of architecture styles in the app I am currently having fun with, the general architecture design involves the following:
- View Controllers – We consider View Controllers to be part of the View in the Model View ViewModel sense.
- Presenters – There’s debate as to whether we’re set up more for a Model-view-presenter (MVP) architecture, versus Model View ViewModel (MVVM) architecture, versus a Presentation Model architecture. This kind of discussion has gone on for over a decade. Presenters are where our display logic lives.
- Service layer – Interacts with the rest of the system such as with Managers, Gateways, and account information objects. The Service layer uses the Gateways layer.
- Managers – as described above in the “iOS Architecture Menu” section
- Gateways – as described above in the “Applying the Client Approach To My World” section
- Translators translating inbound data – Used by the Gateway layer, the Translators are coded as an extension to Dictionary. They create the Model objects.
- Translators translating outbound objects – This responsibility falls on the Request objects and are used by the Gateway
- Request objects – Define which HTTP method to use, translate objects or raw data into parameters, and do other request specific set up. As mentioned, they are modeled after the Alamofire CRUD & Authorization example.
- Model objects – as described above in the “iOS Architecture Menu” section. Sometimes Model objects have toDictionary methods to help out the Request objects.
On top of that, there is Protocol-Oriented Programming related code in there as well. Not all pieces are involved for every feature. OK. Now we’re cooking!
Network Request Maker
In addition to the Gateway code, we also have a lower level object called the Network Request Maker (NRM). The NRM contains the code that is common across Gateway code items. The NRM interacts with the SessionManager, ensures authentication information is added, and casts the Alamofire results into things like
[String: Any]. Borrowing a term from Aspect-Oriented programming, the NRM also handles other cross-cutting concerns like logging.
A Great Start
So, do I like the article How to Isolate Client-Server Interaction Logic in iOS Applications? Yes, I do. Looking at the article is a great start. I am grateful to the awesome person who shared this with me where I work at CARFAX. However, it’s not the entire meal. As one would expect, you’ll have to take the ideas presented and adapt them to your own world. What’re gonna do? What’re gonna do?