-
Notifications
You must be signed in to change notification settings - Fork 59
Clean Architecture Practices
The Clean Architecture, which is an architecture proposed by the famous software master Uncle Bob, is also the current development architecture of various languages. A clean architecture proposes a one-way dependency that logically forms an upward abstract system.
The concentric circles represent different levels in the software system, and usually the closer to the center, the higher the software level. Basically, the outer circle represents the mechanics and the inner circle represents the strategy. Of course, there is a rule that runs through the entire architecture design, that is, its dependency rule: the dependencies in the source code must only point to the inner layer of the concentric circle, that is, the low-level mechanism points to the high-level strategy.
- Entities: Through domain analysis and modeling, we can encapsulate domain entities and related business logic for life cycle management of these domain entities in this layer. For the analysis and identification process of domain entity objects, please refer to my other article "Design Combat – Scene Analysis & Domain Division"
- Use Cases: In the use case layer, logic related to "specific scenarios" is generally placed, and these logics will aggregate information about entities defined in the entity layer. For example, let's take the scenario of "buyer places an order" in the e-commerce system as an example. In this scenario, the entities involved in placing an order include product objects, packages, coupons, buyer information, seller information, and service information (such as delivery service), etc. The order processing logic needs to process the information and aggregate it into order information for the user to confirm or create. The logic that orchestrates these entities according to a specific scenario, which does not belong to any one entity's domain.
- Interface adapter: Expose the information of the use case layer to the outside in some form. For example, in the scenario where a buyer places an order, we can expose the interface in the form of Restful for integration with the web front end.
When building a complex business system, we need to consider the API and SPI design for entity access, and also consider the accumulation of reusable business assets in the scene. If reusable business assets can be reused across projects and customers, it is necessary to reserve scalability points SPI. For scene-level opening, please refer to UseCase Precipitation and Reuse. After the author's practice in many large-scale projects in the field of telecommunications, e-commerce, and smart cities, and referring to some suggestions for clean architecture, I have summarized a set of logical engineering division methods with reasonable layers and very suitable for team collaboration. The general engineering structure and logic modules are divided as follows:
- Business entity layer: The interface definitions of entity services are exposed externally through domain-sdk, and in the process of entity processing, there will be some special customization requirements, which are also exposed in the form of SPI. The manifestation of SPI is exposed in the form of extension points, and the specific implementation is obtained through dynamic loading during runtime, so as to realize the expansion of the processing logic of business entities.
- Use case layer: In the use case layer, it is mainly composed of definitions of various scenarios and reusable business assets. During the execution of the scenario, these business assets can be installed to meet business needs. At the same time, in the process of cross-project and cross-industry reuse of these business assets, some extensibility points can also be reserved and exposed in the form of SPI. This achieves the separation of project-level customization and platform logic.
- Interface adapters (Endpoints): mainly exist in the form of various external interfaces such as various webs and interfaces, and are used to adapt to the access needs of related devices/systems in the outer layer.
- Business Plugins: Business plugins mainly implement the SPI of business assets, and realize the customization and enhancement of personalized business logic in different projects and different industries.
Project samples can be obtained by visiting https://github.com/hiforce/lattice-clean-arch-practice-sample
Level | Module | Project | Description |
---|---|---|---|
Entity | domain-item | domain-item-sdk | The item domain SDK |
domain-item-common | The item domain common model | ||
domain-item-service | Item domain logic implemetation | ||
domain-trade | domain-trade-sdk | The trade domain SDK | |
domain-trade-common | The trade domain common model | ||
domain-trade-service | Trade domain logic implemetation | ||
Use Case | place-order-scenario | Buyer place order scenario | |
place-order-client | The client of buyer place order | ||
place-order-server | The implemetation of buyer place order scenario | ||
usecase-assets | Reusable business assets | ||
presale-usecase-capability | presale-usecase-sdk | PreSale capability's SDK | |
presale-usecase-capability | presale-usecase-server | Implemetation of pre-sale capability | |
Adaptors | sample-endpoints | ||
buynow-web | The web controller for buy now | ||
business customization | sample-business-apps | ||
sample-business-a | Sample Busines A | ||
In this project, we run org.hiforce.sample.buynow.web.starter.BuyNowWebStarter, then open the browser and enter http://localhost:8080/buy/1 , you can see the following output:
{"success":true,"orders":[{"orderId":1,"buyerId":"rocky","orderLines":[{"orderLineId":1,"itemId":"2919311334001001","buyQuantity":1,"unitPrice":4000}]}]}
Continue to enter http://localhost:8080/buy/2 to further observe the changes in the output, as follows:
{"success":true,"orders":[{"orderId":2,"buyerId":"rocky","orderLines":[{"orderLineId":2,"itemId":"2919311334001001","buyQuantity":1,"unitPrice":10000}]}]}
The two results are mainly different in the unit price of the product, because the business customization package realizes the expansion point of the payment ratio of the pre-sale. In different scenarios, pre-sale assets will take effect, resulting in different unit prices. Interested students can debug by themselves for further observation.
In the entity layer, generally we will make a domain extension point facade, under which the extension point facade will be defined hierarchically according to the entity. For example, in the transaction domain, there are entities such as Order and OrderLine, then the transaction domain can be extended to the outside world, and we define it as follows:
public interface TradeEntityExt extends IBusinessExt {
TradeOrderExt getTradeOrderExt();
TradeOrderLineExt getTradeOrderLineExt();
TradePageExt getTradePageExt();
}
The advantage of this definition is that developers and third-party ISVs can easily find their corresponding extension points by entity by browsing the extensible facade in the field. Let's continue to take the entity OrderLine as an example. The extension points provided by this entity are further classified according to the "behavior" of the entity, as follows:
public interface TradeOrderLineExt extends IBusinessExt {
/**
* @return The extension of Order Line entity in init behavior.
*/
TradeOrderLineInitExt getTradeOrderLineInitExt();
/**
* @return The extension of Order Line entity in check behavior.
*/
TradeOrderLineCheckExt getTradeOrderLineCheckExt();
/**
* @return The extension of Order Line entity in saving behavior.
*/
TradeOrderLineSaveExt getTradeOrderLineSaveExt();
}
When we want to extend an entity, a natural idea is to consider what behaviors need to be extended to the entity. For example, is the entity to be custom processed before saving, or to be supplemented with additional information after saving? and many more. Under the specific entity behavior extension point facade, is the specific extension point definition, such as the above OrderLine entity, we define the following extension points in its "save" behavior:
public interface TradeOrderLineSaveExt extends IBusinessExt {
@Extension(reduceType = ReduceType.NONE)
Map<String, String> getCustomOrderLineAttributes(SaveOrderLineExtInput input);
@Extension(reduceType = ReduceType.NONE)
Void processOrderLineBeforeSaving(SaveOrderLineExtInput input);
@Extension(reduceType = ReduceType.NONE)
Void finalProcessOrderLineAfterSaving(SaveOrderLineExtInput input);
}
Different from the business entity layer, the scenarios defined in the business use case layer often span business activities and are organized and managed in the form of business processes. Therefore, at the business use case layer, I suggest that the extension point facade is aggregated according to the business activities defined in the scenario. When we discuss requirements, we often discuss how to implement them in terms of business activities. For example, we need to be able to customize an order prompt component when "buyer places an order"; we need to be able to define the latest delivery time when "seller ships", and so on. Therefore, organizing the extension points of the use case layer according to "business activities" will be more conducive to later maintenance and knowledge dissemination.
Let's take pre-sale trading business assets as an example. The extension point facade it provides is PreSaleTradeExt, which is defined as follows:
public interface PreSaleTradeExt extends IBusinessExt {
/**
* @return The extensions of PreSale trade in buyer place order.
*/
PreSalePlaceOrderExt getPreSalePlaceOrderExt();
/**
* @return The extensions of PreSale trade in fulfillment.
*/
PreSaleFulfillmentExt getPreSaleFulfillmentExt();
/**
* @return The extensions of PreSale trade in refund.
*/
PreSaleRefundExt getPreSaleRefundExt();
}
I have aggregated them according to several business activities such as seller ordering, seller performance, and buyer refund. We further take the buyer's order as an example, and we can see that the pre-sale transaction business asset defines the following extension points in this business activity:
public interface PreSalePlaceOrderExt extends IBusinessExt {
@Extension(name = "Custom PreSale Down Payment Ratio", reduceType = FIRST)
Double getCustomDownPaymentRatio(PreSaleOrderLine orderLine);
}
In the pre-sale scenario, business developers and third-party ISVs can easily find this extension point according to business activities from the pre-sale transaction SDK facade, and realize business customization. I take a certain business A as an example, and its customized implementation is as follows:
@Realization(codes = SampleBusinessA.CODE)
public class BusinessAPreSaleExt extends BlankPreSaleTradeExt {
@Override
public BlankPreSalePlaceOrderExt getPreSalePlaceOrderExt() {
return new BlankPreSalePlaceOrderExt() {
@Override
public Double getCustomDownPaymentRatio(PreSaleOrderLine orderLine) {
return 0.4;
}
};
}
}
The clean structure is a structure that the author has experienced in various large-scale engineering projects, and has indeed gained a lot of insight and experience in actual combat. Based on the Lattice framework, it can manage business extension points well and accumulate business assets. I hope this article can be helpful to programmers and architects.
中文版:https://www.ryu.xin/2022/11/05/lattice-clean-arch-practice/
Thanks..
Getting Started
Key Features
- Business Overlay Product
- Register Business Configuration
- Load Local Configuration
- Reduce Strategy
- UseCase Precipitation and Reuse
- RPC Invoke Extension
- Dynamic Loading
Key Concepts
Stories