Too many dependencies in iOS? Use the composite root pattern
The iOS architecture is not just MVC, MVVM, and M*.
If you ask an iOS engineer, “What architecture will you use to design X applications?” “They usually answer MVC, MVVM, MVP, and so on.
This article will explain why these popular acronyms do not fully answer the question and will show an alternative solution.
MVC, MVVM, MVP is UI layer design or UI architecture, not system architecture.
They describe the flow of data and the separation of responsibilities within the UI layer. It doesn’t answer any questions about navigation, networking, caching, business logic, etc.
Adding these responsibilities to the UI layer results in monolithic applications with a large number of dependency diagrams. This leads to rewrites, untestable code, and costly changes.
The solution is to break up monomers into modular components and combine them at the “composite root”. For example, let’s say you want to create a photo feed like Instagram.
A common user interface architecture might look like this.
A user selects a FeedImageCell and expect to navigate to a FeedDetailedViewController.
The code above is a common policy that requires the view controller to be responsible for navigation and creating its children.
Now imagine that your requirements have changed and you now need to implement the ability to record, retrieve resources from remote or local repositories, and support older operating systems.
You use URLSession, in viewDidLoad, as shown below, to load the data.
When the user selects an item, you check to determine which controller to navigate to and record it.
These minor changes require developers to modify the FeedViewController. This violates the Open/Close principle because the possible navigation paths are determined early in the compilation.
Even if we inject dependencies into the deferred FeedViewController by constructing an injection, we still need to inject dependencies for any child view controllers it creates.
If the child view controller has child view controllers, we must inject these responsibilities on the root view controller and layer them on top of each other. This can go on indefinitely in large, complex applications and cause major problems in large-scale dependency diagrams.
By coupling with the LoggingFramework and URLSession, you can already see the dependency network around FeedViewController. The dependencies of any of its children will also become its dependencies.
Welcome combination root model
Composing the root pattern delays decisions about how components interact and allows us to intercept and modify behavior.
It exists in the Main module, a concept unfamiliar to most iOS engineers.
Let’s look at how to combine components and break up monomers in the main module.
Now, FeedViewController is no longer responsible for creating and navigating its children.
FeedComposer combines the FeedViewController and assembles it with all of its dependencies. Using closures, we can inject behavior without coupling UI module components to Networking or Logging modules.
FeedNavigation handles the routing and the Logging module is notified through its delegate.
Notice the flow of dependencies. The main module depends on all the other modules, and neither module knows about each other.
By creating modular code and grouping components together at the root of the composition, we create virtual boundaries between modules.
Let’s say we want our Feed application to run on an Apple Watch or Mac OS. The original approach was to couple everything in the UI layer and call it MVC/MVVM/M*, which needed to be completely rewritten.
With our new approach, we just need to rewrite the FeedUI module and make up the root connect component in Main. All business logic, network, and logs will be reused. This is the power of modular design.
Thank you for reading. The complete source code can be found in the GitHub repository.