# DDD **Repository Path**: chrisgardner/ddd ## Basic Information - **Project Name**: DDD - **Description**: DDD - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-07-05 - **Last Updated**: 2021-07-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 主要内容 1. [动机](#动机) 2. [业务背景](#业务背景) 3. [项目结构](#项目结构) 4. [测试](#测试) 5. [引用](#引用) ## 动机 为面对复杂的业务逻辑开发,沉淀并隔离对不同中间件和重复商业组件的依赖,我们需要有一套新的脚手架来规范开发。 我们采用了[六边形架构](https://www.jianshu.com/p/c2a361c2406c)的理论来建模我们的商业逻辑和代码,我们称之为Dayu Boot。 所谓六边形架构,也称为洋葱架构。简单来说是把整个服务分层两部分,一部分是六边形的内部,也即你的业务逻辑,一部分是六边形的外部,也称为infrastructure,提供了多个adapter,只关注如何暴露服务(例如,HTTP还是HSF),如何进行持久化(redis或者mysql),如何异步发送和接收消息(各种消息中间件的实现)。 与之前分层的架构不同,在六边形架构中,infrastructure依赖domain,实现domain中定义的各个和具体技术相关的接口,通过框架(spring)注入到domain中来完成业务逻辑。 ![六边形架构](doc/images/hegaxon.png) 本项目通过开发一个实际的业务需求,来演示如何使用Dayu Boot。 ## 业务背景 该项目实现一个简单的需求:账户,租户以及账号租户的关系维护。 1. 一个账号可以属于多个租户。 2. 一个租户包含多个账号。 3. 用户可以通过租户和账号进行查询。 账号和租户是多对多的关系。 ![ER](doc/images/relation.png) 微服务暴露: 1. 多个http接口,对账户,租户和他们之间的关系进行增删改查。 2. 在账户创建成功后,向一个topic发布消息。 3. 暴露一个topic,通过向这个topic发送消息实现将一个账户移出租户。 ## 项目结构 本例采用了多模块的方式,如下: ``` └── api | ├── in | ├── out | └── messaging └── http-client ├── application ├── domain | ├── events | ├── model | ├── repository | ├── service ├── infrastructure | ├── entities | ├── repository | ├── messaging | ├── web | ├── DatabaseConfiguration.java | ├── MessagingConfiguraiton.java | ├── AccountTenantConfiguration.java ├── boot | ├── Application.java | ├── resources ├── bootstrap.yml ``` 下面介绍下各个模块的作用。 ### [api](api) api模块定义了微服务和外界交互的API的入参,出参和返回,除了lombok,为了保持实现的中立,这里没有其他三方依赖。 ### [http-client](http-client) 微服务直接通信,我们通常使用RPC(例如dubbo,HSF),或者HTTP(例如Feign client,REST API)。这个模块依赖[api](#api)模块,用于实现不同的客户端。例如本例中是feign client。 例如: ```java @RestController @RequestMapping("/tenant") @FeignClient(value = "gts-delivery-account-center", contextId = "tenant") public interface TenantAPIHttp extends TenantAPIService { @Override ResultResponse createTenant(CreateTenantCommand createTenantCommand); } ``` api 和 http-client都是需要使用本应用服务要依赖的客户端二方库。二方库开发同学可以通过实现api定义的接口,实现多个不同方式的二方库。当然,它们都需要服务端的支持。 ### [application](application) 应用层,依赖domain和api模块,实现对多个domain的服务编排,安全和事务的控制,和一些和独立的domain无关的逻辑。应用层也依赖spring的AOP和TX,profile,security,transaction都应该实现在这里。 ### [domain](domain) 域服务。我们的核心业务逻辑。该模块除了一些工具类,不依赖任何其他三方库。这个特点使我们非常容易地进行单元测试。 同时,该层通过面向接口,抽象了对数据库,消息,http等的访问。 #### 域服务面向的接口: * EventPublisher,抽象了消息中间件。 ```java public interface AccountEventPublisher extends EventPublisher { } ``` * repository包中定义的所有接口,抽象了持久化方法 注意,在我们的例子中,租户和账号,这两个明显是两个聚合根,他们之间的交互理应通过event,甚至按照微服务的理论,他们应该在两个服务中通过进程间通信! 但是实际在项目中,我们无法避免table join,无法避免强一致。所以例子中我们有意识地把两个domain放在一起来演示这类架构是如何处理多domain的场景。 ### [infrastructure](infrastructure) 基础设施层,也就是我们六边形中定义的adapter。不过我们并没有命名adapter,因为这样命名很布尔乔亚。 该模块依赖了domain,它本质上实现了domain中的抽象接口。 #### [entities](infrastructure/src/main/java/com/aliyun/gts/delivery/account/infrastructure/entities) 定义了和数据库相关的类。 #### [repository](infrastructure/src/main/java/com/aliyun/gts/delivery/account/infrastructure/repository) 实际上实现了定义在domain中的持久化方法。 #### [messaging](infrastructure/src/main/java/com/aliyun/gts/delivery/account/infrastructure/messaging) 通过依赖bpaas中间件,我们实现了rocketmq发送和接受消息。 注意:其中的定义的consumer接受一个来自TOPIC的消息,实现了把一个账号从租户中移出的操作。这个操作并没有http的实现。 #### [web](infrastructure/src/main/java/com/aliyun/gts/delivery/account/infrastructure/web) 实现了feign client中定义的接口,所以该模块依赖了[http-client](http-client)。 ## 如何运行 ### 配置 你有两种方式进行配置 1. 修改源代码boot模块中的[application.yml](boot/src/main/resources/application.yml)文件,配置mq信息。 2. 本地搭建nacos服务,配置mq信息。 ### 编译和启动 1. 配置本地Maven仓库,可以下载[这个配置](https://aliyun-dayu-resource.oss-cn-shanghai.aliyuncs.com/settings-code-aliyun.xml) 2. 本地启动nacos 3. 典型的spring boot,启动boot中的Application.java类。 访问 [http://localhost:8080/swagger-ui.html#/](http://localhost:8080/swagger-ui.html#/) 尝试调用account和tenant相关的方法。 ## 测试 除了使用普通的junit测试,我们使用了archunit来保证一些架构上的clean。例如: ```java @ArchTest static final ArchRule domain_should_not_depend_on_spring = noClasses() .that() .resideInAPackage("..domain..") .should() .dependOnClassesThat() .resideInAPackage("org.springframework.."); ``` ## 引用 1. [Clean Architecture](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164) by Robert C. Martin 2. [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) by Eric Evans 3. [浅谈领域驱动设计(三):六边形架构](atatech.org/articles/164337?spm=ata.13269325.0.0.1fe949faybajfj)