The modular architecture consists of a set of functions that do not directly depend on each other. They just know how to communicate and interact, but it is quite simple to change each structural unit – like in a puzzle. That’s an advantage it has over monoliths.
Let's replace the concept of a piece of the puzzle with a framework. Each separate implemented functionality will be a collection of code for solving a problem integrated into the project as a framework.
This is going to be a root project that hosts the dependencies of other projects/frameworks.
In this example, ModularApp is the root project, and CoreModule, NavigationModule, ServerApiModule, and Onboarding are the particles that make up our product.
The number of modules and their content may vary in different projects. The first three modules are needed at the start of any project. Therefore, unlike the Onboarding module, they are always used in our projects.
We’ll examine these modules in detail below. But first – a quick tutorial on how to set up your work.
1/ Create a project with the “Framework” template.
2/ Deploy the repository where the project will be located.
Arrange the correct access levels for your team. The developer of the module must have an "owner/maintainer” access level. Developers who will use this module in their projects should remain “developers”.
Working with modules is very convenient in teams. Each team member can be responsible for his own module or use a ready-made module previously implemented in another project simply by changing its properties. It’s convenient to always have a person who can conduct a code review and knows the value that this code brings.
Merge to master only after pull request & code review.
3/ Start developing your code (describing the functionality of the module). Read the detailed examples below.
4/ To evaluate and test the code, create a test project, add your framework, and bring it to the desired level. The folder structure will be as follows (BaseApp V2 is a project where I am testing framework):
5/ After we have filled in our modules with code, it is necessary to describe all the functionality in the read.me file. Basic sections include Structure, How to use, Recommendations.
The integration of our module into the project can be done via a standard set: Swift Package Manager, Manual .framework, Carthage, CocoaPods. When choosing a provider, it is necessary to consider some factors:
Now that you know how to work with modules let’s explore the stuffing. We’ll show how we coded our NavigationModule, CoreModule, and ServerApiModule.
NavigationModule has a very simple structure: Navigation Router with a Navigation Module inside, Navigation Controller, and Navigation Module View Controller. Navigation Module can branch into additional Navigation Modules.
All of these parts communicate via NavigationModel, which consists of the initiating View Controller, and functions as a builder of Navigation Controller.
NavigationRouter operates on the Window level. It impacts the current module and changes the navigation protocol of the whole application.
NavigationModule is the object operating on the line of the current navigation. It communicates with NavigationRouter via Delegate and keeps the current Navigation Controller in memory, as well as NavigationModel’s array. If NavigationModel is an isolated case, then linear navigation is built. Otherwise, we create a side-bar or a tab-bar Controller.
As I already mentioned, navigation should remain simple, and modules should be easy to use.
We only need three points for a successful launch:
Here’s the visualization of the process: the object receives Window level, NavigationModel generates Navigation Controller (in this case, for linear navigation), which, in turn, initializes NavigationModel, and, as a result, the embedded View Controller performs navigation inside the application.
Importantly, there's cross-cutting access across all the levels. It always comes in handy: for example, while working with push alerts, we always know where to display them or how to make changes.
This is a very simple CoreModule we created to:
Let’s take a look at a simple example: ButtonStyleProperties.
We take all the changeable things out into it. Our designers provide a single file in Figma with all the styles and properties used, so that we easily transfer it into the project, and then visualize through xib files how everything will look like on the devices.
The idea behind this one was to reuse the existing code and try testing DI in our modules.
Making a call is fast and easy:
Request is an object including endPoint, Method, parameters, and headers:
In practice, our WeatherRequest consists of endPoint, a determined .get Method, query, appid, and the resulting WeatherObject.
When designing a module, keep in mind that the internal implementation can be complex or simple, but the use of the module should be easy to understand.
One of the advantages of modularity is that inside we can use any architecture, any third-party libraries, and this will not break our main project. When initializing a project, we know which modules will be used and how they will communicate with each other, so the control point is exactly the place where the module is glued and not its interior.
Learn about other advantages of modular architecture in the next part of our Modular Architecture Overview. Pros and cons, and revelations from our experience – find out if it’s worth it!