I thought this book did a good job of presenting a number of ideas about software architecture. The three standout discussions in the book for me were the following: (1) The proposal that each of the three major programming paradigms (i.e. structured programming, object-oriented programming and functional programming) removes capabilities from the programmer and imposes discipline rather than adding new capabilities. (2) The definition of high-level and low-level components based on how far or near the component is to the system's inputs and outputs. (3) The importance of writing high-level components (i.e. business rules) so that they can be tested independent of low-level concerns like the database and the user interface.
Yes, this book did have an emphasis on dependency inversion and yes, taken to its extreme, I can see how someone might go on a rampage and create an interface for each and every class in the system. However, I think that is more a fault of the reader than the book itself. Perhaps, in fairness, the book could have called out the dangers and drawbacks of taking dependency inversion to this kind of extreme. All in all, I thought this book could have done with a bit more editing and condensing but it was perfectly readable and there is plenty in it to think about and take away.
See below for some definitions and quotes from the book.
Definitions
Programming paradigms: Ways of programming unrelated to any particular programming language. A paradigm tells you which programming structures to use and when to use them.
Paradigms -> Structured Programming: Imposes discipline on direct transfer of control. Unrestrained jumps like goto
statements are replaced with if/then/else
and do/while/until
constructs.
Paradigms -> Object-Oriented Programming: Imposes discipline on indirect transfer of control. Promotes polymorphism through the disciplined use of function pointers.
Paradigms -> Functional Programming: Imposes discipline upon assignment. A foundational notion is immutability.
Dependency Inversion: The source code dependency between a low-level class L and the interface I that it implements points in the opposite direction to the flow of control from a high-level class H and the call that it makes to I. The source code dependencies are inverted against the flow of control.
The Single Responsibility Principle: A module should be responsible to one and only one actor. Think of "actor" as the singular group of users or stakeholders who may provide a reason for the module to change. Think of "module" as a source file or a cohesive set of functions and data structures.
Cohesion: The force that binds together the code responsible to a single actor.
Architectural Boundary: A line that separates software elements from one another and restricts those on one side from knowing about those on the other. Ideally, this line divides the system into two components: one abstract and the other concrete. The abstract component contains all the high-level business rules of the application. The concrete component contains all the implementation details that those business rules manipulate.
Dependency Rule: Dependencies cross the architectural boundary lines in one direction: toward more abstract entities containing the higher level policy.
Components: The units of deployment. They are the smallest entities that can be deployed as part of a system.
Reuse/Release Equivalence Principle: The granule of reuse is the granule of release. Another way to look at it: Classes and modules that are grouped together into a component should be releasable together. The fact that they share the same version number and the same release tracking, and are included under the same release documentation, should make sense both to the authors and to the users.
Common Closure Principle: 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. This is the Single Responsibility Principle restated for components: A component should not have multiple reasons to change. Another way to look at it: If two classes are so tightly bound that they always change together, then they belong in the same component.
Common Reuse Principle: Don't force users of a component to depend on things they don't need. Another way to look at it: When we depend on a component, we want to make sure that we depend on every class in that component. This principle tells us more about which classes shouldn't be together than about which classes should be together.
The Acyclic Dependencies Principle: Allow no cycles in the component dependency graph. A cycle in the component dependency graph means that the components in the cycle have, in effect, become one large component. The developers working on any of these components will be stepping over one another because they must all use exactly the same release of one another's components. Also, to test any one component in this cycle, the developers must build and integrate with all of the components.
Stability: How easy or difficult it is to change a software component. A component with lots of incoming dependencies is very stable because it requires a great deal of work to reconcile any changes with all the dependent components.
The Stable Dependencies Principle: 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. By conforming to this principle, we ensure that modules that are intended to be easy to change are not depended on by modules that are harder to change.
The Stable Abstractions Principle: A component should be as abstract as it is stable. Taken together with the Stable Dependencies Principles, this implies that dependencies run in the direction of abstraction.
Level (as in: high-level vs low-level): The distance from the inputs and outputs. The farther a policy is from both the inputs and outputs of the system, the higher its level. The policies that manage input and output are the lowest-level policies in the system.
Entity: An object within our computer system that embodies a small set of critical business rules operating on critical business data. The object either contains the critical business data or has very easy access to that data. The interface of the object consists of functions that implement the critical business rules that operate on that data.
Use Case: A description of the way that an automated system is used. It specifies the input to be provided by the user, the output to be returned to the user, and the processing steps involved in producing that output.
Quotes
Foreword: "Not only does a good architecture meet the needs of its users, developers, and owners at a given point in time, but it also meets them over time."
Foreword: "The only way to go fast, is to go well."
Preface: "Why should such different systems all share similar rules of architecture? My conclusion is that the rules of software architecture are independent of every other variable."
Introduction: "Getting software right is *hard*. It takes knowledge and skills that most young programmers haven't yet acquired. It requires thought and insight that most programmers don't take the time to develop. It requires a level of discipline and dedication that most programmers never dreamed they'd need. Mostly, it takes a passion for the craft and the desire to be a professional."
Introduction: "... when you get software right, something magical happens: You don't need hordes of programmers to keep it working. You don't need massive requirements documents and huge issue tracking systems."
Introduction: "When software is done right, it requires a fraction of the human resources to create and maintain. Changes are simple and rapid. Defects are few and far between. Effort is minimised, and functionality and flexibility are maximised."
Introduction: "Have you worked on systems that are so interconnected and intricately coupled that every change, regardless of how trivial, takes weeks and involves huge risks?"
Introduction: "It is far more common to fight your way through terrible software designs than it is to enjoy the pleasure of working with a good one."
What is design and architecture / The Goal? "The goal of software architecture is to minimise the human resources required to build and maintain the required system."
What is design and architecture? / The Goal? "The measure of design quality is simply the measure of the effort required to meet the needs of the customer. If that effort is low, and stays low throughout the lifetime of the system, the design is good. If that effort grows with each new release, the design is bad."
What is design and architecture? / What Went Wrong? "The developers may think that the answer is to start over from scratch and redesign the whole system – but that's just the Hare talking again. The same overconfidence that led to the mess is now telling them that they can build it better if only they can start the race over."
A Tale of Two Values / Architecture: "Software was invented to be 'soft'. It was intended to be a way to easily change the behaviour of machines. If we'd wanted the behaviour of machines to be hard to change, we would have called it *hardware*."
A Tale of Two Values / The Greater Value: "... there's no such thing as a program that is impossible to change. However, there are systems that are practically impossible to change, because the cost of change exceeds the benefit of change."
A Tale of Two Values / The Greater Value: "... it is the responsibility of the software development team to assert the importance of architecture over the urgency of features."
Paradigm Overview / Food For Thought: "Each of the [programming] paradigms [i.e. structured programming, object-oriented programming and functional programming] *removes* capabilities from the programmer. None of them adds new capabilities. Each imposes some kind of extra discipline that is *negative* in its intent. The paradigms tell us what *not* to do, more than they tell us what *to* do."
Paradigm Overview / Food For Thought: "... each [programming] paradigm [i.e. structured programming, object-oriented programming and functional programming] takes something away from us."
Structured Programming / Proof: "... all programs can be constructed from just three structures: sequence, selection, and iteration."
Structured Programming / Science To The Rescue: "Science does not work by proving statements true, but rather by *proving statements false*. Those statements that we cannot prove false, after much effort, we deem to be true enough for our purposes."
Structured Programming / Science To The Rescue: "... mathematics is the discipline of proving provable statements true. Science, in contrast, is the discipline of proving provable statements false."
Structured Programming / Tests: "... a program can be proven incorrect by a test, but it cannot be proven correct. All the tests can do, after sufficient testing effort, is allow us to deem a program to be correct enough for our purposes."
Structured Programming / Tests: "Software development is not a mathematical endeavour, even though it seems to manipulate mathematical constructs. Rather, software is like a science. We show correctness by failing to prove incorrectness, despite our best efforts."
Functional Programming / Squares of Integers: "Variables in functional languages *do not vary*."
Functional Programming / Immutability and Architecture: "All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables. You cannot have a race condition or a concurrent update problem if no variable is ever updated. You cannot have deadlocks without mutable locks."
The Single Responsibility Principle: "... separate the code that different actors depend on."
The Single Responsibility Principle: "... separate code that supports different actors."
Open-Closed Principle: "... the behaviour of a software artifact ought to be extensible, without having to modify that artifact."
Open-Closed Principle / A Thought Experiment: "If component A should be protected from changes in component B, then component B should depend on component A."
Open-Closed Principle / A Thought Experiment: "We want to protect the *Controllers* from changes in the *Presenters*. We want to protect the *Presenters* from changes in the *Views*."
Components: "... well-designed components always retain the ability to be independently deployable and, therefore, independently developable."
Component Cohesion / The Tension Diagram for Component Cohesion: "The three [component] cohesion principles tend to fight each other. The REP [Reuse/Release Equivalence Principle] and CCP [Common Closure Principle] are *inclusive* principles: Both tend to make components larger. The CRP [Common Reuse Principle] is an *exclusive* principle, driving components to be smaller. It is the tension between these principles that good architects seek to resolve."
Component Coupling / Top-Down Design: "... component dependency diagrams have very little to do with describing the function of the application. Instead they are a map to the *buildability* and *maintainability* of the application."
Component Coupling / The Stable Dependencies Principle: "One sure way to make a software component difficult to change, is to make lots of other software components depend on it."
What is Architecture? "The architecture of a software system is the shape given to that system by those who build it. The form of that shape is in the division of that system into components, the arrangement of those components, and the ways in which those components communicate with each other. The purpose of that shape is to facilitate the development, deployment, operation, and maintenance of the software system contained within it. The strategy behind that facilitation is to leave as many options open as possible, for as long as possible."
What is Architecture? "... the architecture of a system has very little bearing on whether that system works. There are many systems out there, with terrible architectures, that work just fine. Their troubles do not lie in their operation; rather, they occur in their deployment, maintenance, and ongoing development."
What is Architecture? "Good architecture makes the system easy to understand, easy to develop, easy to maintain, and easy to deploy. The ultimate goal is to minimise the lifetime cost of the system and to maximise programmer productivity."
What is Architecture? / Keeping Options Open: "... software has two types of value: the value of its behaviour and the value of its structure. The second of these is the greater of the two because it is this value that makes software *soft*."
What is Architecture? / Keeping Options Open: "The way you keep software soft is to leave as many options open as possible, for as long as possible. What are the options that we need to leave open? *They are the details that don't matter.*"
What is Architecture? / Keeping Options Open: "The goal of the architect is to create a shape for the system that recognises policy [i.e. the business rules and procedures] as the most essential element of the system while making the details [i.e. IO devices, databases, frameworks etc.] *irrelevant* to that policy."
What is Architecture? / Keeping Options Open: "If you have a portion of the high-level policy working, and it is agnostic about the database, you could try connecting it to several different databases to check applicability and performance... The longer you leave options open, the more experiments you can run, the more things you can try, and the more information you will have when you reach the point at which those decisions can no longer be deferred."
What is Architecture? / Keeping Options Open: "A good architect pretends that the decision has not been made, and shapes the system such that those decisions can still be deferred or changed for as long as possible."
What is Architecture? / Keeping Options Open: "A good architect maximises the number of decisions not made."
What is Architecture? / Conclusion: "Good architects carefully separate details from policy, and then decouple the policy from the details so thoroughly that the policy has no knowledge of the details and does not depend on the details in any way."
Independence / Use Cases: "The most important thing a good architecture can do to support behaviour is to clarify and expose that behaviour so that the intent of the system is visible at the architecture level... Developers will not have to hunt for behaviours, because those behaviours will be first-class elements visible at the top level of the system."
Independence / Decoupling Layers: "... separate those things that change for different reasons... collect those things that change for the same reasons..."
Boundaries: Drawing Lines: "What kinds of decisions are premature? Decisions that have nothing to do with the business requirements – the use cases – of the system. These include decisions about frameworks, databases, web servers, utility libraries, dependency injection, and the like. A good system architecture is one in which decisions like these are rendered ancillary and deferrable. A good system architecture does not depend on these decisions. A good system architecture allows those decisions to be made at the latest possible moment, without significant impact."
Boundaries: Drawing Lines / Which Lines Do You Draw, And When Do You Draw Them: "The database is a tool that the business rules can use *indirectly*. The business rules don't need to know about the schema, or the query language, or any other details about the database. All the business rules need to know is that there is a set of functions that can be used to fetch or save data."
Boundary Anatomy / The Dreaded Monolith: "High-level components remain independent of lower-level details."
Boundary Anatomy / Local Processes: "... the architectural goal is for lower-level processes to be plugins to higher-level processes."
Boundary Anatomy / Services: "Lower-level services should *plug in* to higher-level services."
Policy And Level: "... lower-level components should be plugins to the higher-level components."
Business Rules: "How the data gets in and out of the system is irrelevant to the use cases."
Screaming Architecture / The Purpose of an Architecture: "Good architectures are centred on use cases so that architects can safely describe the structures that support those use cases without committing to frameworks, tools, and environments."
Screaming Architecture / The Purpose of an Architecture: "... consider the plans for a house. The first concern of the architect is to make sure that the house is usable – not to ensure that the house is made of bricks. Indeed, the architect takes pains to ensure that the homeowner can make decisions about the exterior material (bricks, stone, or cedar) later..."
Screaming Architecture / Frameworks Are Tools, Not Ways of Life: "Look at each framework with a jaded eye. View it skeptically. Yes, it might help, but at what cost? Ask yourself how you should use it, and how you should protect yourself from it. Think about how you can preserve the use-case emphasis of your architecture. Develop a strategy that prevents the framework from taking over that architecture."
Screaming Architecture / Testable Architectures: "If your system architecture is all about the use cases, and if you have kept your frameworks at arm's lengths, then you should be able to unit-test all those use cases without any of the frameworks in place. You shouldn't need the web server running to run your tests. You shouldn't need the database connected to run your tests. Your Entity objects should be plain old objects that have no dependencies on frameworks or databases or other complications. Your use case objects should coordinate your Entity objects."
Screaming Architecture / Conclusion: "Your architecture should tell readers about the system, not about the frameworks you used in your system. If you are building a health care system, then when new programmers look at the source repository, their first impression should be, 'Oh, this is a health care system.' Those new programmers should be able to learn all the use cases of the system, yet still not know how the system is delivered."
The Test Boundary / Design for Testability: "... design the system, and the tests, so that business rules can be tested without using the GUI."
Clean Embedded Architecture: "Although software does not wear out, it can be destroyed from within by unmanaged dependencies on firmware and hardware."
Clean Embedded Architecture: "Being able to test only in the target hardware is not good for your product's long-term health."