In The Two Faces of Modularity & OSGi, I talked about the OSGi runtime and development models. The development model has two facets - a programming model and design paradigm - that impact how organizations will use OSGi to build more modular applications.
In Reuse: Is the Dream Dead, I talked about the failed promise of reuse. In stating that maximizing reuse minimizes use, I examined significant impediments to reuse and use - component dependencies and context dependencies.
Here, I talk about the benefits of modularity, focusing exclusively on the OSGi design paradigm. I review modularity’s relationship to reuse and touch on how modularity helps ease maintenance and improve extensibility. To wrap up, I present some modularity patterns that help balance module weight and granularity and make modules more reusable and useable, while also making the system easier to understand, maintain, and extend.
Logical and Physical Design
Almost all principles and patterns that aid in software design and architecture address logical design. Identifying the methods of a class, relationships between classes, and a system package structure are all examples of logical design. Since most principles and patterns emphasize logical design, it’s no surprise that the majority of developers spend their time dealing ony with logical design issues. Other examples of logical design include deciding if a class should be a Singleton, determining if an operation should be abstract, or deciding if you should inherit from a class versus contain it. Developers live in the code, and are constantly dealing with logical design issues.
Making good use of object oriented design principles and patterns is important. Accommodating the complex behaviors required by most business applications is a challenging task, and failing to create a flexible class structure can have a negative impact on future growth and extensibility. But logical design is just one piece of the software design and architecture puzzle. The other piece of the puzzle is physical design. Physical design represents the deployable units composing a software system. Physical design is equally important as logical design, and physical design is all about modularity.
Benefits of Modularity
Almost all discussions on modularity mention reuse as a prime advantage. But there are other advantages, of course. There is a reduction in complexity that helps ease the maintenance effort. Cohesive modules encapsulate behavior and expose it only through well-defined interfaces. Because modules are cohesive, change is isolated to the implementation details of a module. Because behavior is exposed through interfaces, new modules containing alternative implementations can be developed without modifying existing modules. There are other benefits to modularity that extend beyond design to runtime, too. But from the design perspective, modularity helps increase reuse, ease maintenance, and increase extensibility.
Time for an example. Assume we define an interface to decouple client classes from all classes implementing the interface. In doing this, it’s easy to create new implementations of the interface without impacting other areas of the system. The principle surrounding this idea is the Open Closed Principle - systems should be open for extension but closed to modification. Logical design makes extending the system easier, but it’s also only half of the equation. The other half is how we choose to modularize the system.
Let’s assume the interface we’ve created has three different implementations, and that each of the implementation classes have underlying dependencies on other classes. We’re faced with a contentious issue. On one hand, grouping all the classes into a single module guarantees that change is isolated to only that module (ie. easier to use and maintain). If anything changes, we’ll only have one module to worry about. Yet, this decision results in a coarse-grained and heavyweight module (ie. harder to reuse), and a desire to reuse a subset of that module’s behavior leaves us with one of two choices. Duplicate code or refactor the module into multiple lighterweight and finer-grained modules. In general, logical design impacts extensibility, while physical design impacts reusability and useability. Well…this is only partially true.
As we refactor a coarse-grained and heavyweight module to something finer-grained and lighter weight, we’re faced with a set of tradeoffs. In addition to increased reusability, our understanding of the system architecture increases! We have the ability to visualize subsytems and identify the impact of change at a higher level of abstraction beyond just classes. In the example, grouping all classes into a single module may isolate change to only a single module, but understanding the impact of change is more difficult. With modules, we not only can assess the impact of change among classes, but modules, as well.
Unfortunately, if modules become too lightweight and fine-grained we’re faced with the dilemma of an explosion in module and context dependencies. Modules depend on other modules and require extensive configuration to deal with context dependencies! Overall, as the number of dependencies increase, modules become more complex and difficult to use, leading us to the corollary we presented in Reuse: Is the Dream Dead:
Maximizing reuse complicates use.
Creating lighter weight and finer-grained modules increases reuse but also increases module and context dependencies, while creating fatter modules decreases dependencies but also decreases reuse. Modules that are too lightweight provide minimal value and may require other modules to be useful. Modules that are too heavyweight are difficult to reuse because they do more than what the client needs. Of course, there are other challenges beyond reuse.
Coarse-grained and heavyweight modules may do a good job of encapsulating change to a single module, but understanding the impact of change is more difficult. Conversely, fine-grained and lightweight modules make it easier to understand the impact of change, but even a small change can ripple across many modules. How do we deal with this problem, especially for large software systems where these challenges are even more pronounced?
Large software systems are inherently more complex to develop and maintain than smaller systems. In addition to increasing reuse, breaking a large system into modules, makes the system easier to understand. By understanding the behaviors contained within a module, and the dependencies that exist between modules, it’s easier to identify and assess the ramification of change.
For instance, software modules with few incoming dependencies are easier to change than software modules with many incoming dependencies. Likewise, software modules with few outgoing dependencies are much easier to reuse than software modules with many outgoing dependencies. This tension surrounding module weight and granularity is an important factor to consider when designing software modules.
Today, frameworks like OSGi aid in designing modular software systems. While these frameworks can enforce runtime modularity, they cannot guarantee that we’ve modularized the system correctly. Correct modularization of any software system is contextual and temporal. It depends on the project, and the natural shifts that occur throughout the project impact moduarlity. I examine our willingness to embrace this idea when discussing Agile Architecture.
Modularity patterns provide guidance and wisdom in helping design modular software. They explain ways that we can minimize dependencies while maximizing reuse potential. They help balance module weight and granularity to make a system easier to understand, maintain, and extend. For those who have attempted to design modular software, it’s common for modularity to drive the logical design decisions, which I briefly explained when discussing the SOLID design principles.
Below are a list of modularity patterns that I’ve used on past projects. These patterns also build atop proven object-oriented design concepts. Right now, the list only includes the pattern descriptions, and provides no detailed explanation. Hopefully, you can infer from the name and brief description the general intent of the pattern. Some may be intuitively obvious, while others less so. In the meantime, I welcome your feedback, your questions, and your criticisms, as I work to provide more detail going forward.
- ManageRelationships – Design Module Relationships.
- ModuleReuse – Emphasize reusability at the module level
- CohesiveModules – Create cohesive modules.
- ClassReuse - Classes not reused together belong in separate components.
- AcyclicRelationships - Module relationships must be acyclic.
- LevelizeModules – Module relationships should be levelized
- PhysicalLayers - Module relationships should not violate the conceptual layers.
- ContainerIndependence - Consider your modules container dependencies.
- IndependentDeployment - Modules should be independently deployable units.
- CollocateExceptions - Exceptions should be close to the classes that throw them.
- PublishedInterface - Make a modules published interface well known.
- ExternalConfiguration – Modules should be externally configurable.
- ModuleFacade – Create a facade serving as a coarse-grained entry point to the modules underlying implementation.
- StableModules - Modules heavily depended upon should be stable.
- AbstractModules - Depend upon the abstract elements of a module.
- ImplementationFactory - Use factories to create a modules implementation classes.
- SeparateAbstractions - Separate abstractions from the classes that realize them.
- LevelizedBuild – Execute the build in accordance with module levelization
- TestComponent – For each module, create a corresponding test component that validates it’s behavior and illustrates it’s usage.