Clarifying the Server API
In a client-server application, communication between the client application and the server application must be clear. Similarly, communication between the front-end developers and the back-end developers must be clear. The client sends and requests data from the server, and the server dutifully serves that data. Similarly, the front-end developers and the back-end developers share information about how that data transfer is to be done. Wouldn’t it be nice if both machine and human communication was streamlined?
Using the REST protocol, there is going to be a back-end that serves JSON from an endpoint that looks like:
There’s a lot going on under the hood here. The front-end team is looking at this and dividing it into two or three components, to be able to compose dynamically. The back-end team is making JSON representations of model objects, possibly using JBuilder. Each JSON representation might have properties such as
Then there are view controllers that need to interpret this data into user interfaces. That’s us, the front-end iOS developers. We are the view controllers.
A Communication Problem
If the view controllers call the server using AFNetworking or AlamoFire directly, the code might look something like this:
Pew wee! Stinky code! It looks like a Pyramid of Doom! Newer programmers might ask:
It works, so let’s move on. What’s the problem?
Yes, it works for now. But this method has too many reasons to change. This view controller has too many responsibilities. Some of them are:
- choose the starting index of the beers to be shown
- choose the number of beers to be shown
- remember the server API endpoint
- talk to Alamofire
- handle error
- parse JSON
- and all the other user interactions that ViewControllers handle
That means the view controller is going to get really big really fast, and when it comes time to squashing bugs, they become difficult to find. And when the server call changes, the two dozen view controllers that interface with the api may need to change as well. And when it’s hard to read, its difficult to change for other developers that come after you, even if you were to remember it six months from now.
External documentation doesn’t help much either. Documentation that lists endpoints and examples of their parameters and JSON outputs are non-value-adding activities. And they tend to get outdated faster than the time it takes for you to finish reading it. At that point, you can either ask the API developer to either update the documentation or ask directly about what the key value encoding structure looks like. Then he explains in server-talk gibberish variables, which you have to decode.
What can we do to address these issues? How do we make it super fast to change when change inevitably happens?
Let’s refactor the ugly code shown previously into something that’s more modular, better encapsulated, and easier to read. Furthermore, if the code is self-documenting and in sync with the server, then the human latency between the two sides can be shortened, as well. Before we comtinue, you may want to review some of these design patterns, to get a better understanding of what we’re doing.
Let’s get started! First, let’s factor out some of the variables from within the function.
I would’ve been ok with the method name if I hadn’t changed it, but I’m feeling uneasy about the new name. It feels like I’m missing something. Aha!
That’s what I wanted to do. I want to receive the beers where I ask for them, not buried deep, nested inside a pyramid. Let’s make it happen.
I want to continue, but the endpoint is shared across all my API endpoints. And sometimes I need to switch between develop, staging, and production endpoints. We can either make the endpoint a global variable or a static variable, where we’re also allowed to keep behavior without contaminating the global namespace. Let’s make a wish.
I like the way it sounds, but there’s a compiler error! Let’s fix it.
We’re getting closer. But before we continue, let’s conceptualize what we are trying to do with a diagram:
The user is accessing the database on the Internet through the user interface. It’s starting to look like a kitchen with a menu, as opposed to each customer trying to ask the chef what they want to eat.
The stream of data comes back full circle from the server. Let’s continue refactoring. We’ll name the new class
BeverageServer to reflect its role in its context (See How to Name Things).
I copy pasted the code from earlier into the new method body. But there’s a compiler error! Referring to an unexisting self is not allowed in a static method. By leaning on the compiler, we can see exactly what we need to work on. The priority of work is arranged for us already.
How about the failure? How will we handle that? For now, handling failure is not the responsibility of this method. Such a failure in a GET request is most likely a connectivity issue or a server issue, so we route that to where it’s appropriate: away.
We put a //TODO: tag in the new method, with an assertion message. That will let us know whether the miscommunication is coming from the client side of the server side. Later, when our green path (see Railway Oriented Programming) is complete, we can tidy up our red paths. Moving on.
static-ifying our serverURL, to be shared among all the other calls in this class.
We leave the ! inside during development so that we know where the translation is failing. Also, we keep the JSON parsing as a method to the Beer class because when changing one, the other will change as well. One change corresponds to one class.
It’s starting to look closer to JBuilder. Try to see if you can match the variable names to the JSON strings that the server guys are using. That way, there will be no confusion between developers when referring to the variables. Streamline human communication.
Time == money.
Just like JBuilder, the code is the documentation. It never goes out of date.
Once the code compiles, we run it and see if it behaves as expected. This would be a good time to write a fake beer list using the same constructor, to test some UI.
Now, there’s more than one way to do networking in an app, and there will be more responsibilities introduced later on especially with caching and persistence. But with this setup, new server calls can be set up simply by mimicking the existing pattern. Code is DRY, and when things don’t work, you’ll know exactly where to look.
If you’ve enjoyed this, you might also enjoy Refactoring from Good to Great.