领域驱动设计(DDD)学习记录

b.xie

发布于 2022.07.25 19:40 阅读 1506 评论 0

1 简介

领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型[1]来满足复杂需求的软件开发方法(面向对象建模的方法论)。领域驱动设计的前提是:

  • 把项目的主要重点放在核心领域(core domain)和域逻辑

  • 把复杂的设计放在有界域(bounded context)的模型上

  • 发起一个创造性的合作之间的技术和域界专家以迭代地完善的概念模式,解决特定领域的问题

领域驱动设计是一种由域模型来驱动着系统设计的思想,不是通过存储数据词典(DB表字段、ES Mapper字段等等)来驱动系统设计。领域模型是对业务模型的抽象,DDD是把业务模型翻译成系统架构设计的一种方式。

 

诞生根源

任何一门技术,一种思想的诞生都离不开当时的生产力水平和社会环境的支撑。而DDD最开始诞生在2003年,当时恰逢第一次软件危机,即软件工程的设计跟不上快速流动的业务变化,为了解决这种业务快速变动的软件危机,一些优秀的思想和框架兴起了,诸如敏捷开发,诸如Spring,而DDD也是其中的一员,只不过在接下来的十几年当中一直是由简单粗暴的MVC统治市场,DDD罕有问津。

 

二次流行

DDD的再次流行离不开微服务本身的快速繁荣,从最开始的面向过程,面向对象,出现了软件危机,为了解决危机敏捷开发、MVC独当一面,之后是微服务,以及微服务的快速膨胀,人们热衷于将服务拆解之后再部署,直到他们将服务拆的过于零碎,而业务逻辑的调用又过于混乱,每一个服务都牵扯着好几十个服务,而每一个被牵扯的服务又被其他的好几个服务所依赖,一来二去,服务同服务之间的逻辑成为了一团乱麻,屎山代码往前一站,后人只敢打补丁而不敢翻新,团队甩锅且效率低下。于是第二次软件危机就形成了,即如何解决在微服务本身过于膨胀的背景下,由于业务混乱所造成的维护困境。

 

2 术语

战略设计

官方解释:在某个领域,核心围绕着上下文的设计

核心概念:上下文划分,上下文映射,上下文通用语言

 

战术设计

官方解释:核心关注上下文中的实体建模,定义值对象,实体等,更加偏向于开发细节

核心概念:值对象 实体 聚合 工厂 仓库 领域服务/事件

 

DDD设计的四种方法

  • 事件风暴

  • 领域故事讲述

  • 四色建模法

  • 用例法

 

值对象

官方解释:描述了领域中的一件东西,将不同的相关属性组合成了一个概念整体,当度量和描述改变时,可以用另外一个值对象予以替换,属性判等、固定不变。

 

聚合

官方解释:实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久化操作。聚合中,根实体的生命周期决定了聚合整体的生命周期。

 

Domain Primitive

官方解释:Domain Primitive 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。

  • DP是一个传统意义上的Value Object,拥有Immutable的特性

  • DP是一个完整的概念整体,拥有精准定义

  • DP使用业务域中的原生语言

  • DP可以是业务域的最小组成部分、也可以构建复杂组合

Domain Primitive三原则:

  • 隐性逻辑的显性化

  • 隐性上下文的显性化

  • 封装多对象行为

 

 

3 经典四层架构

介绍:

 

 

4 结构设计

四层架构:interfaces application domain infrastructure

概念申明:值对象(Query结尾) 领域对象(即Entity) 持久化对象(PO)

├─application
│  └─impl
├─domain
│  ├─exception
│  ├─factory
│  ├─entity
│  │  ├─enums
│  │  └─root
│  ├─query
│  └─repository
├─infrastructure
│  ├─config
│  │  └─validatorInterface
│  └─db
│      ├─builder
│      ├─enums
│      ├─mapper
│      ├─po
│      ├─repository
│      └─service
│          └─impl
└─interfaces
    └─web

 

4.1 用户接口层 interfaces

接口服务位于用户接口层,也可以称之为web层,该层主要用于处理用户发送的请求和解析用户输入的配置文件等,并将信息传递给应用层。

 

用户接口直接根据情况放置Controller,Controller接收入参的是值对象

@RestController
@RequestMapping("/admin/busRoute")
public class BusRouteController {
​
    @Resource
    private BusRouteService busRouteService;
​
    @PostMapping("/select")
    public JsonResult select(@RequestBody BusRouteQuery busRouteQuery) throws IllegalParameterException {
        return busRouteService.find(busRouteQuery);
    }
​
    @ExceptionHandler(value = IllegalParameterException.class)
    public JsonResult e1(){
        JsonResult result = new JsonResult();
        result.setSuccess(false);
        result.setTip("检测到非法参数异常");
        return result;
    }
​
}

 

用于查询入参的值对象Query可以放在web中也可以放在Domain中,这里我放在了Domain层

@Data
public class BusRouteQuery {
​
    Long id;
​
    String name;
​
    public BusRouteQuery(){
​
    }
​
    public BusRouteQuery(@NotNull Long id, @NotNull String name) {
        this.id = id;
        this.name = name;
    }
​
    public void checkBusRoute() throws IllegalParameterException {
        if(this.id == null){
            throw new IllegalParameterException();
        }
    }
​
}

 

 

4.2 应用层 application

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

应用层的服务包括应用服务和领域事件相关服务,这里暂时没有用到领域事件。

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

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

 

包结构:

├─application
   └─impl

 

应用层根目录直接放Service接口

public interface BusRouteService {
​
    JsonResult find(BusRouteQuery busRouteQuery);
​
    JsonResult save(BusRouteQuery busRouteQuery, StatusEnum status);
}

 

impl中实现类,实现类负责值对象和领域对象之间的转换,但要注意的是,因为行为本身被内扣到领域中去,所以领域需要去操作相应的仓库来发布行为,但仓库的实现不能在实体中直接调用,因此这里把仓储传递进领域对象中。

@Service
public class BusRouteServiceImpl implements BusRouteService {
​
    @Resource
    BusRouteRepository busRouteRepository;// 获取仓储
​
    @Override
    public JsonResult find(BusRouteQuery busRouteQuery) {
        BusRoute busRoute = BusRouteFactory.toBusRoute(busRouteQuery);// 转换类型
        busRoute.setBusRouteRepository(busRouteRepository);// 递交仓储给实体
        return busRoute.find();// 调用行为
    }
​
    @Override
    public JsonResult save(BusRouteQuery busRouteQuery, StatusEnum status) {
        BusRoute busRoute = BusRouteFactory.toBusRoute(busRouteQuery);
        busRoute.setBusRouteRepository(busRouteRepository);
        return busRoute.save(status);
    }
}

 

 

4.3 领域层 domain

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

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

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

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

 

包结构:

├─domain
   ├─exception  异常
   ├─factory    工厂
   ├─model      实体
   │  ├─enums   枚举
   │  └─root    聚合根
   ├─query      值对象
   └─repository 仓储接口

 

exception包中存放自定义异常

// 非法参数
public class IllegalParameterException extends Exception  {
​
    public IllegalParameterException() {
        super();
    }
​
    public IllegalParameterException(String str) {
        super(str);
    }
}

 

factory提供值对象跟领域对象之间的转换

public class BusRouteFactory {
    
    // 值对象转领域对象
    public static BusRoute toBusRoute(BusRouteQuery busRouteQuery){
        BusRoute busRoute = new BusRoute(busRouteQuery.getId(), busRouteQuery.getName());
        return busRoute;
    }
​
}

 

model即entity,直接目录中用来放领域对象,领域对象中既有数据又有行为

@Data
@NoArgsConstructor
public class BusRoute extends ModelRoot {
​
    Long routeFormalId;
​
    List<BusRouteSiteQuery> site;
​
    BusRouteLineQuery line;
​
    BusRouteRepository busRouteRepository;
​
    public BusRoute(Long id, String name, Long routeFormalId) {
        this.setId(id);
        this.setName(name);
        this.routeFormalId = routeFormalId;
    }
​
    public BusRoute(Long id, String name) {
        this.setId(id);
        this.setName(name);
    }
​
​
    // 查找
    public JsonResult find() {
        JsonResult result = new JsonResult();
        result.setSuccess(true);
        result.setObject(busRouteRepository.find(this));
        return result;
    }
​
    // 增删改操作
    public JsonResult save(StatusEnum status) {
        return busRouteRepository.save(this, status);
    }
​
​
}

enum目录下放一些用到的枚举,因为这里我用一个save方法同时包含了增删改三个操作,因此需要给一个标志位来对具体的操作进行区分

public enum StatusEnum {
    INSERT,
    UPDATE,
    DELETE
}

root下放聚合根,聚合根是整个聚合中公共抽象上层,在领域层应该都由聚合根来统领,禁止绕过根来直接访问领域实体

public class ModelRoot {
    
    private Long id;

    private String name;
    
}

query中是值对象,在用户层已经给过样例了

repository中是仓储的接口,这里只负责向外抛出行为

public interface BusRouteRepository {
​
    BusRoute find(BusRoute busRoute);
​
    JsonResult save(BusRoute busRoute, StatusEnum statusEnum);
}

 

4.4 基础设施层 infrastructure

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

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

 

包结构:

├─infrastructure
   ├─config
   └─db
       ├─builder
       ├─enums
       ├─mapper
       ├─po
       ├─repository
       └─service
           └─impl

 

config中存放面向数据库的诸多配置,这里就不再一一细说了。

enums 枚举可有可无。

db下的builder存放将领域对象和持久化对象相互转化的工具类

public class BusRouteBuilder {
​
    /**
     * 转成领域对象
     * @param smcoBusRoute
     * @return
     */
    public static BusRoute toBusRoute(SmcoBusRoute smcoBusRoute){
        BusRoute busRoute = new BusRoute(smcoBusRoute.getRouteId(), smcoBusRoute.getRouteName());
        return busRoute;
    }
​
    /**
     * 转成领域对象
     * @param smcoBusRoute
     * @return
     */
    public static BusRoute toBusRoute(SmcoBusRoute smcoBusRoute, List<BusRouteSiteQuery> sites, BusRouteLineQuery line){
        BusRoute busRoute = new BusRoute(smcoBusRoute.getRouteId(), smcoBusRoute.getRouteName());
        busRoute.setSite(sites);
        busRoute.setLine(line);
        return busRoute;
    }
​
    /**
     * 转成领域对象
     * @param smcoBusRouteList
     * @return
     */
    public static List<BusRoute> toBusRoute(List<SmcoBusRoute> smcoBusRouteList){
        List<BusRoute> list = new ArrayList<>();
        for (SmcoBusRoute smcoBusRoute : smcoBusRouteList) {
            list.add(BusRouteBuilder.toBusRoute(smcoBusRoute));
        }
        return list;
    }
​
​
    /**
     * 转成值对象
     * @param busRoute
     * @return
     */
    public static SmcoBusRoute toBusRoutePO(BusRoute busRoute){
        SmcoBusRoute smcoBusRoute = new SmcoBusRoute();
        smcoBusRoute.setRouteId(busRoute.getId());
        smcoBusRoute.setRouteName(busRoute.getName());
        return smcoBusRoute;
    }
​
    /**
     * 转成值对象
     * @param busRoute
     * @return
     */
    public static SmcoBusRoute toBusRouteDraftPO(BusRoute busRoute){
        SmcoBusRoute smcoBusRoute = new SmcoBusRoute();
        smcoBusRoute.setRouteId(busRoute.getRouteFormalId());
        smcoBusRoute.setRouteName(busRoute.getName());
        return smcoBusRoute;
    }
​
​
    /**
     * 获取查询wrapper
     * @param smcoBusRoute
     * @return
     */
    public static QueryWrapper<SmcoBusRoute> getWrapper(SmcoBusRoute smcoBusRoute){
        QueryWrapper<SmcoBusRoute> wrapper = new QueryWrapper<>();
        wrapper.eq(smcoBusRoute.getRouteId()!=null,"route_id",smcoBusRoute.getRouteId())
        ;
        return wrapper;
    }
​
    /**
     * 获取查询站点的wrapper
     * @param smcoBusRoute
     * @return
     */
    public static QueryWrapper<SmcoRouteSite> getSiteWrapper(SmcoBusRoute smcoBusRoute){
        QueryWrapper<SmcoRouteSite> wrapper = new QueryWrapper<>();
        wrapper.eq(smcoBusRoute.getRouteId()!=null,"rosi_route_id",smcoBusRoute.getRouteId())
        ;
        return wrapper;
    }
​
​
    /**
     * 获取查询路线的wrapper
     * @param line
     * @return
     */
    public static QueryWrapper<SmcoRouteLine> getLineWrapper(SmcoBusRoute line){
        QueryWrapper<SmcoRouteLine> wrapper = new QueryWrapper<>();
        wrapper.eq(line.getRouteId()!=null,"roline_route_id",line.getRouteId())
        ;
        return wrapper;
    }
​
​
}

 

mapper还是老样子,存放dao

@Repository
public interface SmcoBusRouteMapper extends BaseMapper<SmcoBusRoute> {
​
}

 

po 是persistance object,存放与数据库做直接映射的对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmcoBusRoute extends BaseEntity implements Serializable {
​
    private static final long serialVersionUID = 1L;
​
}

 

repository是领域层中repository的实现类存放地,重点的逻辑

@Service
public class BusRouteRepositoryImpl implements BusRouteRepository {
​
    @Resource
    ISmcoBusRouteService iSmcoBusRouteService;
​
    @Resource
    ISmcoBusRouteDraftService iSmcoBusRouteDraftService;
​
    @Resource
    ISmcoRouteSiteService iSmcoRouteSiteService;
​
    @Resource
    ISmcoRouteLineService iSmcoRouteLineService;
​
    @Override
    public BusRoute find(BusRoute busRoute) {
        SmcoBusRoute smcoBusRoute = BusRouteBuilder.toBusRoutePO(busRoute);
        QueryWrapper<SmcoBusRoute> wraper = BusRouteBuilder.getWrapper(smcoBusRoute);
        QueryWrapper<SmcoRouteSite> siteWrapper = BusRouteBuilder.getSiteWrapper(smcoBusRoute);
        QueryWrapper<SmcoRouteLine> lineWrapper = BusRouteBuilder.getLineWrapper(smcoBusRoute);
        // 要将路线、站点、折线都载入
        BusRoute busRouteFromDb = BusRouteBuilder.toBusRoute(iSmcoBusRouteService.getOne(wraper),
                BusRouteSiteBuilder.toBusRouteSiteQuery(iSmcoRouteSiteService.list(siteWrapper)),
                BusRouteLineBuilder.toBusRouteLineQuery(iSmcoRouteLineService.getOne(lineWrapper)));
        return busRouteFromDb;
    }
​
    @Override
    public JsonResult save(BusRoute busRoute, StatusEnum status) {
        SmcoBusRoute smcoBusRoute = BusRouteBuilder.toBusRoutePO(busRoute);
        JsonResult jsonResult = new JsonResult();
        QueryWrapper<SmcoBusRoute> wrapper = BusRouteBuilder.getWrapper(smcoBusRoute);
        if (status.equals(StatusEnum.INSERT)) {// 插入
            boolean save = iSmcoBusRouteService.save(smcoBusRoute);
            jsonResult.setSuccess(save);
            if (save) {
                jsonResult.setTip("添加成功");
            } else {
                jsonResult.setTip("添加失败");
            }
        } else if (status.equals(StatusEnum.UPDATE)) {// 更新
            boolean update = iSmcoBusRouteService.update(smcoBusRoute, wrapper);
            jsonResult.setSuccess(update);
            jsonResult.setTip(update ? "更新成功" : "更新失败");
        } else if (status.equals(StatusEnum.DELETE)) { // 删除
            // 删除前判断被删除对象是否真实存在
            if (iSmcoBusRouteService.count(wrapper) < 1) {
                jsonResult.setSuccess(false);
                jsonResult.setFailReason("要删除的对象不存在");
            }
            boolean remove = iSmcoBusRouteService.remove(wrapper);
            jsonResult.setSuccess(remove);
            jsonResult.setTip(remove ? "删除成功" : "删除失败");
        } else {
            jsonResult.setSuccess(false);
            jsonResult.setFailReason("无效操作");
        }
        return jsonResult;
    }
​
​
}

 

service和mvc时期一样,因为拿了IService所以直接调

public interface ISmcoBusRouteService extends IService<SmcoBusRoute> {
}
@Service
public class SmcoBusRouteServiceImpl extends ServiceImpl<SmcoBusRouteMapper, SmcoBusRoute> implements ISmcoBusRouteService {
}