Coupling refers to the degree of dependency between different components or modules within a codebase. High coupling can lead to code that is difficult to understand, modify, and maintain.
In this article, we will explore various strategies to reduce coupling and promote cleaner code, but first:
How to measure it ?
There is no definitive metric to measure coupling, here is one approach you can take.
Dependency Analysis: Analyze the dependencies between classes, modules, or packages in your codebase. Tools like static code analyzers or dependency visualization tools can help identify the relationships and dependencies between different components. High levels of interconnectivity and dependencies indicate a higher degree of coupling.
Tools:
JDepend
Structure101
SonarQube (in addition to static code analysis, offers a module called "Dependency-Check" that visualizes dependencies between modules, packages, and classes.)
Maven Dependency Plugin: By running the
dependency:tree
goal, you can obtain a tree-like structure that illustrates the dependency hierarchy of your project's modules and libraries.
Solutions
Tell, Don't Ask (TDA) Principle
Avoid Method Call Chains
Utilize Events
Favor Interfaces over Inheritance
Favor Composition over Inheritance
External Configuration for Parameterizing the Application
Tell, Don't Ask (TDA) Principle
One effective approach to reducing coupling is to follow the "Tell, Don't Ask" principle. Instead of exposing the internal state of an object for others to reason about, encapsulate the behavior within the object itself. By limiting external access to an object's state, we can reduce dependencies and make the codebase more maintainable.
int age = person.getAge();
if (age >= 18) {
// some java code
}
Better approach
if (person.isAdult()) {
// some java code
}
You will not always be able to apply this rule, but at least twice before you pull out data from the object to the outer world.
Avoid Method Call Chains
Chaining method calls can result in tight coupling between objects, making the code more brittle and harder to modify.
Energy.getInstance().getEnergyDropManager().getDrops();
It is fine that Energy
has a direct link to EnergyDropManager
, but think in terms of context of who is calling and everything it needs. If it needs getDrops()
for 99% of calls, you should reconsider the purpose of this method and whether it might be wiser to move it to Energy
instead.
Energy.getInstance().getDrops();
Also, consider composing functions into pipelines or thinking of code as a series of nested transformations. This approach promotes a more modular and decoupled design.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
Utilize Events
Applications that respond to events and adjust their behavior accordingly can be more flexible and adaptable in the real world.
Options
Finite State Machines,
Observer Pattern,
Publish/Subscribe,
Reactive Programming,
Streams
Favor Interfaces over Inheritance
Inheritance can introduce strong coupling between classes, leading to code that is difficult to extend and modify. Instead, favor interfaces over inheritance. Interfaces allow for polymorphism without the tight coupling associated with inheritance.
public interface Renderer {
void render();
}
public class CircleRenderer implements Renderer {
public void render() {
// Render a circle
// ...
}
}
public class SquareRenderer implements Renderer {
public void render() {
// Render a square
// ...
}
}
public class RendererApp {
public static void main(String[] args) {
Renderer circleRenderer = new CircleRenderer();
circleRenderer.render();
Renderer squareRenderer = new SquareRenderer();
squareRenderer.render();
}
}
Favor Composition over Inheritance
Classes should be designed to be composed of other classes or modules rather than relying heavily on inheritance relationships.
public interface Engine {
void start();
}
public class GasEngine implements Engine {
public void start() {
// Start the gas engine
// ...
}
}
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void startEngine() {
engine.start();
}
}
External Configuration for Parameterizing the Application
To reduce coupling and increase flexibility, consider parameterizing the application using external configuration. This can be achieved through static configuration files or as a service behind an API.
By externalizing configuration, you can modify the behavior of the application without modifying the code itself, thereby reducing dependencies and improving the codebase's adaptability.
Conclusion
Reducing coupling is essential for maintaining a clean and maintainable codebase. By following strategies such as "Tell, Don't Ask" principle, avoiding method call chains, utilizing events, favoring interfaces over inheritance, and externalizing configuration, you can effectively reduce coupling and achieve a more modular and flexible codebase. Embracing these practices will not only improve code maintainability but also improve the scalability and extensibility of your software project.
Top comments (3)
Nice recommendations! Thanks for sharing!
also on same topic dev.to/ivangavlik/transforming-bad...
thanks