The point is that Main is a dirty low-level module in the outermost circle of the clean architecture. It loads everything up for the high level system, and then hands control over to it.
Since it is a plugin, it is possible to have many Main components, one for each configuration of your application.
When you think about Main as a plugin component, sitting behind an architectural boundary, the problem of configuration becomes a lot easier to solve.
Service-oriented “architectures” and micro-service “architectures” have become very popular of late. The reasons for their current popularity include the following:
- Services seem to be strongly decoupled from each other. As we shall see, this is only partially true.
- Services appear to support independence of development and deployment. Again, as we shall see, this is only partially true.
The architecture of a system is defined by boundaries that separate high-level policy from low-level detail and follow the Dependency Rule.
One of the big supposed benefits of breaking a system up into services is that services are strongly decoupled from each other.
The services must also strongly agree about the interpretation of the data in that field. Thus those services are strongly coupled to the data record and, therefore, indirectly coupled to each other.
Another of the supposed benefits of services is that they can be owned and operated by a dedicated team.
- services are not the only option for building scalable systems.
- the decoupling fallacy means that services cannot always be independently developed, deployed, and operated.
- Fig 27.1 Services arranged to implement the taxi aggregator system
The elements are:
- Taxi UI
- Taxi Finder
- Candidate Taxis
- Taxi Selector
- Taxi Dispatcher
- Taxi Supplier 1-3
Now we want to add a kitten delivery service.
Look at that diagram of services. How many of those services will have to change to implement this feature? All of them. Clearly, the development and deployment of the kitty feature will have to be very carefully coordinated. In other words, the services are all coupled, and cannot be independently developed, deployed, and maintained.
^ the problem with cross-cutting concerns.
Functional decompositions in the diagram are very vulnerable to new features that cut across all those functional behaviors.
portion of the logic that was specific to rides has been extracted into a Rides component. The new feature for kittens has been placed into a Kittens component. These two components override the abstract base classes in the original components using a pattern such as Template Method or Strategy.
the two new components, Rides and Kittens, follow the Dependency Rule.
- Fig 27.2 Using an object-oriented approach to deal with cross-cutting concerns
Services can, in stead, be designed using the SOLID principles (to apply the same solution as the kitten problem)
adding new features conforms to the Open-Closed Principle.
- Fig 27.3 Each service has its own internal component design, enabling new features to be added as new derivative classes
What we have learned is that architectural boundaries do not fall between services. Rather, those boundaries run through the services, dividing them into components.
- Fig 27.4 Services must be designed with internal component architectures that follow the Dependency Rule
The architecture of a system is defined by the boundaries drawn within that system, and by the dependencies that cross those boundaries.
Are unit tests and integration tests different things? What about acceptance tests, functional tests, Cucumber tests, TDD tests, BDD tests, component tests, and so on?
In fact, you can think of the tests as the outermost circle in the architecture.
Changes to common system components can cause hundreds, or even thousands, of tests to break. This is known as the Fragile Tests Problem.
Irf those tests are strongly coupled to the system.
Fragile tests often have the perverse effect of making the system rigid.
The first rule of software design—whether for testability or for any other reason—is always the same: Don’t depend on volatile things. GUIs are volatile.
This API should have superpowers that allow the tests to avoid security constraints, bypass expensive resources (such as databases), and force the system into particular testable states. This API will be a superset of the suite of interactors and interface adapters that are used by the user interface.
This decoupling encompasses more than just detaching the tests from the UI: The goal is to decouple the structure of the tests from the structure of the application.
The role of the testing API is to hide the structure of the application from the tests.
Tests are not outside the system; rather, they are parts of the system that must be well designed if they are to provide the desired benefits of stability and regression.
"The Growing Importance of Sustaining Software for the DoD" by Doug Schmidt.
“Although software does not wear out, firmware and hardware become obsolete, thereby requiring software modifications.”
Software is this thing that can have a long useful life, but firmware will become obsolete as hardware evolves.
"Although software does not wear out, it can be destroyed from within by unmanaged dependencies on firmware and hardware."
Hardware does evolve, so we should structure our embedded code with that reality in mind.
Stop writing so much firmware and give your code a chance at a long useful life.
Let's look at how we can keep embedded software architecture clean to give the software a fighting chance of having a long useful life.
Kent Beck describes three activities in building software
- "First make it work." You are out of business if it doesn't work.
- "Then make it right." Refactor the code so that you and others can understand it and evolve it as needs change or are better understand.
- "Then make it fast." Refactor the code for "needed" performance.
Learn what works, then make a better solution.
The engineer passed the App-titude test. But the application can't be said to have a clean embedded architecture.
When embedded code is structured without applying clean architecture principles and practices, you will often face the scenario in which you can test your code only on the target.
-
Fig 29.1 Three layers
-
Software
-
Firmware
-
Hardware
-
Fig 29.2 Hardware must be separated from the rest of the system
-
Firmware
-
Hardware
Software and firmware intermingling is an anti-pattern.
- Fig 29.3 The line between software and firmware is a bit fuzzier than the line between code and hardware.
The hardware abstraction layer (HAL)
-
Fig 29.4 The hardware abstraction layer
-
Software
-
HAL
-
Firmware
-
Hardware
The firmware could provide access to the GPIO bits, where a HAL might provide Led_TurnOn(5). That is a pretty low-level hardware abstraction layer.
What is the LED indicating? Suppose that it indicated low battery power.
a real-time operation system (RTOS)
-
Fig 29.5 Adding in an operating system
-
Software
-
OS
-
Firmware
-
Hardware
an operating system abstraction layer (OSAL)
-
Fig 29.6 The operating system abstraction layer
-
Software
-
OSAL
-
OS
-
HAL
-
Firmware
-
Hardware
A clean embedded architecture's software is testable off the target operating system. A successful OSAL provides that seam or set of substitution points that facilitate off-target testing.
-
One basic rule of thumb is to use header files as interface definitions.
-
Don't clutter the interface header files with data structures, constants, and typedefs that are needed by only the implementation.
Letting all code become firmware is not good for your product’s long-term health. Being able to test only in the target hardware is not good for your product’s long-term health. A clean embedded architecture is good for your product’s long-term health.