Tuesday 24 October 2023

Modularisation

Application modularisation has benefits in both architecture and developer performance. It also will add some new problems to solve, and pitfalls to avoid.

Encapsulation

We separate our code by namespaces, package names and or folders. Hierarchies are built to represent the different layers of our application and provide visual and logical distinction. With the basic scopes that can apply to variables, functions and classes we can set an element to be part of the private implementation details of the class, or public to the rest of the application.

Moving code into a module allows a new scope to be applied that marks an element public to just the module, but not the entire application.

Consider the following example:

As an application grows the need to create and enforce separation between layers grows. Consider the following layered example:

Service layer > Repository layer > Business logic layer > UI layer

The service layer contains specialised code to talk to external services like API endpoints and local database.

The repository layer will request and receive data from different services in the service layer, and expose domain models for consumption.

The business logic layer will consume the repository layer data and modify it as necessary to meet the business needs of the application. Here the intentions become more oriented around user tasks, rather than talking with different external processes like databases and API endpoints.

The UI layer takes the output of the business logic layer and presents it to the user, passing back user actions and inputs to the business logic layer for further mutation.

The above example defines clear boundaries and communication paths between layers. In the original model, any code marked public is available to the entire application. Allowing large teams moving quickly to easily break the layered model and incur technical debt, bugs and wasted time. Using well designed modules enforces good code separation, and prevents leaking of library specific code into other modules that do not reference that library. All caught at compile time.

Readability

Developers will spend much more of their time reading and understanding code, than writing it. When writing code, spend some extra cycles ensuring it can be read and understood quickly by a fellow developer. The code will be written once, but then read and maintained many more times.

Code separated into visually distinct and well documented and understood modules will help a developer understand the overall structure of an application quickly, and help them find the parts they are interested in.

Build times

Modern build systems are much more clever than the old brute force approach of 'rebuild entire application for every development code change'. Commonly the line is drawn at rebuilding the entire module for each code change. If all the code is in one single module, then much more rebuilding, code generation, packaging and deployment steps are needed for each code change. Some libraries can also add many more steps to the build process, generally code written with these libraries tends to be more stable, and can be confined to a module not often modified.

Migrating to modules ensures that only the most minimal parts of the application are rebuilt, saving developers time that would have been wasted on each and every build. (Which we do a lot of if you're a non developer reading this)

Design

Designing which modules to create, and how they interact and depend on each other is just as much an art as it is a science. Every code base is different, but here is the approach I tend to prefer.

For really small projects that are ephemeral I use a single module. Don't over architect when you don't need it.

For medium size projects, or small projects that need to be worked on for a longer time I create minimal modules to separate my layers and encapsulate implementations and libraries. One module for each layer mentioned in the earlier example provides this, it won't scale with the number of features I add, and the temptation is always there to take shortcuts, but overall the positive effects are worth the effort.

For large and extremely long term projects I prefer to build a hybrid module system. Feature modules will work alongside library encapsulation layer modules such as API and Database. This also decouples a feature from the lower repository layer.

Communication

For my designs, communication between modules is usually done via an indifferent mediator like local database that can be observed (rather than pinged for updates), and common navigation destination names can be shared throughout the application, with the actual implementation tucked safely away inside a module. Feature module A, can try to navigate to Feature module B by asking the core components to navigate to the concept of Feature B, and core will then call Feature B directly. This does require the core to be aware of all feature modules, but that is already required by advanced dependency injection systems like Android Hilt.

Balance

There is a non zero amount of overhead associated with each module, and this will build up over time in a tragedy of the commons / death by a thousands cuts kind of way. There may be extra mapping required to more common models in order to share data. There will certainly be extra steps to trace issues and longer call stacks when the number of modules increases in a project. 

Balance must be struck when deciding on a module pattern to use. Split by features and it will scale with the app well (no single module will get too big), but all the layers are now split across each feature and you lose the encapsulation. Split by layer, and features become spread out across all modules. The modules will continue to grow in size and complexity along with the application and become unwieldy. My advice is to brainstorm with the team and try out different ideas to see how they feel to code, maintain and understand.

Conclusion

Modularisation is a beneficial practice to any code base. It is best applied at the beginning of a project, but it can be a beneficial exercise to an existing mature code base as well. It forces the frontend developer who is normally used to the mindset of implement the minimum code needed for my current task, to take a step back and consider the application as a whole. The benefits will almost always out weigh the investment needed to begin using multiple modules.

No comments:

Post a Comment