Objective:
Try to know & acquire the importance of clean architecture (hexagonal architecture) and the difference b/w layered architecture by implementing some actual codes.
- Three-layer architecture
-
very common architecture
-
Web → Domain → Persistence
- Web: receives requests and routes them to a service in the domain layer (or business layer)
- Domain: does some business magic and calls components from the persistence layer
- Persistence: stores data
-
This architecture is a solid architecture
→ domain logic can be independent of the web and persistence layers
(we can switch the web or persistence technologies w/o affecting the domain logic)
-
But, it has too many open flanks that allow bad habits to creep in
-
- It promotes Database-Driven Design
-
Foundation of a conventional layered architecture is the database
(Everything builds on top of the persistence layer)
-
With using ORM framework, architecture usually looks like this
-
This creates a strong coupling b/w the persistence layer and the domain layer
-
Persistence code is fused into the domain code
-
- It’s prone to shortcuts
- It grows hard to test
- It hides the use cases
- Services usually are too broad
-
SRP
- Actual definition: A component should have only one reason to change
- Responsibility → Reason to change
- Complex dependencies make a component hard to change
-
DIP
-
Clean Architecture
-
Business rules are
- testable by design
- independent of frameworks, databases, … (technologies)
⇒ Domain code must not have any outward facing dependencies
- All dependencies b/w those layers must point inward
- Domain entities must be accessed only by the surrounding use cases
-
-
Hexagonal Architecture
- It applies the same principles of Clean Architecture
- By defining Ports (Interfaces) in the hexagon (domain), it satisfies no outgoing dependencies
- Adapters are related to specific technologies, so located at outside of the hexagon
- Driver Adapters (which drives our application) use ports
- Driven Adapter (which are driven by out application) implement ports
⇒ Why its original name is “Ports and Adapters Architecture”
-
In greenfield projects, the most important thing is the package structure
-
Organizing by layer
-
It satisfies the direction of dependencies
-
There is no package boundary b/w features of the application
-
Can’t know what use cases the
AccountService
orAccountController
implement(= Can’t know the purpose of those classes only by their name)
-
-
Organizing by feature
-
There are all the codes related to accounts into the high-level package
account
-
Now, there is boundaries b/w the features
(Package boundaries combined w/ package-private visibility enable us to avoid unwanted dependencies b/w features)
-
AccountService renamed to
SendMoneyService
to narrow its responsibility(Can know the purpose of
SendMoneyService
)→ Make the codes scream its intention at us (Screaming Architecture)
-
But, it is hard to identify which ones are ports and incoming or outgoing adapters
-
-
Architecturally expressive package structure
domain
package → contains domain modelapplication
package → contains a service layer around the domain modelSendMoneyService
implements the incoming port interfaceSendMoneyUseCase
- uses
LoadAccountPort
andUpdateAccountStatePort
, which are implemented by the persistence adapter
adapter
package → contains in/out adapters
- What a use case actually does
- Take input
- Why there is no “Validate input” step?
- domain logic shouldn’t be polluted with input validation
- input model (
~Command
) contains Validation step - input models are located in the incoming port package
- Why there is no “Validate input” step?
- Validate business rules
- Manipulate model state
- Return output
- Take input
- Different input models for different use cases
- Might be tempted to use the same input model for different use cases
- Validation step must be difficult → will pollute sacred business codes
- Might be tempted to use the same input model for different use cases
- Different output models for different use cases
- return as little as possible
-
Dependency Inversion
- Web adapter → “driving” or “incoming” adapter
- takes requests from the outside and translates them into calls to application core
- Adapters can interact w/ application core only through ports (dependency inversion)
- Web adapter → “driving” or “incoming” adapter
-
Responsibilities of a Web Adapter
- Transform input model of the web adapter into the input model of the use cases
- Call a certain use case w/ the transformed input model
-
Create a separate controller, potentially in a separate package for each operation
- name the methods and classes as close to our use cases as possible
-
Dependency Inversion
- Persistence adapter is a “driven” or “outgoing” adapter
-
input model to the persistence adapter lies within the application core
-
Slicing Port Interfaces
- Broad port interface makes unnecessary dependencies
- need to adapt ISP (Interface Segregation Principle)
-
Slicing Persistence Adapters
- One persistence adapter per aggregate (domain) → good foundation
-
The Test Pyramid
- Need high coverage of fine-grained tests, which are cheap to build, easy to maintain, fast-running, and stable → Unit Test
- Unit Test
- is the base of the pyramid
- dependencies are replaced with mocks
- Integration Test
- instantiates a network of multiple units
- System Test
- spins up the whole network of objects that make up the application
-
Unit Tests of Domain entity in hexagonal architecture → easy (no dependency)
-
‘No Mapping’ Strategy
- All layers share the domain model
-
‘Two-Way’ Mapping Strategy
- Only inner layers know their own model
- Usually makes a lot of boilerplate code
- Incoming/Outgoing ports use domain objects as input parameters and return values
-
‘Full’ Mapping Strategy
- Uses a separate input/output model per operation
-
‘One-Way’ Mapping Strategy
- Models in all layers implement the same interface
- Domain model can implement a rich behavior