Modularity on iOS

Jean-David Morgenstern-P
4 min readJan 28, 2020

--

What modularity is. What it is not. And why we need it.

Many have published articles on their journey to modularize an existing codebase. I wish to take a step backward and first consider what modularity is and why it is important for large projects and teams.

Once we have a clear understanding of the concept, and we all agree on the need, we can roll up our sleeves and decide upon the best way to achieve greater modularity.

What is Modularity?

Modularization is the process of splitting a codebase into small, reusable and independent components called modules.

A module is a piece of code that can be imported by another module. Frameworks, libraries, applications, and in fact any build target in Xcode, are modules. A single project can include multiple targets and therefore build multiple different modules.

A Fat Dependency Graph

While often understood at the conceptual level, modularity is hard to achieve in practice because it requires foresight.
Consider this graph usually regarded as describing an ideal modular state of a codebase:

Graph Dependency — In Theory

There are three layers. The top layer represents the application layer, the middle layer represents the features and the bottom layer represents the core services.

A non modular implementation would have the Application call a feature, say the Product module, which in turn imports all of the core services.
At first glance this division seems to reflect a modular codebase with small components. After a deeper look it becomes clear the components are not reusable or independent.

Graph Dependency — In Practice

What happens if we want to reuse the Product module in another application? Then this second application would also need to include the Network, Models and Analytics modules. Or whatever feature modules Product depends on such as Cart. We realize that these modules, despite being small, are not independent. Why is it an issue?

  • The Product module can considerably increase the size of the second app.
  • What if the second app has its own network or analytics implementation? There could be a conflict or code duplication.
  • Any change to one of the imported modules and Xcode will recompile the all tree of dependencies.
  • Any bug to one of the imported modules can possibly break the Product module.

Hard vs Soft Dependencies

Modules often depend on other modules for services such as Network, Analytics, Models. If that is so, how can we say that a module is independent? What does dependency mean?

There exists two types of dependencies: hard dependency and soft dependency. A hard dependency occurs when module A calls a specific implementation that lives in module B. Module A then needs to import module B in order to use it.

import Networkclass ProductViewController: UIViewController {
let network: Network
override func viewDidLoad() {
super.viewDidLoad()
network.fetchProduct { result in
}
}
}

A soft dependency is one where module A doesn’t know module B. Module A declares what it needs but is not specific in who is going to provide it. As a consequence, module A doesn’t import module B nor any other module.

protocol Networking {
func fetchProduct(completion: @escaping (Result<[String], Error>) -> Void)
}
class ProductViewController: UIViewController {
let networkService: Networking
override func viewDidLoad() {
super.viewDidLoad()
networkService.fetchProduct { result in
}
}
}

In this scenario, it is the Product caller’s responsibility to provide it with the Networking dependency. Product is not concerned with the details of the fetchProduct method. It might be a REST request or a GQL one. This is an implementation detail hidden to the Product module.

A Flat Dependency Graph

In Swift, a module can declare a soft dependency by using a protocol rather than a class, a struct or enum. Protocol programming enables modules to talk to one another without committing. If module A calls module B, it becomes easy to replace module B with an updated module C. Just make module C implement the same API as module B.

With protocol programming, our originally fat dependency graph can be flattened into:

A modular codebase

A second application can reuse the product features and provide its own implementation of core services.

Why Is Modularity Important?

As we scale our product pipeline and engineering team, modularity enables us to:

  • Build multiple products that share some code
  • Work efficiently together on the same codebase

Small reusable and independent components will have a positive impact on our app performance by reducing the app size. We will be able to A/B test features, replace components without breaking a tree of dependencies, and unit test our core services.

Second, but just as important, is that small and independent modules will have a positive impact on our work as engineers. Do you know the best way to reduce build time? By building less! Small independent components directly translate into faster build time. Independent components are also easier to reason about. Greater readability and easier maintenance will outweigh the additional boilerplate.

We will be able to deliver more features faster.

--

--

No responses yet