Takeaway: An organization needs to take the quality of its software architecture seriously.
The goal of software is to minimize the human resources required to build and maintain the required system.
The only way to go fast, is to go well.
- There is no difference between design and architecture.
- Design quality is measured by how much effort is required to meet the needs of the customer. Good design has low effort to meet customer needs and vice versa.
- There is a case study shown where a company hired many developers, but they didn’t produce more code as measured by lines of code (LOC). From the business point of view, over a cycle of 8 releases, the cost exploded from a couple hundred thousand dollars to millions.
- The cause of this is that developers buy into this lie of “We can clean it up later; we just have to get to market first”. But that never happens as the market pressue is always constant; someone is always on your tail.
- Making a mess does not mean you’ll code faster. Making messes is always slower than staying clean.
Takeaway: A software developer mainly deals with behavior and architecture. The chapter concludes that if architecture comes last, then the system will become ever more costly to develop, and eventually change will become practically impossible for part or all of the system.
It is the responsibility of the software development team to assert the importance of architecture over the urgency of features.
- Software provides two values to stakeholders that developers are responsible for: behavior and structure.
- Behavior is the most common task: implement requirements and fix bugs. However, structure (architecture) is equally important.
- When a change request comes in, it should be easy to change within the scope of the change, not to the shape of the change. What this means is that a change request should be easy to integrate into the system shape and the system should not try to adopt any certain shape because if you end up with a system that is a round hole and the requirement is a square peg, then it will be difficult to integrate.
- There are systems out there that are too costly to change.
- Urgency and importance:
- Urgent and important
- Not urgent and important
- Urgent and not important
- Not urgent and not important
- Software architecture is in the first 2 and behavior is in the latter 2. The mistake many make is elevating items in 3 to 1.
Takeaway: Paradigms are important because they align with our primary concerns of architecture: function, separation of components, and data management.
Structured programming imposes discipline on direct transfer of control.
Object-oriented programming imposes discipline on indirect transfer of control.
Functional programming imposes discipline upon assignment.
- There are 3 paradigms: structured programming, object-oriented programming, and functional programming.
- Each paradigm restrict the programmer. In functional programming, you can’t assign variables, in structured programming you can’t simply move across statement logic, in object-oriented programming you can’t use function pointers and have to deal with objects. It is unlikely that we will ever see any other paradigms as there is nothing else left to take away from the programmer.
Takeaway: Software architects strive to create modules, components, and services that are testable (falsifiable).
The problem that Dijkstra recognized, early on, was that programming is hard, and that programmers don’t do it very well.
All programs can be constructed from just 3 structures: sequence, selection, and iteration.
- Dijkstra discovered that usage of goto statements prevented divide-and-conquer approach necessary for proofs. But some uses of goto that involved control flow did not present this problem.
- Usually languages don’t give us undisciplined direct transfer of control previously afforded to use by goto statements. Even languages that have the goto function restrict its scope to the currently running function.
- Creating programs via proofs never came about, however we started using the scientific method and that method is falsifiable but not provable. Science works not by proving statements are true but by proving statements false. Those statements that cannot be proven false are deemed true enough.
- Mathematics is the discipline of proving provable statements true and science is the discipline of proving provable statements false.
Takeaway: For a software architect, the definition of OO is the ability to use polymorphism to gain absolute control over every source code dependency of a system through dependency inversion.
- This chapter starts my examining the definition of OO. It says that modelin world and “a combination of function and data” are not good answers to the definition of OO. Neither is encapsulation, inheritance, and polymorphism.
- Encapsulation means that data and functions can be hidden. The C language had perfect encapsulation; in the header file you did not have to declare internal variables to a struct, but C++ and its compiler required that header files show that information so now clients knew about the internal variables. C# and Java removed declaration files and definition files all together which weakened encapsulation even further. Any language after C has very weak encapsulation which is why it is not really a true definition of OO.
- Inheritance is the redeclaration of a group of variables and functions within an enclosing scope. In C you could hack in inheritance by declaring the structs such that the order and types were overlapped. For OO, higher level languages did make this easier to do by making this casting implicit.
- Polymorphism is exihibited in older languages like C in that you can perform IO with getchar and putchar simply by calling functions of underlying drivers (e.g. read, write, close, etc.). Pointers to functions is an application of polymorphism and that has been around for a very long time. However, OO languages have made it much safer and more convenient. Pointers to functions are dangerous because they are driven by conventions and if you don’t follow that convention then you’ll get bugs that are hard to track down.
- Dependency Inversion
- The implication above is important. In the first figure, the flow of control and the dependency direction move in the same direction. However, if you introduce inheritance as in the second figure, the flow of control is different from the dependency direction. HL1 calls F() from ML1 through the interface and that is resolved at runtime. This redirection is what OO is all about from the architect’s point of view.
Takeaway: Software is composed of sequence, selection, iteration, and indirection. Nothing more, nothing less.
Architects would be wise to push as much processing as possible into the immutable components, and to drive as much code as possible out of those components that must allow mutation.
- Mutable programs don’t exist in functional programming. Variables in functional languages do not vary.
- When you don’t have to worry about mutability, then all race conditions, deadlock conditions, and concurrent update problems go away. These are all caused by mutable variables. But is immutabilty practical?
- You will have to segregate the application into mutable and immutable components. These immutable components will interact with mutable components.
- There is an example of how to manage showing account balances for a banking application in this chapter. One way it can be done is that you can update the state of the account balance every time a transaction is performed. Another is that you can keep all the records of every transaction and sum them all up. The latter would require a lot of time and memory and that is the idea behind event sourcing. Storage is not an issue; we have plenty of storage capacity available nowadays. So you can make your entire application functional and immutable if you desired. And this is exactly how a source control system works.
- This section explored the SOLID principles. These principles tell us how to arrange functions and data structures into classes, and how to connect those classes.
- Goal: To create software structures that tolerate change, are easy to understand, and be the basis of components that are used in many software systems.
- SRP: Single Responsibility Principle - Each software module has only one reason to change and that is because of the social structure of the organization that uses it.
- OCP: Open Closed Principle - For software systems to be easy to change, they must allow behavior to be changed by adding new code, rather than changing existing code.
- LSP: Liskov Substitution Principle - Build systems from contracts that allow parts to be interchanged.
- ISP: Interface Segregation Principle- Avoid depending on useless things.
- DIP: Dependency Inversion Principle - Code that implements high-level policy should not depend on code that implements low-level details.
A module should be responsible to one, and only one, actor.
Separate the code that different actors depend on.
- This principle does not mean that a module should do one thing, but rather that a module is responsible to a user or stakeholder. We use actor in the definition above because a module can be responsible to many users/stakeholders or a group(s).
- A module can be a single file, or a collection of code bound together cohesively.
- Violations of SRP:
Employeeclass has too many actors depending on it. The actors are now coupled. If the COO comes up with a requirement that requires a change to the
Employeeclass, then that change can also affect the CTO team.
- Merges are also difficult here if both teams submit change requests that require modifications to the
- Solution is to use the Fascade pattern whereby the
EmployeeFascadeclass is used by client code instead of devs having to keep track of the other 3 classes.
A software artifact should be open for extension but closed for modification.
- If simple changes to requirements force the software to change massively, then the system architecture is a failure.
- The book presents an example of reporting on financial data. This report should separate out the presentational and calculation logic so that if someone requests that the report be in a different format, then you will only have to introduce a new class (e.g.
PrintReporter) to fulfill the request.
- The goal of limited modification is accomplished by partitioning the system into components and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
- The above is a good example of LSP. A bad example is if you have a
Rectangleclass with methods
setWand you have a
Squareclass that inherits from this. This is because for a rectangle the height and width should be changed independently, but for a square, they must change together.
- An interface is not limited to how they are defined in C# or Java. You can also define interfaces as classes that share the same signature or services that all respond to the same REST interface. In these cases the interfaces define the contract and the services implement that contract independently.
Dynamically typed languages create systems that are more flexible and less tightly coupled than statically typed languages.
It is harmful to depend on modules that contains more than you need.
- The above image shows the problem. The class
User1might depend on
op3. But if
User1will also have to be recompiled and re-deployed. The solution is shown below, now
User1won’t care if a change is made to
The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
Interfaces are less volatile than implementations.
Stable software architectures are those that avoid depending on volatile concretions, and that favor use of stable abstract interfaces.
- The chapter begins by presenting what I think is an extreme examples of Java only importing modules that contains interfaces, abstract classes, and other abstract declarations, nothing concrete. In practice, I have rarely seen this, and the book admits as much.
- The book clarifies that it is the volatile concrete elements that you want to protect from. So something like the
Stringclass, which is concrete, is OK to import since changes to that should be infrequent.
- Don’t refer to volatile concrete classes.
- Don’t derive from volatile concrete classes.
- Don’t override concrete functions.
- Never mention the name of anything concrete and volatile.
- To make sure that this is possible in dynamically type languages, you use abstract factories to create the implementations of the abstractions as shown below:
- If you look at the boundary and the arrows you’ll notice that the arrows point in the opposite directions.
Applicationhas a source code dependency on
ServiceFactory, but the flow of control is in the opposite direction:
The SOLID principles tell us how to arrange the bricks into walls and rooms, the component principles tell us how to arrance rooms into buildings.
Compnents are the units of deployment. In .NET, they are DLLs. Well desgined components always retain the ability to be independently deployable and independently developable.
In the early days, programs were not relocatable. You needed to keep track of where programs should be located in memory. If you wanted to compile a program, you would keep the source code of the library function with the application code you were writing and compile it all as one; libraries were not separate dlls that you could just reference.
- This meant that since devices were slow and memory expensive and limited, you couldn’t keep all the source code in memory. The compiler was also super slow and had to read the source code multiple times; it could take HOURS!
- The workaround was to separate out the function libary and compile it separately and then load it at a specific memory address and the app code would just have to know where to start their code so that it would run after this position.
- If either the app code or function code grew, then the addresses would need to be adjusted as they would now take up more space. This was not maintainable.
The solution to the above was to output binary code taht could be relocated in memory by a smart loader. Loader was told where to load the code. This allowed the programmer to tell the loader where to load the lib code and app code.
The linking loaders allowed programmers to divide the programs into separate compilable and loadable segments.