Module Boundaries in iOS

We’ve talked about what modularization is, and what its advantages are when used in a decoupled, cohesive way. It feels only reasonable that we should dig into the meat of what a modular architecture could look like, starting with your module boundaries.

I say “could” on purpose. Whilst I believe there are some anti-patterns to modular design, I feel you should be wary of anyone being too prescriptive on what modularization should look like. Yes, there are guidelines, and yes, there are characteristics like decoupling and cohesion that are proven to be valuable. But the reality is the design of a module, particularly the internal implementation details of a module, has a level of subjectivism about it.

So well, where to start about something so subjective? Well, I want to talk about arguably the most objective portion of this design – the boundaries. Getting your boundaries right from the beginning can help define the use cases of your module, aiming for that high cohesion, but also can help make sure you only expose what needs to be exposed, helping that loose coupling characteristic. Your internal implementation of those features can be a mess, but you get a large amount of the benefits that a modular architecture can bring if you have a well defined boundary around that module.

What are boundaries?

Boundaries are the public point in your module in which other things can interact with it, and often exist around the “outside” of your module. Its the external interface (or API) of your module that exposes specific functionality that needs exposing to other modules, and hides the implementation details from the consumer of that module.

Am image showing a codebase interacting with a module boundary

For example, say your module adopts a Model-View-Controller structure in its implementation, you could refactor to an Model-View-View-Model structure without the consumer of your module knowing about the changes. Its worth noting it’s also important to have a high quality test suite in place when making those changes to ensure functionality isn’t lost or altered as that could impact the consumer of the module.

For example, lets say you have a Network module. This module contains all the code you need to be able to make a request to a web API. What functionality of that module would you need to expose to the rest of your app? Whilst not comprehensive, this is a rough outline of what that boundary design needs to be able to provide in plain English:

Consumers of the module need to be able to:

  • Create the Network module.
  • Send get requests.
  • Send post requests.

If we flip that into code, we get something that looks like the below:

protocol NetworkModule {

    func createModule()

    func get()

    func post()
}

We don’t need to worry how to design the underlying code for that functionality yet. Just having the boundaries in place for what is expected of that module is a decent place to start, as it allows us to see what functionality we need to expose from the module.

So why start there?

An analogy

I absolutely love running an analogy into the ground so here goes.

When building a house, the designers of the house don’t start with thinking about where the couch will go inside the house. The designers start by thinking about the foundations of the house, the positioning of the house, how it might fit in with the neighbors either side, or how many rooms the house should have. If you get those properties of a house wrong, adjusting them after, whilst doable, can be a bit of a hassle, and can cause inconvenience for you and your neighbors.

It might be tempting to think about the couch, as that’s ultimately one of the things that will make the house function as a home. Deciding on a couch is also arguably easier than building and designing the house, which adds to the temptation to focus on it. But it’s not where a project should begin, and the foundations and boundaries of the house are much more important for the stability of the house and for the occupants to enjoy their time there. It also delays the decision of deciding on the couch as if you designed your house around a specific couch, replacing that couch could prove very costly. Imagine something has to change during the build of the house, and you end up with a bigger room. Suddenly, you’re wishing you sprung for the corner sofa rather than the 2 seater.

Moving back onto modules…

Stability

The external interface to your module is arguably the most important piece of the puzzle when it comes to module design, like the boundary of a house. It’s the place where consumers will interact with your module, so it also becomes the place where making changes can impact those consumers. The underlying implementations of those interfaces can change and your consumers will be none the wiser (assuming those changes don’t impact the core functionality of the interaction), but changing the interfaces that the consumers are using can have wider impacts. These interfaces should be the most stable part of the module, and require the least amount of changes in the lifetime of the module.

Defers premature decision making

It also allows us to defer premature decisions about how a system should work. A premature decision is anything that isn’t related to the business related use cases of the system. Using the example of our Network library before, a common example of a premature decision might be deciding which networking library should be implemented rather than focusing on designing the interface around the yet-to-be-decided networking dependency (i.e URLSession). Your yet-to-be-decided networking library finds itself in a similar position to the couch in the house analogy.

Software design that allows those dependencies and systems to be changed easily should be the end goal, so spending time on those dependencies at the start of designing a system can result in software designed around a specific piece of technology, which can create a world of hurt if that technology ever needs to change. Deferring those decisions to when they’re needed allows you to spend the time designing the interfaces you would like to interact with for whichever dependency you end up using, rather than the dependency deciding those interfaces for you.

Build your house for you, not for the couch that may only be there for a couple years.

Conclusion

I think the main takeaway from this is that building well defined module boundaries as the starting point when defining your module, enables you to build out your module feature set in a way that takes into consideration what you need to expose to other modules, and helps you push down the implementation detail of the module, deferring the decision making process of what that implementation might look like.

  • The Double Edged Sword: Apple Music and Dolby Atmos

    A month or so back I bought “Dark Side of The Moon” on Blu-Ray to finally listen to the Atmos remix and – not to mince words her – it was revelatory. Maybe the most…

  • ImageSequencer – Build a video from a collection of images in iOS/macOS/tvOS

    I’ve been working on Lapsey – an app to make beautiful timelapse’s with – and whilst I won’t be open-sourcing the entire project, I am trying to open-source various components in the app that feel…

  • Get all available cameras in iOS using AVCaptureDevice.DiscoverySession

    I’m working on an iOS project that required finding all of the users available cameras on their device. This used to be simple enough prior to iOS 10, as you could just call: This would…

  • Examples of Use Cases in Swift

    In the last post we went over what use cases are in software development and how they can be used in iOS development. We also went over their origin as a requirements gathering technique for…

  • Use Cases in iOS Development

    Use cases have historically been a somewhat confusing topic to venture into for me personally, and I’m now of the believe that is because they typically have a couple of definitions, depending on who you’re…

  • UML Diagrams with PlantUML and SwiftPlantUML

    PlantUML is an open-source tool used to produce an assortment of diagrams using text. With other diagramming tools, the paradigm is typically a GUI and some dragging and dropping of various objects to build up…

  • Camera for iOS – A Swift Package

    Currently preparing a large post going over Clean Architecture in iOS and how that relates to modularization, but whilst that is in the making, I figured I’d post about my newly released Camera framework, and…

  • Feature Modularization in iOS

    So you’ve decided a loosely coupled, highly cohesive, modular architecture with well defined boundaries, is the approach you want to take with your project. Now its time to go over how to deal with separating…

  • Module Boundaries in iOS

    We’ve talked about what modularization is, and what its advantages are when used in a decoupled, cohesive way. It feels only reasonable that we should dig into the meat of what a modular architecture could…

  • Advantages to modularization in iOS

    We’ve already talked about what modularization is, and why a team might want to architect their codebase in such a way. But what are the real life advantages to having multiple units of code to…