Photo by Shubham Dhage on Unsplash
Understand Component Design Principles
What are components and how to organize them efficiently
The set of principles provided by Component Design gives great insights into how to organize components inside an application.
It's also one of the requirements to understand Clean Architecture. That's the reason multiple pieces of knowledge come from Clean Architecture, which makes use of other references such as A Use Case Driven Approach or Lean Architecture.
Prerequisites
SOLID
Inside this article, lots of principles you will learn are actually derived from SOLID (but applied to components). You definitely need to understand it first, before reading any further.
Classes
In the following part of this article, and probably most articles about architecture, you will read about organizing classes.
It definitely makes sense if your whole application is built on top of OOP, but might get you to second-guess information when your code is not using classes.
Feel free to substitute classes for the structure you are actually using (be it functions, or anything else).
What are components?
A very simple definition of a component is a unit of deployment, which is the smallest entity that makes up a system. In practice, components take different forms depending on the language they are made with.
In interpreted languages, they are aggregated source files. On the other hand, they are closer to binary files with compiled languages (JAR files for Java or gem files for Ruby).
In the end, components can be used together to form a complete system. Potentially, they can be independently deployable, but more importantly, independently developable.
While those definitions are quite abstract, they include all the possible forms of components. In practice, we can think of components as part of this practical but non-comprehensive list:
Multiple applications working together to create a system
Packages (or libraries)
Group of source files (sometimes called modules)
Cohesion
When it comes to Component Design, you will often get asked "Which classes belong to which components? ". Usually, those decisions are made based on context, but three main Component Cohesion Principles help you give a better answer.
Reuse/Release Equivalence Principle (REP)
This principle tells us components should only be re-used when they are tracked through a release process (and given an identifier when released).
Obviously, it's necessary to ensure components work together when new versions get released. Developers must be told when a new version is released, as well as which changes are included. Only then can they decide whether to use a new version or not.
As a general rule of thumb, all the elements that make up a component should be releasable together. They must be grouped because it makes sense to users, but "making sense" is not precise enough advice, hence the following two principles.
Common Closure Principle (CCP)
The Common Closure Principle can be seen as the Single Responsibility Principle, applied to components:
Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times.
To be more straightforward, components should not have multiple reasons to change (just like a class). In the end, the same challenges apply:
It's easier for maintainability to gather all the changes in a single component, instead of many components
When changes occur in a single component, you only need to (re)validate whoever uses this specific component.
Common Reuse Principle (CRP)
The Common Reuse Principle is closer to the Interface Segration Principle, albeit more generic:
Don't force users of a component to depend on things they don't need.
Like the previous principle, it helps decide which classes and modules should be placed into a component. Typically, you will find classes with multiple dependencies between each other.
On the other hand, this principle also tells us which classes should not be kept together. From a general standpoint, classes that are not tightly bound to each other shouldn't be in the same component.
Don't depend on things you don't need.
Tension Diagram
If you understood those principles correctly, you realized they have an incompatible relationship with each other: REP and CCP are inclusive, while CRP is exclusive.
Everything is about balance, which in this case, is called tension between the three principles:
The goal of any great architect is to find the right place on this tension diagram for its current system. Focusing too much on REP and CRP will cause too many components to be updated when a change is made. On the other hand, focusing solely on CCP and REP will increase the number of necessary releases
Coupling
In the previous section, we talked multiple times about the relationship between components. But how exactly do we design those components, their relationship, and how coupled they are?
Acyclic Dependencies Principle (ADP)
It's mandatory for components to have no cycles in their dependency tree. Take the following dependency graph for example (where components are nodes and dependencies are edges):
No matter which components you take and follow dependencies, you will always end up with entities. It's impossible to follow dependencies back to the same component.
In other words, this dependency graph has no cycles, it's a directed acyclic graph.
When a new version of a component is released, it's easy to find out which components are affected: you can follow the dependencies backwards.
Teams responsible for the affected components need to validate everything works fine with the new version. The unaffected components don't need to undergo the same process.
Now let's take another example:
Here, we can see a new dependency between Entities and Authorizer, adding a cyclic dependency to this graph. Immediately, we face new issues.
When a new version of a component is released, it has to be compatible with many more components. For example Database must ensure it works not only with Entities, but also Authorizer.
That's the case for every component that uses Entities, including Interactors. That also means Entities cannot be built in autonomy, but must integrate with Authorizer and Interactors. Generally, testing and releases are rendered bothersome.
To avoid cyclic dependencies there are two main solutions:
Use Dependency Inversion Principle (DIP)
Instead of adding a new relationship between two components, create a new component, they both depend on.
But don't forget:
Allow no cycles in the component dependency graph.
Stable Dependencies Principle (SDP)
Components and their relationship are volatile by design. Following the Common Closure Principle leads us to create components affected by some changes and immune to others.
As a general rule, a volatile component shouldn't be depended on by a component that is difficult to change. Otherwise, the volatile component will be difficult to change as well.
Be careful, a module that is easy to change can be made difficult to change when depended on by a volatile dependency.
That's the reason the Stable Dependencies Principle tells us:
Modules that are intended to be easy to change should not be depended on by modules that are harder to change.
Usually, a component will be harder to change (or stable), when it is depended on by multiple dependencies. It will also tend to be more volatile when it depends on more dependencies.
Another way to understand the Stable Dependencies Principle is the degree of volatility should decrease the deeper you go into the dependency graph.
Unfortunately, it will happen that one of your stable components depends on one that's volatile. Similar to the previous principle, it's possible to solve this issue by:
Using Dependency Inversion Principle
Creating another component depended on by the other two.
To make it more simple:
Depend in the direction of stability
Stable Abstraction Principle (SAP)
Usually, some part of a software is made to rarely change. That's what we call high-level policy. This type of business and architecture decision is intended to be stable, not volatile.
However, if the code for those policies is placed into a stable component, it will be made hard to change. Sometimes, you will need to have a stable high-level policy, which implementation should be easier to change, but how?
We can get some inspiration from the Open-Closed Principle (which is part of SOLID). It tells us it's possible (and desirable) to create classes flexible enough to be extended without requiring modification: Abstract classes.
This is where the Stable Abstraction Principle comes in. It defines a relationship between stability and abstractness:
A stable component should also be abstract so that its stability does not prevent it from being extended.
An unstable component should be concrete (not abstract) since its instability allows the concrete code within it to be easily changed.
In turn:
If a component is to be stable, it should consist of interfaces and abstract classes so that it can be extended.
If we combine the Stable Dependencies Principle and Stable Abstraction Principle, we are able to make a component partially abstract or stable. There is no perfect level of abstraction or stability, it depends on the policies you wish to express.
What you should aim for, is the right level of abstraction compared to the level of stability necessary for your components.
For example, a component that is both stable and concrete will be painful to maintain. It can hardly be extended because it's not abstract, and very difficult to change because it's stable.
On the other hand, a component that is both volatile and abstract will probably be useless. That means it expresses abstract policies, which nobody depends on.
Do you want to learn more about Clean Architecture? Let me know here.