- SOLID principle How to arrange the bricks into walls and rooms.
- Component principle How to arrange the rooms into buildings.
Components, jars, gems, packages, DLLs, .jar
, .dll
, .exe
... well designed components always retain the ability to be independently deployable and deployable.
It used to take a long time to compile the code. Programmers made the code into smaller components and compiled them individually to save up the compile time.
- Fig 12.1 Early memory layout
Application and Function Library are mapped in difference memory spaces.
- Fig 12.2 Splitting the application into two address segments
When the application grew, programmers needed to split the applications into two address segments.
The bigger the program is, the more space to allocate needed. The solution was "relocatable binaries". The loader was told which part of loaded data had to be altered to be loaded at the selected address. ...then the linking loader was born.
The linking loader - In the late 1960s and early 1970s, programs got a lot bigger. It worked well when small programs were being linked with small libraries. But this approach was too slow, because the linking loader had to scan through tons of library to resolve. Then the rise of linker - the output of this was a linked relocatable that a relocating loader could load very quickly.
Murphy's law of program size:
Program will grow to fill all available compile and link time.
These dynamically linked files, which can be plugged together at runtime, are the software components of our architectures. It has taken 50 years, but we have arrived at a place where component plugin architecture can be the casual default as opposed to the herculean effort it once was.
Three principles of Component Cohesion
- REP: The Reuse / Release Equivalence Principle
- CCP: The Common Closure Principle
- CRP: The Common Reuse Principle
The granule of reuse is the granule of release.
From a software design and architecture point of view,this principle means that the classes and modules that are formed into a component must belong to a cohesive group.
I used to think mono-repo was useful only for packages that are public. However, this principle changed my perspective. It works for private projects as well. Each repository needs to be grouped in "make sense", then gets managed the release/reuse.
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 and for different reasons.
It's close to OCP (Open Closed Principle). 100% closure is not attainable, closure must be strategic.
- SRP (Single Responsibility Principle) for components.
- "A class should not contain multiple reasons to change."
- CCP
- "A component should not have multiple reasons to change."
Both principles can be summarized by the following sound bite:
Gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons.
Don't force users of a component to depend on things they don't need.
What classes and modules should be belonging to the component.
Reusable classes collaborate with other classes that are part of the reusable abstraction.
It seems like the abstraction level is the key. Any example?
classes that are not tightly bound to each other should not be in the same component.
^ e.g., Data model objects likely not be in the same component.
- ISP (Interface Segregation Principle)
- not to depend on classes that have methods we don't use.
- CRP
- not to depend on components that have classes we don't use.
Don't depend on things you don't need.
-
REP and CCP are inclusive principles. -> driving components to be bigger.
-
CRP is exclusive principle. -> driving components to be smaller.
-
Fig 13.1 Cohesion principles tension diagram
The diagram describes:
- REP: Group for reusers
- CCP: Group for maintenance
- CRP: Split to avoid unneeded releases
- If you only focus on REP and CRP ... too many components are impacted when simple changes are made.
- If you only focus on CCP and REP ... too many unneeded release to be generated.
- If you only focus on CCP and CRP ... components are hard to reuse.
Find what's the current problem your team is facing, and take the best option.
I personally found this is interesting. What I care last a few years is the struggle of maintainability. Hard to debug, hard to fix. Although that means the product's stage is leaning toward to the left of the diagram. However, when the first developers were making this, what they cared was develop-ability. (But it doesn't mean it's really maintainable, though.) This is a map of buildability and maintainability.
Also, releasing is no longer a big hustle nowadays. Browsers get updated pretty often, and so are OSes.
In choosing the classes to group together into components, we must consider the opposing forces involved in reusability and develop-ability.
This chapter will talk about the relationship between components.
Allow no cycles in the component dependency graph.
"morning after syndrome" -> You built something and go home. Then on the next day it doesn't work because someone changed.
To mitigate the issue, two solutions are made:
- "The weekly build"
- Acyclic Dependencies Principle (ADP)
All developers work individually until Friday, then merge on Friday.
- You can work freely most of the week.
- You need to deal with all debt on Friday. (And deploying on Friday!?)
This is pretty bad.
The components become units of work that can be the responsibility of a single developer, or a team of developers.
Then when it's done they release. The other team, or developers takes the released version into their dev-env. Integration happens in small increments.
You need to manage dependency structure of components - DO NOT MAKE CIRCULAR DEPENDENCIES!
- Fig 14.1 Typical component diagram
With the graph, you'd notice:
- the structure is a directed graph. The components are nodes, and the dependency relationships are the direct edges.
- the structure has no cycle. It is a directed acyclic graph (DAG).
- Fig 14.2 A dependency cycle
The
User
class inEntities
use thePermissions
class inAuthorizer
. But theAutorizer
is used inEntities
.
Circular dependency. This also makes it testing harder.
To mitigate the circle problem,
- Apply the Dependency Inversion Principle (DIP)
- Fig 14.3 Inverting the dependency between
Entities
andAuthorizer
- Fig 14.3 Inverting the dependency between
- Create a new component that both
Entities
andAuthorizer
depend on.- Fig 14.4 The new component that both
Entities
andAuthorizer
depend on
- Fig 14.4 The new component that both
The second solution above creates component structure that is volatile, then the component dependency structure jitters and grows.
The component structure cannot be designed from the top down. It is not one of the first things about the system that is designed, but rather evolves as the system grows and changes.
If we tried to design the component dependency structure before we designed any classes, we would likely fail rather badly.
Component structure grows, you can't design in the beginning.
Depend in the direction of stability.
Any component that we expect to be volatile should not be depended on by a component that is difficult to change.
- Fig 14.5 x: a stable component
The component 'x' has lots of incoming dependencies thus not easy to change -> stable.
- Fig 14.6 y: a very unstable component
The component 'y' has no components relying on it. It depends on other components.
How can we measure the stability?
-
Fan-in: Incoming dependencies.
-
Fan-out: Outgoing dependencies.
-
Instability: I = Fan-out / (Fan-in + Fan-out)
- The range
[0, 1]
- 0: Maximally stable
- 1: Maximally unstable
- The range
-
Fig 14.7 Our example
The component Cc
has three incoming deps and one outgoing dep. Fan-in = 3, Fan-out = 1, thus I = 1/(3+1) = 1/4
.
- Fig 14.8 An ideal configuration for a system with three components
- Fig 14.9 SDP (Stable Dependencies Principle) violation
- Fig 14.10
U
withinStable
usesC
withinFlexible
- Fig 14.11
C
implements the interface classUS
The flexible component (meant to be volatile by design) has an incoming dependency from a stable component. To mitigate this, we can use the DIP (Dependency Inversion Principle).
It only contains an interface.
A component should be as abstract as it is stable.
- The software that encapsulates the high-level policies of the system should be placed into stable components.
- The software that we want to be able to change should be placed into unstable components.
if a component is to be stable, it should consist of interfaces and abstract classes so that it can be extended. Stable components that are extensible are flexible and do not overly constrain the architecture.
The A metrics to measure the abstractness of a component.
- Nc: The number of classes in the component.
- Na: The number of abstract classes in the component.
- A: Abstractness. A = Na / Ac
- The range
[0, 1]
- 0: The component has no abstract classes
- 1: The component has only abstract classes
- The range
- Fig 14.12 The I/A graph
The graph takes Instability and Abstractness.
Highly stable, and concrete.
Examples:
- Database schemas are volatile, and concrete. Schema update is generally painful.
- A concrete utility library. It's painful if you need to change the String component.
Highly abstract, but no deps.
Just nobody is using it.
Ideally, components should be on the main sequence.
-
D: Distance. D = |A + I - 1|
- The range
[0, 1]
- 0: The component is on the main sequence
- 1: Far away from the main sequence
- The range
-
Fig 14.14 Scatterplot of the components
Plot all components into the graph.
- Fig 14.15 Plot of D for a single component over time
Plot a component into the graph over time
The dependency management metrics described in this chapter measure the conformance of a design to a pattern of dependency and abstraction that I think is a "good" pattern. Experience has shown that certain dependencies are good and others are bad.