Let’s talk about some of the fundamental software design principles, which are typically applied behind the scenes by designers.
Software architecture represents the result of a sequence of design decisions which take place over time as long as software system complexity increases.
For the sake of clarity, let’s define an architecture as a collection of components combined together via connectors, which represent constraints on how components interact.
Tackling Software Erosion
Technical debt results from architecture constraint violations, which create gaps between the planned architecture and the corresponding code implementation.
In order to mitigate this phenomenon, software architecture must clearly describe constraints on how components can exchange information.
Component Boundaries, and Interfaces
Constraints are described by clear component boundaries, which indicate that internal data and functionalities of components are hidden to the rest of the architecture.
In addition to this, component interactions are forced to take place at restricted data exchange areas across component boundaries, namely interfaces.
These interfaces indicate clear constraints on what data information are allowed in and out of the corresponding components.
Example of Constraint Violation
Let’s consider a layered architecture, where each component is constrained to only use information provided by the component immediately below it.
Any source code component that does not observe this constraint represents an architecture constraint violation, which must be corrected ASAP.
If such violations are not corrected, they can transform the architecture into a monolithic block which is difficult to maintain and extend.
As opposed to the concept of monolithic architecture, let’s introduce modularity.
Often undervalued by developers, modularity is fundamentally what architecture design choices should be supposed to be aiming at.
Let’s start by introducing these concepts of coupling and cohesion in order to see what modularity means in its essence.
Coupling and Cohesion
In a software architecture,
- Coupling refers to multiple components, particularly to how the different components are dependent on one another, whereas
- Cohesion refers to a single component, particularly to how internal functionalities of components are closely related to one another.
Loosely Coupled and Highly Cohesive Components
A loosely coupled architecture is one in which each of its components has, or makes use of, little or no knowledge of internal information about other components.
In other words, if there is minimal co-dependency between different components, then we say the architecture is loosely coupled.
On the other hand, a highly cohesive component is made of a set of functionalities which are strictly related to one another.
Typically, a highly cohesive component follows the one-thing-done-well principle. See “Unix Philosophy with an Example”.
About Maintainability, Extendibility, and Reusability
Let’s see how loose coupling and high cohesion increase software quality and reduce costs by improving maintainability, extensibility, and reusability.
Since loosely coupled components are poorly co-dependant, while functionalities of highly cohesive components are strictly correlated:
- It’s easy to understand a given source code component, because there’s no need to analyse other code outside of the component at hand.
- Modifications on a given source code component will seldom affect other code outside of the given component, which also makes the architecture more robust.
- Components can be easily set apart and reused in other architectures, because their functionalities are typically put together so as to solve a very specific sub-problem.
The latter also means components can be easily tested in isolation without the need of reproducing their architectural context.
Loose Coupling and High Cohesion are Correlated
High cohesion relates to loose coupling and vice versa, thus an architecture made of highly cohesive components also exhibits loosely coupled components.
That said, we may question the utility of keeping in mind both loose coupling and high cohesion when designing an architecture.
In fact, due to this correlation, it should be sufficient to design architecture components by keeping in mind only one of such two characteristics.
We could just design a set of highly cohesive components, without taking loose coupling into account, and then put them together. The resulting architecture should be loosely coupled per se.
However, it’s generally a good idea to take into account both loose coupling and high cohesion when making design choices, let’s see why.
Software Architecture Decomposition
Let’s start by defining architecture decomposition as an iterative design process which is aimed at decomposing large and complex systems into smaller and specialised components.
All in all, decomposition consists of separating closely related system functionalities by encapsulating them into distinct, and loosely coupled, highly cohesive components.
Internal component functionalities and data remain totally isolated and hidden from other components. The only way components can interact with one another is via interfaces.
Inseparability and Non-Extensibility of Functionalities in Highly Cohesive Components
So why should we keep in mind both loose coupling and high cohesion while decomposing a system, even though they seem to be equivalent? Isn’t it enough to only take into account high cohesion?
First of all, when we split a set of functionalities into two or more distinct components, we need to check for potential component co-dependencies which this separation could imply.
Let’s say that functionalities in highly cohesive components are both inseparable and non-extensible, which are two principles that can be exploited when decomposing a system.
It follows an explanation on how both inseparability and non-extensibility can help designers to decompose a system into loosely coupled and highly cohesive components.
Suppose we are given two loosely coupled components A and B each one consisting of a set of respectively n and m highly cohesive functionalities fA1, fA2, …, fAn, and fB1, fB2, …, fBm.
Such functionalities in A and B are both inseparable and non-extensible, which means we can’t remove functionalities from A and put them into B or vice versa, let’s see why.
Suppose we arbitrary remove one functionality from A, say fAn, and put it into B. The resulting two components A* and B* will consist of n-1 and m+1 functionalities, respectively.
In particular, we will have A* made of fA1, fA2, …, fAn-1, and B* made of fB1, fB2, …, fBm, fAn. Let’s see why fAn is inseparable from A, and functionalities in B cannot be extended by adding fAn.
Due to the fact that the original components A and B were loosely coupled and they were both made of highly cohesive functionalities:
- fAn is inseparable from A. In fact, functionalities fA1, fA2, …, fAn-1 in A* strictly depend on functionality fAn in B* and vice versa, which increases the co-dependency between A* and B*.
- Functionalities in B can’t be extended by adding fAn. In fact, fAn is not strongly related to the other functionalities fB1, fB2, …, fBm in B* and vice versa, which reduces B* cohesiveness.
In other words, such components A* and B* would immediately exhibit mutual dependencies and less cohesiveness compared to the original components A and B.
This was an example on how to take into account both loose coupling and high cohesion when decomposing a system by making use of both inseparability and non-extensibility principles.
- Software architecture is the result of a sequence of design decisions.
- Tackling software erosion is performed by preventing the accumulation of technical debt.
- Technical debt results from architecture constraint violations.
- Software architecture must clearly describe constraints on how components interact.
- Internal component information remain totally isolated and hidden.
- The only way components can exchange data is via interfaces.
- A modular architecture is made of loosely coupled and high cohesive components.
- Architecture decomposition is an iterative design process.
- Decomposition separates closely related functionalities into distinct components.
- Taking into account both loose coupling and high cohesion can be done by making use of both inseparability and non-extensibility principles.