It is time to turn to software architecture. Having covered modules, classes, and components, this article focuses on the basic definition of architecture.
Before We Begin
🌟 A software architect must be a programmer and must continue doing hands-on development. Without personally experiencing the difficulties caused by a system’s design, architects lose touch with the pain of poor design and gradually lose their sense of direction.
What Is Software Architecture?
Architectural work is fundamentally about deciding how to divide a system into components and arrange their relationships and communication—in other words, defining boundaries.
Architectural design generally has two purposes:
- To make components easier to develop, deploy, run, and maintain.
- To make all this work easier by keeping as many options open as possible for as long as possible;
The quality of a system’s architecture is not closely related to whether its behavior works correctly. Many systems with poor architecture work perfectly well. The trouble usually appears during development, deployment, and subsequent enhancements. This supports the author’s view that architectural value can outweigh behavioral value.
The goals of architectural design are:
- Core goal: organize architecture around use cases, isolating them from surrounding concerns by postponing decisions and preserving options.
- Primary goal: support the full software lifecycle. Good architecture makes a system understandable, changeable, maintainable, and easy to deploy.
- Ultimate goal: maximize programmer productivity while minimizing the system’s total operating cost.
Architectural design focuses on separating policies and regrouping them according to how they change, thereby establishing boundaries. Applicable principles include SRP, CCP, CSP, SDP, SAP, and others.
The Responsibilities of Architecture
Architecture supports development, deployment, operation, and maintenance.
Development: architecture should facilitate software development, so different teams may need different architectural designs. This reflects Conway’s law to some extent.
Why does Conway’s law imply that architecture reflects a team’s structure? This video explains:Conway’s Law: Why Does Your Architecture Reflect Your Team Structure? — Bilibili
Deployment: architecture should make deployment easy, ideally with one click.
Operation:
- As mentioned earlier, architecture has less influence on runtime efficiency than on the other concerns. The author attributes this largely to the fact that additional hardware can compensate for operational shortcomings, while architectural design requires more expensive human effort.
- Beyond efficiency, architecture should reveal the system’s runtime requirements. A well-designed architecture makes operation apparent to developers. Use cases, features, and required behaviors should be visible as first-class elements, simplifying understanding. The chapter “Screaming Architecture” explores this idea in detail.
Maintenance: maintenance is usually the most expensive part of a system’s lifecycle. Its costs fall into two categories: spelunking and risk.
- Spelunking: the effort of exploring an existing system to find the best location and method for adding a feature or fixing a problem.
- Risk: the possibility that those changes introduce new problems, along with the cost of managing that possibility.
Keeping Options Open
Recall the two kinds of value: behavioral and architectural. Increasing architectural value means making software more flexible, and flexibility comes from keeping more architectural options open.
The elements of a software system fall into two categories: policy and details. Policy contains the business rules and procedures that provide the system’s real value. Details enable users, other systems, and programmers to interact with policy without defining it. I/O devices and databases are examples. Choices about these details should remain open for as long as possible.
The architect’s goal is to create a system in which policy is the fundamental element and details are separated from it, allowing decisions about details to be deferred. The later a decision is made, the more information is available to make it well. A good architect works to maximize the number of options that remain open.
The author uses device independence to illustrate the value of deferring details. Modern operating systems support many I/O devices, but early computers relied largely on punched paper tape. Programmers naturally coupled tape-reading and tape-writing code to the system. Magnetic tape required new code, then optical discs required more. To avoid repeated implementation and poor adaptability, developers introduced device independence: devices were abstracted behind functions that the operating system called, and developers supplied the concrete implementations. I/O became a plugin to the operating system. This was an early form of OCP: welcome additions 👏 and resist modifications 🥊.
Preserving Independence
Good architecture provides sufficient independence.
Decoupling helps preserve that independence.
Decoupling can be horizontal or vertical.
- Horizontal decoupling, or decoupling by layer: divide a system into layers such as the UI and database.
- Vertical decoupling, or decoupling by use case: also divide the layers into vertical slices by use case. For example, separate the UI for adding an order from the UI for deleting an order.
Decoupling according to reasons for change allows new use cases to be added without affecting existing ones.
Decoupling can also happen at different levels: source code, deployment, or services.
- Source-code level: control dependencies between source modules. Components interact through function calls. This is a monolithic structure.
- Deployment level: control dependencies between deployment units such as JAR files. Components may communicate across threads—not necessarily networks—through sockets or shared memory.
- Service level: reduce intercomponent dependencies to data structures and communicate through network packets, as microservices do with RPC or REST.
No strict rule makes one level of decoupling universally better. The best choice may change as a project matures. A well-designed architecture should allow a system to begin as a monolith deployed in one file, grow into independently deployable units or services, and return to a monolith if circumstances change. It should also protect most source code from these transitions. For the system as a whole, decoupling mode should itself remain an option. Large deployments may use one mode and small deployments another.
With decoupling established, let us consider its effects on independence.
Operational Independence
If use cases with different concerns are properly isolated, high-throughput and low-throughput use cases naturally separate. Separating the UI and database from business logic allows them to run on different servers. Applications requiring greater bandwidth can also run multiple instances across servers.
Development Independence
When a system is properly decoupled by layers and use cases, its architecture supports multiple teams regardless of whether they are organized by features, components, layers, or another division of work.
Deployment Independence
With effective decoupling, layer implementations and use cases can even be hot-swapped while a system runs. Adding a use case may require only adding new JAR files or starting new services, leaving everything else unaffected.
What Is Code Duplication?
There are two kinds of duplication:
- True duplication: repetition that should be eliminated.
- False duplication: code that looks similar but follows different evolutionary paths, with different rates and reasons for change. CRP calls for code with different usage patterns to live in different components. Ignoring this can make false duplication look like code that should be merged. Some apparent duplication is necessary—remember the salary-calculation example?
(End of Part 5)