DEV Community

Cover image for Breaking an app up into modules
Donny Wals
Donny Wals

Posted on • Originally published at donnywals.com

Breaking an app up into modules

As apps grow larger and larger, their complexity tends to increase too. And quite often, the problems you're solving become more specific and niche over time as well. If you're working on an app like this, it's likely that at some point, you will notice that there are parts of your app that you know on the back of your hand, and other parts you may have never seen before. Moreover, these parts might end up somehow talking to each other even though that seems to make no sense. As teams and apps grow, boundaries in a codebase begin to grow naturally while at the same time the boundaries that grow are not enforced properly. And in turn, this leads to a more complicated codebase that becomes harder to test, harder to refactor and harder to reason about.

In today's article, I will explain how you can break a project up into multiple modules that specialize on performing a related set of tasks. I will provide some guidance on how you can implement this in your team, and how you identify parts of your app that would work well as a module. While the idea of modularizing your codebase sounds great, there are also some caveats that I will touch upon. By the end of this article, you should be able to make an informed decision on whether you should break your codebase up into several modules, or if it's better to keep your codebase as-is.

Before I get started, keep in mind that I'm using the term module interchangeably with target, or framework. Throughout this article, I will use the term module unless I'm referring to a specific setting in Xcode or text on the screen.

Determining whether you should break up your codebase

While the idea of breaking your codebase up into multiple modules that you could, in theory, reuse across projects sounds very attractive, it's important that you make an educated decision. Blindly breaking your project up into modules will lead to a modularized codebase where every module needs to use every other module in order to work. If that's the case, you're probably better of not modularizing at all because the boundaries between modules are still unclear, and nothing works in isolation. So how do you decide whether you should break your project up?

Before you can answer that question, it's important to understand the consequences and benefits of breaking a project up into multiple modules. I have composed a list of things that I consider when I decide whether I should break something out into its own module:

  • Does this part of my code have any (implicit) dependencies on the rest of my codebase?
  • Is it likely that I will need this in a different app (for example a tvOS counterpart of the iOS app)?
  • Can somebody work on this completely separate from the app?
  • Does breaking this code out into a separate module make it more testable?
  • Am I running into any problems with my current setup?

There are many more considerations you might want to put into your decision, but I think if you answer "yes" to at least three out of the five bullet points above it might make sense to break part of your code out into a separate module. Especially if you're running into problems with your current setup. I firmly believe that you shouldn't attempt to fix what isn't broken. So breaking a project up into module for the sake of breaking it up is always a bad idea in my opinion. Any task that you perform without a goal or underlying problem is essentially doomed to fail or at least introduce problems that you didn't have before.

As with most things in programming the decision to break your project up is not one that's clear cut. There are always trade-offs and sometimes the correct path forward is more obvious than other times. For example, if you're building an app that will have a tvOS flavor and an iOS flavor, it's pretty clear that using a separate module for shared business logic is a good idea. You can share the business logic, models and networking client between both apps while the UI is completely different. If your app will only work on the iPhone, or even if it works on the iPhone and iPad, it's less clear that you should take this approach.

The same is true if your team works on many apps and you keep introducing the same boilerplate code over and over. If you find yourself doing this, try to package up the boilerplate code in a framework and include it as a separate module in every project. It will allow you to save lots of time and bug fixes are automatically available to all apps. Beware of app-specific code in your module though. Once you break something out into its own module, you should try to make sure that all code that's in the module works for all consumers of that module.

Identifying module candidates

Once you've decided that you have a problem, and modularizing your codebase can fix this problem, you need to identify the scope of your modules. There are several obvious candidates:

  • Data storage layers
  • Networking clients
  • Model definitions
  • Boilerplate code that's used across many projects
  • ViewModels or business logic that used on tvOS and iOS
  • UI components or animations that you want to use in multiple projects

This list is in no way exhaustive, but I hope it gives you an idea of what things might make sense as a specialized module. If you're starting a brand new project, I don't recommend to default to creating modules for the above components. Whether you're starting a new project or refactoring an existing one, you need to think carefully about whether you need something to be its own module or not. Successfully breaking code up into modules is not easy, and doing so prematurely makes the process even harder.

After identifying a good candidate for a module, you need to examine its code closely. In your apps, you will usually use the default access level of objects, properties, and methods. The default access level is internal, which means that anything in the same module (your app) can access them. When you break code out into its own module, it will have its own internal scope. This means that by default, your application code cannot access any code that's part of your module. When you want to expose something to your app, you must explicitly mark that thing as public. Examine the following code for a simple service object and try to figure out what parts should be public, private or internal:

protocol Networking {
  func execute(_ endpoint: Endpoint, completion: @escaping (Result<Data, Error>) -> Void)

  // other requirements
}

enum Endpoint {
  case event(Int)

  // other endpoints
}

struct EventService {
  let network: Networking

  func fetch(event id: Int, completion: @escaping (Result<Event, Error>) -> Void) {
    network.execute(.event(id)) { result in
      do {
        let data = try result.get()
        let event = try JSONDecoder().decode(Event.self, from: data)
        completion(.success(event))
      } catch {
        completion(.failure(error))
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

There's a good chance that you immediately identified the network property on EventService as something that should be private. You're probably used to marking things as private because that's common practice in any codebase, regardless of whether it's split up or not. Deciding what's internal and public is probably less straightforward. I'll show you my solution first, and then I'll explain why I would design it like that.

// 1
internal protocol Networking {
  func execute(_ endpoint: Endpoint, completion: @escaping (Result<Data, Error>) -> Void)

  // other requirements
}

// 2
internal enum Endpoint {
  case event(Int)

  // other endpoints
}

// 3
public struct EventService {
  // 4
  private let network: Networking

  // 5
  public func fetch(event id: Int, completion: @escaping (Result<Event, Error>) -> Void) {
    network.execute(.event(id)) { result in
      do {
        let data = try result.get()
        let event = try JSONDecoder().decode(Event.self, from: data)
        completion(.success(event))
      } catch {
        completion(.failure(error))
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that I explicitly added the internal access level. I only did this for clarity in the example, it's the default access level so in your own codebase it's up to you whether you want to add the internal access level explicitly. Let's go over the comments one by one so I can explain my choices:

  1. Networking is marked internal because the user of the EventService doesn't have any business using the Networking object directly. Our purpose is to allow somebody to retrieve events, not to allow them to make any network call they want.
  2. Endpoint is marked internal for the same reason I marked Networking as internal.
  3. EventService is public because I want users of my module to be able to use this service to retrieve events.
  4. network is private, nobody has any business talking to the EventService's Networking object other than the service itself. Not even within the same module.
  5. fetch(event:completion:) is public because it's how users of my module should interact with the events service.

Identifying your module's public interface helps you to identify whether the code you're abstracting into a module can stand on its own, and it helps you decide whether the abstraction would make sense. A module where everything is public is typically not a great module. The purpose of a module is that it can perform a lot of work on its own and that it enforces a natural boundary between certain parts of your codebase.

Creating new modules in Xcode

Once you've decided that you want to pull a part of your app into its own module, it's time to make the change. In Xcode, go your project's settings and add a new target using the Add Target button:

Add Target Button

In the next window that appears, scroll all the way down and select the Framework option:

Select Add Framework

In the next step, give your module a name, choose the project that your module will belong to, and the application that will use your module:

Configure new module window

This will add a new target to the list of targets in your project settings, and you will now have a new folder in the project navigator. Xcode also adds your new module to the Frameworks, Libraries, and Embedded Content section of your app's project settings:

Framework added to app

Drag all files that you want to move from your application to your module into the new folder, and make sure to update the Target Membership for every file in the File Inspector tab on the right side of your Xcode window:

Target Membership settings

Once you have moved all files from your app to your new module, you can begin to apply the public, private and internal modifiers as needed. If your code is already loosely coupled, this should be a trivial exercise. If your code is a bit more tightly coupled, it might be harder to do this. When everything is good, you should be able to (eventually) run your app and everything should be good. Keep in mind that depending on the size of your codebase this task might be non-trivial and even a bit frustrating. If this process gets to frustrating you might want to take a step back and try to split your code up without modules for now. Try to make sure that objects are clean, and that objects exist in as much isolation as possible.

Maintaining modules in the long run

When you have split your app up into multiple modules, you are now maintaining several codebases. This means that you might often refactor your app, but some of your modules might remain untouched for a while. This is not a bad thing; if everything works, and you have no problems, you might not need to change a thing. Depending on your team size, you might even find that certain developers spend more time in certain modules than others, which might result in slightly different coding styles in different modules. Again, this is not a bad thing. What's important to keep in mind that you're probably growing to a point where it's unreasonable to expect that every developer in your team has equal knowledge about every module. What's important is that the public interface for each module is stable predictable and consistent.

Having multiple modules in your codebase introduces interesting possibilities for the future. In addition to maintaining modules, you might find yourself completely rewriting, refactoring or swapping out entire modules if they become obsolete or if your team has decided that an overhaul of a module is required. Of course, this is highly dependent on your team, projects, and modules but it's not unheard of to make big changes in modules over time. Personally, I think this is where separate modules make a large difference. When I make big changes in a module I can do this without disrupting other developers. A big update of a module might take weeks and that's okay. As long as the public API remains in-tact and functional, nobody will notice that you're making big changes.

If this sounds good to you, keep in mind that the smaller your team is, the more overhead you will have when maintaining your modules. Especially if every module starts maturing and living a live off its own, it becomes more and more like a full-blown project. And if you use one module in several apps, you will always have to ensure that your module remains compatible with those apps. Maintaining modules takes time, and you need to be able to put in that time to utilize modularized projects to their fullest.

Avoiding pitfalls when modularizing

Let's say you've decided that you have the bandwidth to create and maintain a couple of modules. And you've also decided that it absolutely makes sense for your app to be cut up into smaller components. What are things to watch for that I haven't already mentioned?

First, keep in mind that application launch times are impacted by the number of frameworks that need to be loaded. If your app uses dozens of modules, your launch time will be impacted negatively. This is true for external dependencies, but it's also true for code that you own. Moreover, if you have modules that depend on each other, it will take iOS even longer to resolve all dependencies that must be loaded to run your app. The lesson here is to not go overboard and create a module for every UI component or network service you have. Try to keep the number of modules you have low, and only add new ones when it's needed.

Second, make sure that your modules can exist in isolation. If you have five modules in your app and they all import each other in order to work, you haven't achieved much. Your goal should be to write your code so it's flexible and separate from the rest of your app and other modules. It's okay for a networking module to require a module that defines all of your models and some business logic, or maybe your networking module imports a caching module. But when your networking code has to import your UI library, that's a sign that you haven't separated concerns properly.

And most importantly, don't modularize prematurely or if your codebase isn't ready. If splitting your app into modules is a painful process where you're figuring out many things at once, it's a good idea to take a step back and restructure your code. Think about how you would modularize your code later, and try to structure your code like that. Not having the enforced boundary that modules provide can be a valuable tool when preparing your code to be turned into a framework.

In summary

In today's article, you have learned a lot about splitting code up into modules. Everything I wrote in this post is based on my own experiences and opinions, and what works for me might not work for you. Unfortunately, this is the kind of topic where there is no silver bullet. I hope I've been able to provide you some guidance to help you decide whether a modularized codebase is something that fits your team and project, and I hope that I have given you some good examples of when you should or should not split your code up.

You also saw how you can create a new framework in Xcode, and how you can add your application code to it. In addition to creating a framework I briefly explained how you can add your existing code to your framework and I told you that it's important to properly apply the public, private and internal access modifiers.

To wrap it up, I gave you an idea of what you need to keep in mind in regards to maintaining modules and some pitfalls you should try to avoid. If you have any questions left, if you have an experience to share or if you have feedback on this article, don't hesitate to reach out to me on Twitter.

Top comments (0)