Modularizing and Decoupling Large Code Bases
Good architecture requires many components: defining modules, defining the purpose of these modules, following separation of concerns philosophy, decoupling to reduce inter-module dependency, testability etc. The list can be exhaustive.
As part of the TextNow foundation squad, I am responsible for ensuring the core parts of our products are well-architected and align with industry best practices when it comes to iOS development.
Our codebase has some great architecture, and some not-so-great parts of code that lack a central purpose of existence. While all of the above cannot be achieved instantly, it certainly can be done over a period of time with care and a good understanding of the codebase. Code that has not been architected for some time (did I mention it’s a constant process?) usually goes into the downward spiral of copy-pasting without understanding the underlying architecture or philosophy.
During our last sprint, we shifted the way we setup dependency of the individual components, UI, Services, and Core Data, and how they depend on each other:
UI: The UI layer is where the user interacts with the application. Touches, drags, selections, entries are all events that are eventually handed down to other components for processing.
Services: The Services layer is where all background processing happens. This could mean network calls, or data storage in Core Data layer etc.
Core Data: The Core Data layer is where all data processing happens. Fetching data, updating data etc. are the responsibility of this layer.
The UI layer depends heavily on the Data layer, i.e. anything that is shown to the user is being pulled from the Data layer. So when views loads up, data is requested from the Data layer. In addition, the UI layer also listens for Data changes directly using FetchRequestControllers (FRC). FRCs are part of Core Data’s set of classes.
FRCs are live objects that act as channels for retrieving data from the Core Data layer and also reporting changes in the Core Data layer. It uses the delegate pattern to report changes (Inserts, Updates, Deletes, Moves).
The UI layer sets up the FRCs for certain entities (in this case the logged in user’s account info) and waits for changes on those entities. At random times (user action driven) the UI requests new information for the User entity by calling the Service layer. The Service layer makes a REST call to the TextNow server using background queues, gets the data, and stores it in the Core Data layer — first into the child context and then into the main context. This save triggers the FRCs to fire. Completion blocks are registered on the delegate callbacks and they execute each time the FRC’s fire.
The above dependency of the UI layer on Core Data layer is shown below. The problem with this architecture is that the UI layer now needs to know about the Data layer, and it has to include the necessary Core Data header files in order to set up the FRCs. There is no control on when the UI should be notified about changes, since no one is managing the FRCs. They fire when they see a change. So it could mean firing when the user is working on the UI — Dragging, Swiping, Entering text etc.
To avoid the dependency of UI layer on the data layer, we introduced asynchronous messaging. Messaging is a fairly standard way of de-coupling systems. The message queue acts as the intent (commands) from one component to the other. It can be bi-directional, where each module just sends a message to the queue with an intent. This message is then picked by some other module who is interested in the intent, processes it and drops the results back in the queue again with a response intent.
Messaging is asynchronous. So modules will have to be prepared to communicate asynchronously. In fact even Apple’s touch events are queued and delivered to the application layer on the main thread. So messaging can be applied anywhere, even intra-module communications.
Here is how the new architecture looks: with Apple’s Asynchronous Notification queues, we can enqueue messages and indicate that the notifications should fire only when the system is idle. This is exactly what was needed in our architecture. Since user’s account info changes do not interrupt the user’s actions, an idle notification would be sufficient and in fact appropriate in our case.
Pros of Messaging:
- Messaging is great when one component does not need to know who the subscriber or consumer is. A message is dropped into the queue and that is about it.
- Messaging is great when you want to avoid spaghetti dependency of the different logical parts of the codebase.
- Messaging is great when you want testable code. Since dependency is reduced, tests do not have to set up all the components of the system. It just needs to set up each logical component and test it out.
Cons of Messaging:
- Messaging issues are sometimes harder to find. One cannot put breakpoints on one module and break on an exact call of a certain message. One would have to log the messages, or mock a message and see how a module behaves.
At TextNow we are solving interesting problems like this everyday. If you are like a good challenge, check out some of our engineering job openings.