Simon Brown talks to us about how, now days, we have many diagramming tools and concepts which some of us like to use, and sometimes are even imposed upon the developers by the corporations managers, who actually have no idea of technicalities and the usefulness or not of those diagrams. However, despite the tools and concepts we have, when we create a diagram of the architecture of a software program we are developing, most of the time it ends up not matching the actual code, we can not see the architecture in the code.
Binding architecture and code
- Goes from top to bottom: Top layers use the bottom layers;
- A controller should only have a hard dependency on one service, although exceptionally it might have soft dependencies on other services, in order to get data used in the UI. And in turn, a service should only talk to (use) one repository.
- Goes horizontally: Code units can use each other if in the same layer;
- A controller can redirect the user to another controller. In a web context, the controller can redirect the browser to another controller (URL);
- A service can use other services, in order to get the data it needs or to perform related functionality which is responsibility of other(s) service(s);
- A repository can use other repositories in order to retrieve composed data structures from whatever persistence tool (DB) we are using, although with most modern ORMs this should not be necessary;
- Never goes directly from bottom to top: Bottom layers never use the top layers;
- A service is never allowed to directly use a controller;
- A repository is never allowed to directly use a service nor a controller.
- The only way a lower layer can execute logic in a top layer is through a “side effect” management system, ie through observers and events which should only be used for logic that is not a business-logic requirement for the current use case, but rather a technical-logic requirement, like logging every time a repository saves an entity in the DB or rebuilding the application cache when certain data changes.
I also feel its important to do a retrospective and note that throughout the last decades, software development has evolved in the direction of isolating/encapsulating data and logic:
- 1950s: Plain straight code;
- 1960s: Structured programming with conditionals and loops;
- 1970s: Procedural code with functions/procedures that could be reused;
- 1980s: OOP which encapsulates data with logic (functions/methods);
- 1990s: MVC spread and brought us the beginning of layered architecture;
- 2000s: SOLID, Design Patterns and DDD, brought even more specialized classes and groups of classes;
- 2010s: Micro-services are spreading, to encapsulate data and logic even further, providing high level decoupling between bounded contexts, and between the code and the technologies supporting it.
(these dates are roughly when the concepts spread widely in the community, not when the concepts were actually formalized, ie: MVC idea showed up in 1978, the first article was written in 1988, and wide usage started somewhere during the 1990s)
In the case of layered architecture, the objective is to isolate code by its functionality, in the sense of its architectural responsibility. Its all about the Single Responsibility Principle brought to us by Robert C. Martin.
However, as Simon Brown tells us, layered architecture is not enforced by most programming languages, its just a concept, a style. Meaning: in less clear moments we can and will break its rules and end up with something like the following, or even worst:
To avoid this, and since there are no language constructs to enforce it, we need to be very disciplined, but we also need to think of a design that reflects those restrictions, and we need to reflect the architecture in the code, as much as possible.
George Fairbanks calls it, in his book, using an architecturally evident coding style. In practice, this means doing things like postfixing classes names with their architectural function, for example “CarController”, “CarService”, “CarRepository” or “CarBuilder”. Also, if we want to make it evident that “CarController”, “CarService” and “CarRepository” all correspond to one bounded context, then we must make it evident in the structure of the project by encapsulating them in one top level folder, a “CarModule” or “CarComponent”.
Just like before, In the case of this componentized architecture, the objective is to isolate code by its common responsibility, in this case its business-logic responsibility. Its once again the Single Responsibility Principle brought to us by Robert C. Martin. The idea here is to encapsulate everything relating to that subject, in such a way that by removing the folder containing a module, we would remove everything related to that module. This architecture makes it evident, from the top of the project, what the application is all about, again in sync with what Robert C. Martin tells us about a project structure.
Furthermore, another very good thing behind this approach is that it will get us in a middle stage between a monolith architecture and a micro-services architecture. This means we are still a in a monolith, but its highly decoupled monolith: we can easily extract and deploy just one of the components into a separate server. Neither a monolith architecture nor a micro-services architecture are inherently bad, they both have advantages and disadvantages and we should try to harvest the best of both worlds.
Documenting the architecture
Another very interesting concept Simon Brown introduces, is the diagrams he uses, and which he calls the C4 model:
- Context: A high-level diagram that sets the scene; including key system dependencies and actors.
- Container: A container diagram shows the high-level technology choices, how responsibilities are distributed across them and how the containers communicate.
- Component: For each container, a component diagram lets you see the key logical components and their relationships.
- Classes: This is an optional level of detail and I will draw a small number of high-level UML class diagrams if I want to explain how a particular pattern or component will be (or has been) implemented. The factors that prompt me to draw class diagrams for parts of the software system include the complexity of the software plus the size and experience of the team. Any UML diagrams that I do draw tend to be sketches rather than comprehensive models.
Loved the talk and the contents. Simon Brown website is also packed with more explanations of these concepts, his conference talks and links to his (free) leanpub books, which look pretty cool.