- What is Design and Architecture?
- no difference between design and architecture
- low-level details and high-level structures are all part of the same whole
- The goal of software architecture is to minimize the human resources required to build and maintain the required system
- Bad architecture can result in increasing costs per new feature
- making a mess is always slower than staying clean
- can start from scratch, but overconfidence will drive the redesign into the same mess as the original project
- every organization should take the quality of its software architecture seriously
- A Tale of Two Values
- every software system provides behavior and structure
- behavior: programmers are hired to make machines behavior in a way to make or save money
- structure: software should be simple and easy to change
- if a program is impossible to change it will become useless when the requirements change
- if a program does not work but is easy to change, it’s easy to fix
- architecture is important but never particularly urgent
- fight for the architecture
- you are a stakeholder and must do what is best for the company
- Paradigm Overview
- structured programming imposes discipline on direct transfer of control
- object-oriented programming imposes discipline on direct transfer of control
- functional programming imposes discipline upon assignment
- Structured Programming
- structured programming allows modules to be recursively decomposed into provable units
- decomposed functions can be represented using the restricted control structures of structured programming
- use tests to prove those small provable functions
- Object-Oriented Programming
- what is OO? Is it encapsulation, inheritance, and polymorphism?
- encapsulation
- many OO languages have little or no enforced encapsulation
- inheritance?
- inheritance is just the redeclaration of a group of variables and functions within an enclosing scope
- C programmers could do before OO languages
- OO did not give us something new but it did make it more significantly more convenient
- polymorphism
- polymorphism is an application of pointers to functions
- OO made it much safer and more convenient
- pointers to functions are dangerous and bugs are devilishly hard to track down
- OO languages eliminated conventions and the dangers
- OO allows the plugin architecture to be used anywhere, for anything
- dependency inversion
- any source code dependency can be inverted
- absolute control over the direction of all source code dependencies
- e.g. plugin the UI and database to the business rules
- UI and the database can be compiled into separate components
- the component containing the business rules will not depend on the components containing the UI and the database
- results in independent deployability and developability
- OO is the ability, through the use of polymorphism, to gain absolute control over every source code dependency in the system
- Functional Programming
- variables in functional languages do not vary
- eliminates race conditions, deadlocks, concurrent update problems
- one pattern is to separate the application into mutable and immutable components
- use transactional memory to protect mutable variables from concurrent updates and race conditions
- push as much processing as possible into immutable components
- event sourcing is strategy for tracking all transactions and getting to the state by applying all transactions
- with enough storage and processor power, can make the application entirely immutable
- SRP: The Single Responsibility Principle
- A module should have one, and only one, reason to change
- “reason” being users or stakeholders
- accidental duplication: separate the code that different actors depend on
- merges: multiple people changing the same file
- solutions
- can separate the data from the functions
- can keep the most important method in the original class and use that class as a facade for the lesser functions
- SRP is about functions and classes but it reappears as the “Common Closure Principle” for components and the “Axis of Change” for the architectural level
- OCP: The Open-Closed Principle
- A software artifact should be open for extension but closed for modification
- An interactor that deals with interfaces will not be impacted by changes to the implementations
- a hierarchy of protection based on the notion of “level”
- Interactors are the highest-level concept so most protected
- Interfaces also provide information hiding
- the goal of the system is to make the system easy to extend without incurring high impact of change
- the goal is accomplished by arranging components into a dependency hierarchy that protects higher-level components from changes in lower-level components
- LSP: The Liskov Substitution Principle
- when substituting based on subtypes the behavior shouldn’t change
- violation when the subtype cannot conform
- e.g. if a square is a subtype of a rectangle, setWidth and setHeight may have unexpected behavior
- in early days LSP guided the use of inheritance
- over the years it has morphed into a broader principle that pertains to interfaces and implementations
- a simple violation of substitutability can cause a system’s architecture to be polluted with a significant amount of extra mechanisms
- ISP: The Interface Segregation Principle
- it has harmful to depend on modules that contain more than you need
- depending on something that carries baggage that you don’t need can cause you troubles that you didn’t expect
- DIP: The Dependency Inversion Principle
- in the most flexible systems, source code dependencies refer only to abstractions
- e.g. in Java, the use, import, and include statements should refer only to source modules containing interfaces and abstract classes
- not realistic to avoid all concrete elements, but strive to avoid depending on volatile ones
- interfaces are less volatile than the concrete implementation
- good software designers work hard to reduce the volatility of interfaces
- don’t refer to volatile concrete classes
- puts severe constraints on the creation of objects and generally enforces the use of Abstract factories
- don’t derive from volatile concrete classes
- in static languages, inheritance is the strongest most rigid of all source code relationships
- use with great care
- don’t override concrete functions
- concrete functions often require source code dependencies
- when you override those functions you inherit those dependencies
- to manage multiple dependencies, you should make the functions abstract and create multiple implementations
- never mention the name of anything concrete and volatile
- Use abstract factories to create objects
- e.g. An Application may deal with a ServiceFactory (Interface) to get a Service (Interface) where the ServiceFactory is backed by a ServiceFactoryImpl and the Service is backed by a ConcreteImpl
- DIP violations cannot be entirely removed, but they are gathered into a small number of concrete components e.g. main
- e.g. the main function would instantiate the ServiceFactoryImpl and place that instance in the global variable of type ServiceFactory
- The way dependencies point towards more abstract entities is the Dependency Rule
- Components
- components are the unit of deployment
- can be linked together into a single executable or deployed separately
- independently developable
- initially, programmers included source code of the library functions with their application code, but devices were slow and memory was expensive
- to shorten, programmers compiled the function library separately and loaded the binary at a known address
- they would load the binary function library and then the application, but started running out of memory
- the compiler was changed to output binary code that could be relocated in memory by a smart loader resulting in the linking loader
- eventually, linking loaders were to slow to tolerate so the linking and loading were separated into two phases
- eventually, Moore’s law won out and linking time started shrinking fast
- linking became very fast so component plugin architecture was born
- Component Cohesion
- the reuse/release equivalence principle
- the granule of reuse is the granule of release
- people who want to reuse software won’t unless components are tracked through a release process and given release numbers
- classes and modules in a component must belong to a cohesive group
- the common closure principle
- gather into components classes that change for the same reasons and at the same time
- single responsibility principle for components
- for most applications, maintainability is more important than reusability
- combining components that need to change together minimizes the workload related to releasing, revalidating, and redeploying the software
- associated with open-closed principle because 100% closure is not attainable so keep component closed to the same type of changes
- the common reuse principle
- don’t force users of a component to depend on things they don’t need
- more about what classes to keep out of a component
- when we depend on a component try to make sure we depend on every class in that component
- generic version of ISP, do not depend on classes we don’t use
- need to balance between all three principles
- in early stage, you may lean more towards CCP because developability is more important than reuse
- may shift to care more about reuse as the project matures
- Component Coupling
- allow no cycles in the component dependency graph
- weekly builds can slowly devolve as project size grows
- partition development environment into releasable components
- allows teams to handle integration in small increments on their own schedule
- cycles result in one large component
- break the cycle by applying DIP or creating a new component that both components depend on
- components cannot be designed from top-down it must evolve as the system grows and changes
- component dependency diagrams are a map to the buildability and maintainability of the application
- one of the overriding concerns is the isolation of volatility
- the stable dependency principle
- depend in the direction of stability
- a component with a lot of incoming dependencies is very stable because it requires a great deal of work to reconcile any changes
- a component that has no dependencies is unstable
- can measure stability by counting the number of dependencies that fan in vs fan-out
- not all components should be stable
- if a component depends on a less stable component, create an interface and depend on that instead
- a component should be as abstract as it is stable
- unstable components should be concrete since instability allows it to be easily changed
- What is Architecture?
- architecture is the shape given to that system by those who build it
- purpose of the shape is to leave as many options open as possible, for as long as possible
- troubles do not lie in operations, but on the deployment, maintenance, and ongoing development
- the primary purpose of architecture is to support the life cycle of the system
- development
- should make the system easy to develop
- cannot make progress if you have multiple developers if not broken into well-defined components
- if no other factors are considered, the system will likely evolve into one component per team which is likely not the best for deployment, operations, and maintainability
- deployment
- the higher the cost of deployment the less useful of a system
- should easily be able to deploy with a single action
- e.g. microservices may be easy to develop, but deployment may be difficult
- operation
- a good architecture communicates the operational needs of the system
- architecture should reveal the operation
- maintenance
- the most costly aspect
- the primary cost is spelunking and risk
- spelunking is the cost of digging through the existing software and trying to determine the best place and strategy to add a new feature or repair a defect
- while making changes, the likelihood of creating defects is always there, adding to the cost of risk
- carefully thought-through architecture mitigates these costs
- keep options open
- all systems decomposed into policy and details
- the goal of the architect is to create a shape for the system that recognizes policy as the most essential element of the system while making the details irrelevant to that policy
- delay and defer detail decisions
- the longer you wait, the more time you have to make the proper ones
- even if the detail decision has been made, a good architect pretends like it hasn’t been
- Independence
- use cases
- the system must support the intent of the system
- the most important thing a good architecture can do to support behavior is to clarify and expose that behavior so that the intent of the system is visible at the architectural level
- maintenance
- architecture must support all kinds of throughput and response time based on the use case
- may involve many different processing strategies e.g. multiple services in parallel, lightweight threads, single process, etc
- leave these decisions open so you can transition through threads, processes, and services as the operational needs of the system change over time
- development
- conway’s law: the design will be a copy of the organization’s communication structure
- a system that must be developed by many teams must have an architecture that facilitates independent actions by those teams
- deployment
- good architecture doesn’t rely on multiple configuration scripts and property file tweaks
- most of the time we don’t know all the use cases and even if we did, they will change
- decoupling layers: separating business rules, UI, database, etc
- the use cases themselves can change (vertical)
- separating to the service level is an option you should also leave open
- duplication
- accidental duplication is when two seeming duplication sections can change at different rates and for different reasons
- if duplication is incorrect, you will make the wrong abstraction with a new dependency for both of those components
- my perference is to push the decoupling point to where a service could be formed and leave option open for a servie
- a good architecture allows for the reversing of microservices back into monolith if separate services no longer required
- Boundaries: Drawing Lines
- don’t pollute core business logic
- draw lines between things that matter and things that don’t
- e.g. the database is just a tool to store and fetch data
- the business rules don’t care about what kind of database we use
- you business logic shouldn’t even be aware of the DI framework you’re using
- business rules use plugins that aren’t related to the core business
- Boundary Anatomy
- boundaries in monolith is just function call, very fast
- threads is just a way to organize the schedule and order or execution, can be contained within a component or spread across many
- local processes are stronger boundaries and run on separate address spaces
- often communciate with each other using sockets, mailboxes, or queues
- local process is like an uber component
- strongest boundary is a service
- assumes all communications take place over the network
- must deal with high latency
- lower-level services should plug in to higher-level services
- the source code of higher-level services must not contain any specific knowledge of any lower-level services
- Policy and Level
- policies that manage I/O are the lowest level policies in a system
- high level policies tend to change less frequently and for more important reasons than lower-level policies
- keep policies separate with all source code dependencies pointing in the direction of the higher-level policies reduces impact of change
- Business Rules
- an entity is an object that embodies critical business rules and operates on critical business data
- class stands alone unsullied with concerns about databases, user interfaces, or third party frameworks
- not all business rules are as pure as entities since some only make sense as part of an automated system
- may have application-specific business rules
- use cases describe the application-specific rules that govern the interaction between the users and the Entities
- Entities have no knowledge of the use cases that control them
- use cases accept simple request data structures and returns simple response data structures
- Screaming Architecture
- a good architecture allows decisions about frameworks, databases, webservers, and other environmental issues to be deferred
- should be easy to change your mind
- the web is an IO device
- for frameworks develop a strategy so it doesn’t take over your architecture
- should be able to unit test use cases without frameworks in place
- the architecture should tell readers about the system not the details
- The Clean Architecture
- hexagonal architecture, DCI, BCE have many things in common
- independent of framework
- testable
- independent of UI
- independent of the database
- independent of any external agency
- dependency rule: source code dependencies must point only inward towards higher-level policies
- no operational changes to an application should affect the entity level
- software in the use cases layer contain application-specific business rules and shouldn’t affect entities nor should be affected by UI, db, frameworks
- software in the interface adapters layers converts data format
- conver to form most convenient for entities and use case
- no code inward of circle should know anything about the database
- data that crosses boundarie consist of simple data structures not entities or database rows
- always pass data in the form that is most convenient for the inner circle
- Presenters and Humble Objects
- humble object
- split behaviors into two modules
- one of those modules is humble which contains the hard to test code
- the other contains all the testable behaviors
- e.g. the Presenter and the View
- the view is humble and hard to test, but doesn’t process data
- the presenter accepts data and formats it for the view to show on the screen
- database gateways
- between use case interactors and the database
- interface contains methods for every create, read, update, or delete operation
- use case layer doesn’t know SQL
- the gateway implementations are humble because simply use SQL
- interactors encapsulate application-specific business rules
- ORMs should be data mappers because they return data variables not objects
- should exist at the data layer
- ORMS are another form of humble object boundary
- service listeners
- application will load data into simple data structures
- then pass those across the boundary to modules that properly format the data and send it to external services
- Partial Boundaries
- fully-fledged boundaries are expensive
- do all the work to create independent components but keep them together
- full-fledged boundaries use reciprocal boundary interfaces
- simpler structure only has one way (strategy pattern)
- e.g. Client uses a serviceBoundary interface
- boundary can be defined by facade class
- client will have transitive dependencies to all service classes
- Layers and Boundaries
- architectural boundaries are expensive need to know when to use them
- if ignored expensive to add later too
- you must guess intelligently
- must constantly make the decision as the system evolves
- implement the boundaries right at the inflection point where the cost of implementating becomes less than the cost of ignoring
- The Main Component
- main component is the ultimate detail or the lowest level policy
- creates all the factories, strategies, and other global facilities
- DI should take place in the main component
- think of main as a plugin to the application
- Services: Great and Small
- services don’t define boundaries, the separation between high-level policy from low-level detail does
- services are strongly coupled by the data they share
- services cannot always be independently developed, deployed, and operated
- services can be designed using SOLID principles and given a component structure
- services must be designed with internal component architectures that follow the dependency rule
- The Test Boundary
- tests are part of the system
- tests are very detailed and concrete and should depend inward toward the code (dependency rule)
- should be independently deployable
- tests that are not well integrated into the design of the system tend to be fragile
- if tests are strongly coupled to the system must change along with the system
- design for testability
- don’t depend on volatile things e.g. GUI
- create a specific API that the tests can ues to verify all the business rules
- this api is a super set of the suite of interactors and interface adaptors used by the user interface
- role of the testing api is to hide the structure of the application from the tests
- decouples the tests from the application
- structural coupling is strongest and most insidious forms of test coupling
- allows production code to be refactored and evolved in ways that don’t affect the tests
- Clean Embedded Architecture
- don’t let all code become firmware
- program to interfaces and substituability
- The Database Is a Detail
- your use case should not care if database is relational, etc
- databases are prevalent because of disks
- disk is being replaced by RAM and once they are will likely no longer keep in tables and directory structures
- will organize into linked lists, trees, hash tabes, etc
- database performance is a concern that can be entirely encapsulated and separated from the business rules
- the organizational structure of data, the data model, is architecturally significant but technologies that move data on and off disks is not
- The Web is a Detail
- constant oscillation between computing power in the client vs server
- decouple business rules from the UI
- the GUI is a detail and the web is a GUI
- the business logic can be thought of as a suite of use cases
- this kind of abstraction is not easy and it will likely take several iterations to get just right
- Frameworks Are Detail
- framework authors don’t know you nor your problems
- they don’t owe you anything
- they make no commitment to you whatsoever
- you take on all the risk and burden of the framework
- they may violate dependency rules
- it may fight you as your application evolves
- the framework may evolve in a direction you don’t find helpful
- don’t marry the framework
- business objects should not know about your framework
- Case Study: Video Sales
- The Missing Chapter
- devil is in the implementation details
- package by layer e.g. controller, service, repository is good way to get started
- package by feature is vertical
- both are suboptimal
- consider inside (domain) and outside (infrastructure)
- inside contains all domain concepts and outside contains all interactions with outside world
- rename “OrdersRepository” to “Orders” since inside should be stated in terms of the ubiquitous domain languages
- package by component
- bundling all responsiblities related to a single coarse-grained component into a single Java package
- similar to microservices
- provides one interface to interact through
- doesn’t expose repository
- don’t mark everything as public
- if all packages are public then structure is only organization not encapsulation
- if you’re separating infrastructure from domain code use proper access modifiers e.g. so web controller can’t access database repository directly
- best design intentions can be destroyed in a flash if you don’t consider the intricacies of the implementation strategy
Like this:
Like Loading...