The goal of software architecture is to minimise the human resources required to build & maintain the required system.
- Lines of code over time
- Cost per line of code
- Productivity over time
Consider importance & urgency matrix. In most cases, business logic (behaviour) is considered more urgent and architecture may never be urgent in the eyes or business managers.
Consider the extreme, new features can be easily added to a system with very good architecture. System with too many features, but poor architecture may end up becoming impossible for additional features to be added in.
- Structured Programming: impose discipline on direct transfer of control
- ability to create falsifiable units of programming
- functional decomposition recursively breaks down a big problem to many small ones
- Dijkstra, "tests show the presence, not the absence, of bugs". Software is like a science, which shows correctness by failing to prove incorrectness.
- OOP: impose discipline on indirect transfer of control
- OO: combination of data & function
- a way to model the real world
- encapsulation (to expose only a subset of functions & data); inheritance (more conveniently play with data structure and reuse code); polymorphism: allows dependency inversion, which allows engineers to have absolute control over every source code dependency in the system
- Functional Programming: impose discipline upon assignment
- immutability: no concurrency issue
- event sourcing: store only the transactions, not the state. Instead of CRUD, just CR. Assuming unlimited storage and processing power, the system is entirely functional. (how source control system works)
Essentially: software is composed of sequence, selection, iteration, and indirection. Nothing more, nothing less.
- tolerate change
- easy to understand
- basis of components that can be used in many software systems
A module (a cohesive set of functions & data structures) should be responsible to one, and only one, actor
- Accidental Duplication Consider the following example:
Class Employee {
calculatePay(); // specified by the accounting department
reportHours(); // by HR
save(); // by DBA
}
SRP suggests to separate the code that different actors depend on
- Merges separating the code prevents merge conflicts
Solution:
Create a EmployeeFacade class for lesser functions, with the purpose of delegating to classes with needed functions.
A software artifact should be open for extension but closed for modification
- for example, simple extensions to the requirements should need simple engineering efforts
A system that displays financial summary on a web page, to be extended to print the same information on paper
-
Directional Control: much complexity exists to make sure dependencies point to the right direction
-
Information Hiding:
FinancialReportRequester
preventsFinancialReportController
from knowing too much internals of theInteractor
-
No transitive dependencies: software entities should never depend on things they do not directly use
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 behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
- User to access Rectangle(+setH,+setW) extended by Square(+setSide)
- This violates LSP since user has to know whether a Rectangle is a Square
- Earlier, LSP as a way to guide the use of inheritance
- Now, applicable to all interface related concepts
- it is harmful to depend on modules that contain more than you need
- the following picture is the right way instead of having 3 users depending on OPS directly
source code dependencies refer only to abstractions, not to concretions, as much as possible
-
Stable abstractions: changes in implementations should not require changes in interface
- do not refer to volatile concrete classes. use Abstract Factories
- do not derive from volatile concrete classes
- do not override concrete functions: make function abstract and create multiple implementations
- never mention the name of anything concrete
-
Factories:
Dynamically linked files which can be plugged together at runtime are the software components of our architecture
- REP: The Reuse/Release Equivalence Principle
- CCP: The Common Closure Principle
- CRP: The Common Reuse Principle
The granule of reuse is the granule of release
- Classes & modules that are formed into a component must belong to a cohesive group, and should be releasable together
- The group should "make sense"
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.
- Restatement of SRP, SRP: a class should not contain multiple reasons to change, CCP says a component should not have multiple reasons to change
- For most applications, maintainability > reusability
- When new requirement comes, good chance that change can be restricted to a minimal number of components
Don't force users of a component to depend on things they don't need.
- focus on which classes should not be kept together in a component
- classes that are not tightly bound to each other should not be in the same component
- CCP & REP are inclusive principles to make components larger
- CRP is an exclusive principle
Generally, projects tend to start on the right hand side of the triangle, where reusability is sacrificed, the as project matures, slide to left. It also varies from project to project.
Allow no cycles in the component dependency graph
- The weekly build: developers focus on their private work, and integrate at the end of the week. This will not work as project scale grows.
- Eliminating Dependency Cycles:
- Partition the development environment into releasable components.
- Release with a version number, then it is the duty of the client team to decide whether to use it.
- During release process, individual components are released independently, verification from bottom up, e.g. components to be depended on will be tested first.
- The most important thing is that there is no cyclic dependency - component graph has to be a DAG
- Once there is a cycle, all components have to be released together
Example of A Dependency Cycle:
Top Down Design: isolation of volatility, components which change frequently should not affect other stable components
- Component dependency graph created to protect stable components
- Design structure grows & evolves with the logical design of the system
- If we tried to design the component dependency structure before any classes, it is likely to fail badly since we do not know much about common closure, reusable elements etc.
Depend in the direction of stability
- Any component that we expect to be volatile should not be depended on
- Generally, a component with lots of incoming dependencies is very stable
- Fan-in: incoming dependencies, Fan-out: outgoing dependencies; Instability: I = Fan-out / (Fan-in + Fan-out)
- In many cases, DI is a way to reverse the dependency, interface or abstract classes can be extremely stable
A component should be as abstract as it is stable
- Relationship bet/w stability & abstractness
- a stable component should be abstract so that its stability does not prevent it from being extended
- an unstable component should be concrete since its instability allows the concrete code within it to be easily changed
- A: Abstractness: percentage of interfaces/abstract classes in a component to measure abstraction
- too abstract & stable: useless; too concrete & unstable: rigid (e.g. database schema, String class in Java - though String class rarely changes so it is ok)
- Try to have (A,I) as (0,1) or (1,0), though not possible in most cases
Ultimate goal: minimize the lifetime cost of the system and to maximize programmer productivity
Architecture of a software system is the shape given to that system. The form of the 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. And the strategy behind that facilitation is to leave as many options open as possible, for as long as possible
- small team tends to produce monolithic system with no well-defined architecture
- system developed by 5 teams may end up having 5 components
- both are not ideal
- make a system that can be easily deployed with a single action
- consider the factor of deployment initially
- many micro-service architecture becomes difficult to maintain because of this
- A good software architecture communicates the operational needs of the system (e.g. database maintenance, adding servers etc.)
- Certain examples include: stateless vs stateful login
- Mitigate the cost of troubleshooting & fixing bugs etc.
- Cost to figure out a bug should be low
- Cost to fix a bug should be low, and no additional defects should come out of it