Modifiability: Ability of a system to be able to admit changes due to a new requirement or by detecting an error that needs to be fixed.
The changes can happen due to:
- New functionalities
- Change functionalities
- Remove functionalities
- Fix issues
- Strengthen security
- Improve user experience
- Incorporate new technologies
- Incorporate new platforms
- Incorporate new protocols
- Incorporate new standards
Questions an architect would make when starting to consider modifiability as a matter to be solved or instead of taking into account:
- What can be changed? You have to consider all the aspects of the changes the system could have.
- What is most likely to change? The architect has to decide which changes are most likely to happen and which are not.
- When will the changes be made and who will be in charge?
- What is the cost of the change?
To understand modifiability, it is essential that coupling and cohesion be understood.
Coupling and Cohesion
When a change is made in a module, what is the impact on the rest of the system? How strong are the responsibilities each module shares? In order to answer these questions, the concept of coupling arises. Coupling measures how the responsibilities of the modules are related. In other words, when changes are made in a module, coupling measures how the functionalities in the other modules are affected.
In a module, we handle different functionalities, how much those functionalities are related to each other? That is what cohesion is about.
The approach we are going to follow to help with coupling and cohesion is with the use of tactics.
Which Tactics?
- Those that divide modules and thus reducing the cost of making changes.
- Others reduce the coupling between the modules. It generally involves the use of intermediaries between the modules.
- And in case of low cohesion, those that are to remove responsibilities that are not affected by anticipated changes and put those removed functionalities in the appropriate module (which is refactoring).
Tactics
Tactics lists:
Reduce the Size of a Module
Split Modules -> When a module has many capacities, any change has a high cost. Refining a module into several smaller modules will make it less difficult to make changes in it.
Increase Cohesion
Increase Semantic Coherence -> It is important to maintain semantic consistency in modules, this may involve moving functionalities that do not have the same purpose from one module to another or creating a new module with those functionalities.
Reduce Coupling
Encapsulate -> Add interfaces to a module and that the interaction of the outside with it is through these interfaces.
Using an Intermediary -> When you want to break the dependency between two responsibilities A and B, you put an intermediary in the middle.
Restrict dependencies -> It consists of restricting the visibility of modules with respect to others; an example is a layered architecture and how the different modules interact.
Refactor -> Involves duplicate code removal, overly complex code cleanup, moving code from one place or another, and more.
Abstracting Common Services -> If that functionality is replicated in different places of our code, this technique suggests centralizing the code in one place and in an abstract way.
Defer Binding
Deferred Binding is how functionality can change its features accordingly to the context in which it is running.
Some ways to do that is using parameterization:
Adding parameters in a method -> a method changes its functionality accordingly to the parameters.
Properties files -> we can use files to change how our program behaves, for example connections to other components or modules, also change starting variables and more.
Java Solutions
Already presented the different tactics to improve modifiability, now we will introduce how some of them can be implemented in java.
Split Modules
Java gives you the freedom to organize the classes in namespaces called packages, the elements that are in them have some visibility to each other, and also have common functionality. JARS also allows you to group packages given a common functionality.
Using packages, we can create modules that organize the code in the desired way.
Encapsulate
Java allows you to use interfaces as a way to hide the details of the implementation. Mainly one use may be to separate the implementation of a service from which consumes it by providing only one interface.
We mentioned above that using packages makes it easy to encapsulate as well as the access modifiers, the ability to bundle packages with JARS and design patterns that expose an interface and hide the implementation. Some of those patterns are:
Pattern Adapter
Pattern Iterator (Java has a standard library considering that pattern)
Pattern Facade
Using an Intermediary
In java, there are several solutions and examples to break the dependency between consumer and producer. Dependency injection and solutions for publish-subscribe are some of them.
The facade pattern is widely used to help with the separation of dependencies between responsibilities. Some frameworks make things easier with dependency injection and control inversion favoring encapsulation and decoupling. Spring is one of those frameworks that collaborates with the wiring of our system, dependency injection, and various other things.
For publish-subscribe problems, there are several solutions like ActiveMQ, RabbitMQ, or Kafka that can be implemented with java.
Restrict dependencies
As previously explained, this tactic is commonly seen in layered architectures, where the system is separated into specific functionalities.
Spring Framework is commonly used for the creation of layered Architectures because it helps with the configuration, dependency injection, connection to the database, exposing services, and more.
Refactor
There are several techniques to refactor code, below some of them:
- Rename
- Move class
- Extract Method: For example, when a very long method is divided into smaller methods, we extract methods from its code.
- Extract Superclass: when we centralize the common code in an abstract class and the specific code in classes that implements it (Polymorphism).
- Replace Conditionals with Polymorphism.
Clearly, Java is a very diverse language with lots of solutions. Here only some solutions were presented (in other words, they are not the only ones) to improve modifiability.
Bibliography:
- Bass, Len. Software Architecture in Practice (SEI Series in Software Engineering). Pearson Education.
- http://wiki.uqbar.org/wiki/articles/atributos-de-calidad.html
- https://en.wikipedia.org/wiki/Java_package
- https://www.adictosaltrabajo.com/2017/10/30/modularidad-en-java-9-12/
- https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
- https://medium.com/java-vault/layered-architecture-b2f4ebe8d587
- https://www.methodsandtools.com/archive/archive.php?id=4