领域驱动设计

简介

学习文档:基于DDD的微服务设计和开发实战_文化 & 方法_欧创新_InfoQ精选文章

领域驱动实践总结(基本理论总结与分析+架构分析与代码设计V+具体应用设计分析)_l领域驱动实践总结-CSDN博客

Dao层service层controller层mannager层和biz层详解-CSDN博客

https://juejin.cn/post/6844903636334542856

设计思想

领域模型 + DDD分层思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合 DDD 分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。

通过领域模型和 DDD 的分层思想,屏蔽外部变化对领域逻辑的影响,确保交付的软件产品是边界清晰的微服务,而不是内部边界依然混乱的小单体。在需求和设计变化时,可以轻松的完成微服务的开发、拆分和组合,确保微服务不易受外部变化的影响,并稳定运行。

适用范围

不适用于只求结果,不求长远,或者说长远的事以后再设计的情况。

DDD架构

DDD 分层架构包括:展现层、应用层、领域层和基础层。

各层分工

展现层

展现层负责向用户显示信息和解释用户指令。

应用层

应用层是很薄的一层,主要面向用户用例操作,协调和指挥领域对象来完成业务逻辑。

应用层也是与其他系统的应用层进行交互的必要渠道。应用层服务尽量简单,它不包含业务规则或知识,只为下一层的领域对象协调任务,使它们互相协作。应用层还可进行安全认证、权限校验、分布式和持久化事务控制或向外部应用发送基于事件的消息等。

领域层

领域层是软件的核心所在,它实现全部业务逻辑并且通过各种校验手段保证业务正确性。它包含业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。它负责表达业务概念、业务状态以及业务规则,具体表现形式就是领域模型。

基础层

基础层为各层提供通用的技术能力,包括:为应用层传递消息、提供 API 管理,为领域层提供数据库持久化机制等。它还能通过技术框架来支持各层之间的交互。

服务视图和数据视图

微服务内服务视图

微服务内有 Facade接口、应用服务、领域服务和基础服务,各层服务协同配合,为外部提供服务。

接口服务

接口服务位于用户接口层,用于处理用户发送的 Restful请求和解析用户输入的配置文件等,并将信息传递给应用层。

应用服务

应用服务位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装。

应用层的服务包括应用服务和领域事件相关服务。

应用服务可对微服务内的领域服务以及微服务外的应用服务进行组合和编排,或者对基础层如文件、缓存等数据直接操作形成应用服务,对外提供粗粒度的服务。

领域事件服务包括两类:领域事件的发布和订阅。通过事件总线和消息队列实现异步数据传输,实现微服务之间的解耦。

领域服务

领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程。

领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露。

为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露。

为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。

基础服务

基础服务位于基础层。为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响。

基础服务主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务,领域服务和应用服务调用仓储服务接口,利用仓储实现持久化数据对象或直接访问基础资源。

微服务外服务视图

前端应用与微服务

微服务中的应用服务通过用户接口层组装和数据转换后,发布在 API网关,为前端应用提供数据展示服务。

微服务与外部应用

跨微服务数据处理时,对实时性要求高的场景,可选择直接调用应用服务的方式(新增和修改类型操作需关注事务一致性)。对实时性要求不高的场景,可选择异步化的领域事件驱动机制(最终数据一致性)。

数据视图

数据视图应用服务通过数据传输对象(DTO)完成外部数据交换。领域层通过领域对象(DO)作为领域实体和值对象的数据和行为载体。基础层利用持久化对象(PO)完成数据库的交换。

DTO 与 VO 通过 Restful 协议实现 JSON 格式和对象转换。

前端应用与应用层之间 DTO 与 DO 的转换发生在用户接口层。如微服务内应用服务需调用外部微服务的应用服务,则 DTO 的组装和 DTO 与 DO 的转换发生在应用层。

领域层 DO 与 PO 的转换发生在基础层。

领域事件和事件总线

领域事件是领域模型中非常重要的部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,有助于形成完整的业务闭环。领域事件主要用于解耦微服务,各个微服务之间不再是强一致性,而是基于事件的最终一致性。

微服务内的领域事件

微服务内的领域事件可以通过事件总线或利用应用服务实现不同聚合之间的业务协同。当微服务内发生领域事件时,由于大部分事件的集成发生在同一个线程内,不一定需要引入消息中间件。但一个事件如果同时更新多个聚合数据,按照 DDD“一个事务只更新一个聚合根”的原则,可以考虑引入消息中间件,通过异步化的方式,对微服务内不同的聚合根采用不同的事务。

微服务之间的领域事件

微服务之间的数据交互方式通常有两种:应用服务调用和领域事件驱动机制。

应用服务调用方式通常应用于实时性要求高的业务场景,但一旦涉及到跨微服务的数据修改,将会增加分布式事务控制成本,影响系统性能,微服务之间的耦合度也会变高。

领域事件驱动机制更多地用于不同微服务之间的集成,实现微服务之间的解耦。事件库(表)可以用于微服务之间的数据对账,在应用、网络等出现问题后,可以实现源和目的端的数据比对,在数据暂时不一致的情况下仍可根据这些数据完成后续业务处理流程,保证微服务之间数据的最终一致性。

事件总线

事件总线位于基础层,为应用层和领域层服务提供事件消息接收和分发等服务。大致流程如下:

  1. 服务触发并发布事件。

  2. 事件总线事件分发。

  • 如果是微服务内的订阅者(微服务内的其它聚合),则直接分发到指定订阅者。

  • 如果是微服务外的订阅者,则事件消息先保存到事件库(表)并异步发送到消息中间件。

  • 如果同时存在微服务内和外订阅者,则分发到内部订阅者,并将事件消息保存到事件库(表)并异步发送到消息中间件。为了保证事务的一致性,事件表可以共享业务数据库。也可以采用多个微服务共享事件库的方式。当业务操作和事件发布操作跨数据库时,须保证业务操作和事件发布操作数据的强一致性。

事件数据持久化

事件数据的持久化可以保证数据的完整性,基于这些数据可以完成跨微服务数据的一致性比对。

事件数据的持久化存储可以有两种方案,在项目实施过程中根据具体场景选择最佳方案。

  1. 事件数据保存到微服务所在业务数据库的事件表中,利用本地事务保证业务操作和事件发布操作的强一致性。

  2. 事件数据保存到多个微服务共享的事件库中。需要注意的一点是:这时业务操作和事件发布操作会跨数据库操作,须保证事务的强一致性(如分布式事务机制)。

微服务设计方法

事件风暴

本阶段主要完成领域模型设计。

基于 DDD 的微服务设计通常采用事件风暴方法。通过事件风暴完成领域模型设计,划分出微服务逻辑边界和物理边界,定义领域模型中的领域对象,指导微服务设计和开发。事件风暴通常包括产品愿景、场景分析、领域建模、微服务设计和拆分等过程。

产品愿景

产品愿景是对产品的顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。建议参与角色:业务需求方、产品经理和开发组长。

场景分析

场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。

建议参与角色:产品经理、需求分析人员、架构师、开发组长和测试组长。

领域建模

领域建模是通过对业务和问题域进行分析,建立领域模型,向上通过限界上下文指导微服务边界设计,向下通过聚合指导实体的对象设计。

建议参与角色:领域专家、产品经理、需求分析人员、架构师、开发组长和测试组长。

微服务拆分和设计

结合业务限界上下文与技术因素,对服务的粒度、分层、边界划分、依赖关系和集成关系进行梳理,完成微服务拆分和设计。

微服务设计应综合考虑业务职责单一、敏态与稳态业务分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等因素。

建议参与角色:产品经理、需求分析人员、架构师、开发组长和测试组长。

实践

实践1

架构

加入业务逻辑

request
@Getter
@Setter
@ToString
@Schema(description = "创建后台类目请求")
public class CreateCategoryRequest extends ApiRequest {
    @Schema(description = "父类目Id", example = "0", required = true)
    @NotNull(message = "父类目Id不可为空")
    private Long parentId;
    @Schema(description = "类目名称", example = "一级类目测试", required = true)
    @NotBlank(message = "类目名称不可为空")
    private String categoryName;
    @Schema(description = "可否有子类目", example = "true", required = true)
    @NotBlank(message = "可否有子类目不可为空")
    private Boolean hasChildren;
    @Schema(description = "类目名称缩写", example = "yjlmcs")
    private String categoryNameEn;
}
facade
@RequestMapping("category")
public interface CategoryFacade {
  @PostMapping("create")
  ApiResult<CreateCategoryData> create(@RequestBody CreateCategoryRequest request);
}
controller
@Slf4j
@RestController
@Api(tags = "分类接口")
public class CategoryController implements CategoryFacade {
    @Resource
    private CategoryBizService categoryBizService;
    @Override
    public ApiResult<CreateCategoryData> create(CreateCategoryRequest request) {
        CreateCategoryData createCategoryData = categoryBizService.create(request);
        return ApiResult.ok(createCategoryData);
    }
}
bizService
public interface CategoryBizService {
    CreateCategoryData create(CreateCategoryRequest request);
}
bizServiceImpl
@Service
public class CategoryBizServiceImpl implements CategoryBizService {
    @Resource
    private CategoryConvert categoryConvert;
    
    @Override
    public DeleteCategoryData delete(DeleteCategoryRequest request) {
        // 获取领域对象
        CategoryDomain categoryDomain = DomainFactory.create(CategoryDomain.class);
        // 赋值
        categoryConvert.toDomain(categoryDomain, request);
        // 触发领域行为
        categoryDomain.delete();
        // 封装返回结果
        DeleteCategoryData deleteCategoryData = new DeleteCategoryData();
        return deleteCategoryData;
    }
}
DomainFactory
public class DomainFactory {
    /**
     * Create t.
     *
     * @param <T>       the type parameter
     * @param entityClz the entity clz
     * @return the t
     */
    public static <T> T create(Class<T> entityClz) {
        return ApplicationContextHelper.getBean(entityClz);
    }
}
domain
@Slf4j
@Getter
@Setter
@DomainEntity
public class CategoryDomain {
    // pojo 全部属性
    
    private CategoryStatusEnum categoryStatusEnum = CategoryStatusEnum.INIT;
​
    @Resource
    private StateMachine categoryStateMachine;
​
    /**
     * Create.
     */
    public Long create() {
        CategoryStateContext context = new CategoryStateContext();
        context.setCategoryDomain(this);
        // 触发状态机
        fireEvent(CategoryStateEventEnum.CREATE, context);
        return this.getCategoryId();
    }
​
    private void fireEvent(CategoryStateEventEnum event, CategoryStateContext context) {
        categoryStateMachine.fireEvent(this.getCategoryStatusEnum(), event, context);
    }
​
}
stateMachine
@Slf4j
@Component
public class CategoryStateMachineConfig {
    @Resource
    private CreateCategoryAction createCategoryAction;
    @Resource
    private ModifyCategoryAction modifyCategoryAction;
    @Resource
    private DeleteCategoryAction deleteCategoryAction;
​
    @Bean
    public StateMachine<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext> categoryStateMachine() {
        StateMachineBuilder<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext> builder = StateMachineBuilderFactory.create();
​
        //创建
        builder.internalTransition().within(CategoryStatusEnum.INIT).on(CategoryStateEventEnum.CREATE)
            .perform(new AbstractEventAction<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext>() {
                @Override
                public void doExecute(CategoryStatusEnum from, CategoryStatusEnum to, CategoryStateEventEnum event, CategoryStateContext context) {
                    createCategoryAction.doExecute(from, to, event, context);
                }
            });
​
        //修改
        builder.internalTransition().within(CategoryStatusEnum.INIT).on(CategoryStateEventEnum.MODIFY)
            .perform(new AbstractEventAction<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext>() {
                @Override
                public void doExecute(CategoryStatusEnum from, CategoryStatusEnum to, CategoryStateEventEnum event, CategoryStateContext context) {
                    modifyCategoryAction.doExecute(from, to, event, context);
                }
            });
​
        //删除
        builder.internalTransition().within(CategoryStatusEnum.INIT).on(CategoryStateEventEnum.DELETE)
            .perform(new AbstractEventAction<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext>() {
                @Override
                public void doExecute(CategoryStatusEnum from, CategoryStatusEnum to, CategoryStateEventEnum event, CategoryStateContext context) {
                    deleteCategoryAction.doExecute(from, to, event, context);
                }
            });
​
        StateMachine<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext> stateMachine = builder.build(StateMachineName.CATEGORY_STATE_MACHINE_NAME);
        String plantUml = stateMachine.generatePlantUML();
        log.info(plantUml);
        return stateMachine;
    }
}
AbstractEventAction
public abstract class AbstractEventAction<S, E, C extends StateContext> implements Action<S, E, C> {
​
    @Override
    public void execute(final S from, final S to, final E event, final C context) {
        doExecute(from, to, event, context);
    }
​
    public abstract void doExecute(final S from, final S to, final E event, final C context);
}
action

可以直接使用 repository层,但是如果有那种 service层需要下沉,dao层需要集成的功能,那就放到 manager层做。manager层是更通用的,公共代码,细化的,使得 service 中可以组合使用。

repository层就是 dao层的功能展现。

@Service
@Slf4j
public class CreateCategoryAction extends AbstractEventAction<CategoryStatusEnum, CategoryStateEventEnum, CategoryStateContext> {
    @Resource
    private CategoryManager categoryManager;
    @Resource
    private CategoryRepository categoryRepository;
​
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void doExecute(CategoryStatusEnum from, CategoryStatusEnum to, CategoryStateEventEnum event, CategoryStateContext context) {
        CategoryDomain domain = context.getCategoryDomain();
        CategoryInfo parent = categoryRepository.getById(domain.getParentId());
        if (Objects.isNull(parent)) {
            throw new GoodsException(ApiCode.PARAMETER_EXCEPTION.getMessage());
        }
        CategoryInfo maxBrother = categoryRepository.getMaxBrotherByParentId(domain.getParentId());
        // 新增
        CategoryInfo categoryInfo = new CategoryInfo();
        categoryConvert.toEntity(categoryInfo, domain, parent);
        categoryRepository.insert(categoryInfo);
        if (Objects.isNull(categoryInfo.getCategoryId())) {
            throw new GoodsException(ApiCode.FAIL.getMessage());
        }
        // 更新
        String treeCode;
        String categoryPath;
        if (Objects.equals(CommonConstant.LONG_ZERO, domain.getParentId())) {
            treeCode = CommonConstant.ONE_STRING;
            categoryPath = String.valueOf(categoryInfo.getCategoryId());
        } else {
            categoryPath = parent.getCategoryPath().concat("," + categoryInfo.getCategoryId());
            treeCode = Optional.ofNullable(maxBrother)
                .map(item -> item.getTreeCode().substring(0, item.getTreeCode().lastIndexOf('.') + 1) + (Integer.parseInt(item.getTreeCode().substring(item.getTreeCode().lastIndexOf('.') + 1)) + 1))
                .orElse(parent.getTreeCode().concat("." + CommonConstant.ONE_STRING));
        }
        categoryInfo.setTreeCode(treeCode);
        categoryInfo.setCategoryPath(categoryPath);
        categoryRepository.updateById(categoryInfo);
    }
}
AbstractAsyncEventAction
@Slf4j
public abstract class AbstractAsyncEventAction<S, E, C extends StateContext> extends AbstractEventAction<S, E, C> {
​
    @Async
    @Override
    public void execute(final S from, final S to, final E event, final C context) {
        super.execute(from, to, event, context);
    }
}
CategoryStatusEnum
public enum CategoryStatusEnum {
    /**
     * Init category status enum.
     */
    INIT((byte) 0, "初始化"),
    ;
    /**
     * The Code.
     */
    private byte code;
    /**
     * The Message.
     */
    private String message;
​
    CategoryStatusEnum(final byte code, final String message) {
        this.code = code;
        this.message = message;
    }
​
    public static CategoryStatusEnum findByName(final String name) {
        return findByPredicate(categoryStatusEnum -> categoryStatusEnum.name().equals(name));
    }
​
    public static CategoryStatusEnum findByCode(final byte code) {
        return findByPredicate(categoryStatusEnum -> categoryStatusEnum.getCode() == code);
    }
​
    private static CategoryStatusEnum findByPredicate(Predicate<CategoryStatusEnum> predicate) {
        return Arrays.stream(CategoryStatusEnum.values()).filter(predicate).findFirst().orElse(null);
    }
​
    public byte getCode() {
        return code;
    }
​
    public String getMessage() {
        return message;
    }
}
CategoryStateEventEnum
public enum CategoryStateEventEnum {
    /**
     * Create category state event enum.
     */
    CREATE,
    /**
     * Delete category state event enum.
     */
    DELETE,
    /**
     * Modify category state event enum.
     */
    MODIFY,
    /**
     * Query category state event enum.
     */
    QUERY,
    ;
​
    public CategoryStateEventEnum findByName(final String name) {
        return Arrays.stream(CategoryStateEventEnum.values())
            .filter(categoryStateEventEnum -> categoryStateEventEnum.name().equals(name))
            .findFirst().orElse(null);
    }
}
CategoryStateContext
@EqualsAndHashCode(callSuper = true)
@Data
public class CategoryStateContext extends StateContext {
    /**
     * The Category domain.
     */
    private CategoryDomain categoryDomain;
}
AbstractEventAction
public abstract class AbstractEventAction<S, E, C extends StateContext> implements Action<S, E, C> {
    @Override
    public void execute(final S from, final S to, final E event, final C context) {
        doExecute(from, to, event, context);
    }
    
    public abstract void doExecute(final S from, final S to, final E event, final C context);
}
StateMachineName
public interface StateMachineName {
​
    String GOODS_STATE_MACHINE_NAME = "goodsStateMachine";
​
    String CATEGORY_STATE_MACHINE_NAME = "categoryStateMachineName";
​
    String ATTRIBUTE_STATE_MACHINE_NAME = "attributeStateMachineName";
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值