In this article, we explore the concept of modularity in software architecture and its importance. We’ll discuss how to calculate its key components - cohesion, coupling, and connascence - and their significance in assessing architectural quality.
Modularity in software architecture refers to the practice of dividing a system into separate, independent modules that encapsulate different functionalities.
At its core, modularity involves organizing a system into cohesive units, each responsible for a specific aspect of the overall functionality. These modules are designed to be self-contained, with well-defined interfaces for interaction with other modules. For example, in a banking system, a module handling financial transactions could be separated from a module responsible for managing customer accounts.
Such a modular structure allows for flexible software design, testing, and development, enhancing scalability and maintainability. However, to reap the full benefits of modularity, an architect must also consider aspects such as cohesion, coupling, and connascence, which impact the efficiency and flexibility of the system. Thus, modularity becomes a crucial element in creating modern and sophisticated information systems capable of meeting dynamic market demands.
Dividing a software system into modules and establishing clear boundaries between them is a crucial aspect. Effective module division and boundary establishment promote maintainability, scalability, and flexibility. Here are some strategies for achieving this:
Follow the SRP, which states that a module should have only one reason to change. Identify cohesive functionalities within your system and group them together into modules. Each module should encapsulate a single, well-defined responsibility. For example, in a web application, you might have separate modules for user authentication, order processing, and product management.
Apply the SoC principle to divide your system into distinct concerns, such as data access, business logic, and user interface. Each concern should be addressed by a separate module, allowing for easier maintenance and scalability. For instance, in a web application, you could have separate modules for handling database interactions, business rules, and frontend presentation.
Utilize DDD to identify the core domains of your application and model them as separate modules. Define bounded contexts for each domain, establishing clear boundaries that prevent domain logic from leaking into other parts of the system. For instance, in an e-commerce application, you might have separate modules for managing products, orders, and customers.
Design module interfaces with the ISP in mind, which states that clients should not be forced to depend on interfaces they do not use. Define minimal and focused interfaces that cater to the specific needs of each module, avoiding unnecessary dependencies. This allows for better encapsulation and reduces the impact of changes on other modules.
Apply the DIP by decoupling modules from their concrete implementations and depending on abstractions. Use dependency injection to inject dependencies into modules, allowing for greater flexibility and testability. This promotes loose coupling between modules and makes it easier to replace implementations without affecting the rest of the system.
Consider dividing a module when it becomes too large or when it violates the single responsibility principle (or other form the above rulesl depending of which you are using). However, avoid dividing tightly coupled modules, as this can lead to increased complexity and reduced maintainability.
Trying to split a coherent module would only increase coupling and decrease readability. ~Larry Constantine
Modularity includes cohesion, coupling and connascence.
Together, these three factors contribute to the modularity of an architecture, affecting its flexibility, maintainability and scalability. Understanding and quantifying these factors is essential for architects to effectively manage and control the current state of the architecture
Let’s take a look at each of them!
Cohesion measures the degree to which elements within a module belong together. High cohesion implies that elements within a module are closely related and contribute to a single, well-defined purpose.
Let’s explore some types of cohesion, starting from the highest level.
Methods in a module perform a common, logical task or operate on a common set of data. This is the highest level of cohesion, where all methods in the module collaborate to achieve one well-defined task.
Methods in a module are called in a specific sequence, where the output of one method is used as input for the next method. This is sequential cohesion, where methods are linked by the sequence of their invocations.
Methods in a module perform similar tasks and often exchange data among themselves. Communicational cohesion occurs when methods in a module are linked by data exchange.
Methods in a module perform similar tasks, but not necessarily in a logically related manner. For example, a module containing methods for data processing, where each method performs different operations on data but there’s no logical connection between them.
Methods in a module are invoked at the same point in time, but not necessarily to perform related tasks. Temporal cohesion is when methods are linked by the time of their invocation, but not necessarily by their functionality.
In this case, methods in a module perform related tasks that are logically connected. However, these methods may vary in terms of specific operations, which can affect code efficiency and comprehensibility.
This is the lowest level of cohesion, where methods in a module perform different, unrelated tasks. It results from a random collection of methods in a module that lack logical connection. Modules with coincidental cohesion are difficult to understand and maintain.
Lack of Cohesion in Methods measures the lack of cohesion within a class. LCOM version 1 is the simplest formula for calculating consistency. It is useful for quick and basic calculations in uncomplicated cases.
It is calculated as the difference between the number of method pairs that do not share any instance variables and the number of method pairs that do share instance variables. Mathematically, LCOM can be expressed as:
Let’s consider a class with 5 methods. Among these methods, 3 pairs of methods share at least one instance variable (|P| = 3), and 2 pairs of methods do not share any instance variables (|Q| = 2).
Substituting these values into the formula - |3| is greater than |2|, so:
In this case, LCOM equals 1. This value indicates some cohesion in the class, as there are more pairs of methods sharing at least one instance variable than pairs of methods not sharing any instance variables.
Interpretation of LCOM1 values:
LCOM 4 is preferred over LCOM 1 because it considers method dependencies, providing a more nuanced understanding of method cohesion, especially in complex class structures. By accounting for all dependencies between methods, LCOM 4 offers a more accurate assessment of method cohesion, reflecting real-world software design practices more effectively.
LCOM in version 4 can be calculated using the following formula:
This formula takes into account combinations of vertex degrees in the method dependency graph to calculate LCOM 4 for the class.
Take look at our example:
This allows us to say what the values of and are:
Based on diagram, we get:
method1:
field1, field3, and field4.field3 and field4 is shared within method1.method2 references field2 and field3.method2 does not share any pairs of fields.method3 references field3 and field4.field3 and field4 are shared within method3.And, we can go to the calculations:
The value of for this example is . The closer the value is to zero, the better organized the code is.
High Cohesion: If the result is close to zero or low, it indicates that the methods in the class are strongly related and collaborate with each other. This corresponds to a situation where most methods in the class operate on the same data fields and collaborate with each other.
Low Cohesion: If the result is high, it suggests that the methods in the class are weakly related and operate in isolation. This may suggest that methods in the class operate on different sets of data and do not collaborate with each other, leading to difficulties in understanding and maintaining the code.
Refers to the degree of interdependence between modules. Low coupling indicates that modules are relatively independent of each other. High coupling means that a change in one module will likely necessitate changes in another, while low coupling means that modules can change independently of one another.
Afferent coupling refers to the number of incoming dependencies to a module or component. It indicates how many other modules or components rely on a particular module. High afferent coupling means that many other modules depend on a given module, suggesting that the module is highly reused or serves as a core part of the system.
Key Points:
Efferent coupling refers to the number of outgoing dependencies from a module or component. It measures how many other modules a given module depends on. High efferent coupling means that a module relies on many other modules, suggesting that it may be more complex and harder to understand in isolation.
Key Points:
Consider a class A that provides utility functions used by many other classes (high afferent coupling), and another class B that needs to use several external libraries to perform its operations (high efferent coupling).
In the JavaScript world, there are several tools available that can measure coupling or at least analyze dependencies, including:
But! We can also calculate the coupling manually, for instance, by using the CBO.
Coupling Between Objects (CBO) is a metric used to measure the degree of interdependence between classes in an object-oriented system. It helps in understanding how changes in one class might affect other classes. High coupling indicates that classes are highly dependent on each other, which can reduce modularity and make the system harder to maintain.
CBO is primarily associated with object-oriented programming (OOP), but the concept of coupling is relevant to other programming paradigms as well.
In non-OOP paradigms, coupling still refers to the degree of interdependence between software modules. For instance, in procedural programming, functions or modules may have dependencies on each other, affecting modularity and maintainability.
Consider a simplified e-commerce system with the following classes and relationships:
OrderPaymentInventoryCustomerExample relationships:
Order class uses Payment and Inventory classes.Payment class uses Customer class.Customer class uses Order class.CBO Calculation:
Order class:
Payment, InventoryPayment class:
CustomerInventory class:
Customer class:
OrderAbstractness is a metric used to quantify the proportion of abstract elements in a component or package. It provides insight into how flexible and extensible a module is.
Abstractness ((A)) is calculated using the formula:
The abstractness value ranges from 0 to 1:
Consider a package with the following types:
First, identify the number of abstract types :
Next, determine the total number of types :
Now, calculate the abstractness :
This result indicates that approximately 56% of the types in this component are abstract.
Remember: Code with too little abstraction is difficult to maintain, and code with too much abstraction is weak, easy to break.
Connascence is a concept introduced by Meilir Page-Jones to describe the relationship between software components based on how closely related their elements are. It focuses on the degree of correlation or interdependence between elements rather than the structure or type of dependencies.
Connascence comes in different forms, from the least severe (easiest to remove) to the most severe (hardest to remove):
While both connascence and coupling relate to dependencies between software components, they focus on different aspects:
Coupling measures the degree of interdependence based on the structure, data exchange, control flow, or timing between modules. Connascence describes the degree of correlation or interdependence between elements within or between modules based on their meaning, name, position, timing, algorithm, or value.
In this article, we discussed the key elements of software architecture modularity: cohesion, coupling, and connascence. Understanding these concepts and their calculation methods is crucial for architects to manage architectural quality effectively.