Modular Software Architecture
Express and easily manage complex dependencies between classes and components within a single layer, by grouping them into Modules.
The most popular and common 3 layer software architecture was created from the need to concise and clarify the dependency graphs between classes and components. Had we kept just a single layer for the whole application, those graphs would be unmanageable and tangled. It would be impossible to define any rules about the importance of one component over the others. It would also be impossible to define the 3 different responsibility classifiers which are explicitly expressed by the 3 layers. When we belong a class to the layer, we explicitly denote it to solve one of these general responsibilities.
Very similarly, in many of the modern software projects, layers have become very large themselves, as they contain lots of classes and those depend on each other. Sometimes this dependency graph is so complex that it naturally calls for splitting layers into more granular sub-layers. Many brave developers have gone a far way by being creative about this, and have introduced many new layers in their applications. But at some point this approach becomes somewhat arguable - can there really be so many layers? If so, then why do these all books talk about 3 layers only? and if not, then how do we manage the complex dependency graphs within a large layer?
Solution - Module
If we simply want to group interrelated classes together, there is no necessity to introduce yet another layer. Instead, we can introduce a Module.
I call group of classes a Module, which represents some functionality of the application. I use term "component" to represent a class or any other element (e.g. structure or enumeration) which is grouped within a module. Thus, module consists of components. Module can be considered a more granular layer of the application.
Modules come with many similar characteristics to those of a layer (detailed relationships diagram is below):
- Module can depend on other modules, similar to how a layer can depend on other layers. e.g. ModuleA depends on ModuleB.
- Component can depend on other components, if its containing module depends on the module containing the other components. This is similar to dependencies spanning across layers. e.g. ComponentA1 can depend on ComponentB1 if ModuleA (container of ComponentA1) depends on ModuleB (container of ComponentB1).
- Module defines the allowed dependencies for its components, similar to how a layer is not allowing its elements to depend on those residing in the layers above. e.g. ComponentB2 cannot depend on ComponentA2, because ModuleB (container of ComponentB2) does not depend on ModuleA (container of ComponentA2). Since the dependency direction is the other way around between the container modules, ComponentA could depend on ComponentB, should it choose to do so.
- Ability to define dependency directions between modules is the instrument used to avoid circular references between them (just like we can't and shouldn't have circular references between software layers). This helps great deal in complex software projects when the class dependency graph within a project becomes tangled and hard to understand, with many circular references (bare classes do allow circular references!). With modules, this headache is gone - you have to follow the right design due to the module dependency constraints. As a result, we have a clean class (i.e. component) dependency graph out of the box.
- Component dependencies within a module are allowed, similar to dependencies within a layer. e.g. ComponentA1 can depend on ComponentA2 since they both belong to the same module.
All these relationships are expressed below.
Additional Benefits of Module
Modules come with additional benefits, which we can't really enjoy with layers.
- As opposed to the layers, modules can have one to many relationships. i.e. a single module can depend on several other modules. While layers can do the same, it's not generally a good idea, because then replacing the layers at the bottom is not so easy, and then the benefit of the layer separation is somewhat lost. It's a rule of thumb that the layer should only speak to the layer just below it.
- Each module is a functionality, a feature, which is not generally true about layers. Layers usually consist of many different features, which can have complex dependencies among them. Module can avoid such complexities by splitting further into several modules, and by clearly defining dependencies among these newly born modules. Layers cannot be split into new layers indefinitely (that's where we started at the beginning this article).
- In feature rich applications, we can now focus on features (by representing each as a module), rather than focusing layers (which mean nothing to domain experts) or relationships between classes (which may not be the level of granularity that domain experts understand). We can visually draw dependencies between existing features, as well as plan building out a new feature by finding the right place for it in the graph of existing ones.
- We can be creative with modules by allowing to turn them off. This must be accompanied with different classifications of the module dependencies, to avoid breaking the whole system when a single module is off. e.g. dependency can be weak, which means that the dependency module may be off but the dependent module can tolerate it. This can eliminate the need for commonly used feature toggle pattern, when the code becomes bloated with IF conditions wrapping code blocks, based on a flag which commonly comes from configuration files. With properly designed module dependency system, feature toggles become implicit to the developers, as long as the code can tolerate null references to the components from the weakly referenced modules.
- Opportunities are endless.
Modular Architecture Implementations
Quite unfortunately, applications supporting plugins are referred to as modular applications. That's because those applications support loading new plugins dynamically, as long as they all follow a unified interface structure expected and executed by the application. I'd rather call those plugins, since the uniformity of the interface is not so much specific to the modules, although it can be implemented for modules too.
This confusion is partly guilty in very small number of application frameworks supporting modules as they are explained in this article. Almost all papers talking about modular architecture explain plugins (not modules) and how to dynamically add and load new plugins into the existing application.
Some of the existing frameworks (e.g. require.js) have used module to denote something different than what I explained here.
So far, only angular.js's Module stands closest to the module explanation that I provided above. However, it's a complete application framework, and the choice is "all or nothing" (if you use angular, you need to use modules). Also, importance of the module is either not well explained or not well understood, since eventually developers end up having a single giant angular module, depending on all other existing modules in the application. On the other hand, I expect that the application does not necessarily need a module, and it can be defined only when the necessity exists (not "all or nothing" approach). For simple layers, modules will not be needed. For complex and evolving software layers, modules will be very nice to have.
I still can't name a framework supporting modular architecture for strongly typed languages such ac C# (my favorite). I hope it's somewhere there, and it's just I haven't found it yet.