领域模型和数据模型还傻傻分不清? 如何实现领域模型

数据模型和领域模型

数据模型比较好理解,我们常用 E-R 图来表示数据模型。数据模型表示存下来的实体的结构和关系,例如数据库里面的表 (实体结构)和外键 (关系)是数据模型的一种实现方式。

领域模型则是业务上面使用的模型,表示的也是业务对象的结构和对象之间的关系,但它和技术无关,只和领域有关,意味着不一定是程序员,只要对领域了解的人也能看得懂。领域模型是面向对象的,可以通过类图来表示,支持对象间的多种关系, 例如关联,组合,聚合,实现,继承等。

领域模型最好的实现方式就是通过内存实现了,不用管存的事情。我们程序员的价值之一就是要通过和领域专家沟通挖掘出领域模型。<分析模型> 里面有讲到,表面上是用杆撞击白球,白球就会沿轨迹撞击其他球或者到了边缘反弹。但如果是程序员,则需要了解球是怎么运动的,加速度怎么变化,撞击后力的传递是如何的。这就是领域模型,有了领域模型才能更容易应对用例的变化。

程序员除了挖掘出领域模型,还要把这个领域模型实现了,并基于这个领域模型去完成业务用例。用内存实现是不太可能的,因为内存有限,并且不可持久化,因此需要用其他技术存下来。最常用的就是 mysql 这种关系型数据库,关系型数据库有自己的约束,例如不支持继承,不支持数组,不支持直接的对象关联,有自己一套的设计方法,例如范式的设计,sql 的编写和优化等等。基于存储方式来设计的模型就是数据模型。

数据模型可以表示领域模型

领域模型和数据模型的关系是先设计出领域模型,再通过考虑如何存储领域模型而设计出数据模型。而现在常常为了减少模型转换而用数据模型替代领域模型,导致面向对象被异化成了面向过程,同时设计也受制于数据模型。

在前一篇文章中介绍了DDD 聚合的作用,里面没有用到聚合的例子就是直接用数据模型来替代领域模型。DDD 中的聚合是新东西吗? 有什么用?

java 复制代码
@Data
public class Device {
    private String deviceId;
    private String deviceName;
    private String schoolId;
    private String bindTime;
}

使用数据模型来替代领域模型的问题在于

  1. 数据模型会导致更多的平铺属性,而非抽取出更多对象,导致可维护性降低。例如上面的 schoolId 和 bindTime 可以单独设计成值对象,但映射到数据库只能是通过另一个表或者 json来存,为了查询性能一般也不会弄太多表,如果是json 则需要上层业务自己序列化。
  2. 数据模型关心性能和效率,会适当冗余数据,增加缓存,冷热属性分离等,从而导致领域模型变复杂。
  3. 领域模型拓展困难,例如绑定的逻辑越发复杂,从领域模型上需要拆成两个聚合,设备和设备绑定。但因为数据库变更涉及到数据迁移,会更困难,导致聚合难以拆分。

当设计成聚合,也就是领域模型之后

java 复制代码
@Getter
public class Device {

    private String deviceId;
    private String deviceName;
    private DeviceBindInfo bindingInfo;


    public void updateDeviceName(String changeDeviceName) {
        
        if (changeDeviceName.length() > 10) {
            throw new RuntimeException("设备名长度过长");
        }

        if (deviceName.equals(changeDeviceName)) {
            throw new RuntimeException("设备名没变");
        }

        deviceName = changeDeviceName;
    }

    public void bindSchool(String bindSchoolId) {
        if (bindingInfo != null) {
            throw new RuntimeException("设备已绑定学校");
        }

        bindingInfo = DeviceBindInfo.bind(bindSchoolId);
    }
}

@Getter
public class DeviceBindInfo {

    private String schoolId;
    private LocalDateTime bindTime;

    private DeviceBindInfo(String schoolId, LocalDateTime bindTime) {
        this.schoolId = schoolId;
        this.bindTime = bindTime;
    }

    public static DeviceBindInfo bind(String schoolId) {
        return new DeviceBindInfo(schoolId, LocalDateTime.now());
    }
}
  1. 对象更加有层次,更清晰。
  2. 领域模型着重考虑如何更好的表达业务,无需考虑性能问题。
  3. 领域模型和数据模型各自演进,领域模型可以走的更快,只需要中间加转换器即可。

因此强烈建议领域模型和数据模型分开。在业务开始的时候可能领域模型和数据模型差异并不大,但经过几次迭代之后差异会越来越大。不分开会被数据模型拖着走,代码很难写好,也很难重构。

领域模型的实现方式

  1. 用数据模型来实现领域模型之外,这种前面提了并不推荐。小项目或者临时的项目可以使用。
  2. 领域模型和数据模型动态转换。例如使用 hibernate 框架。代码上面是领域模型,但可以使用各种注解,让领域模型可以通过配置转换成数据模型。对 hibernate 这类框架比较熟悉,领域模型比较简单的可以用。但使用门槛会比较高。
  3. 在仓储层手动进行模型转换。推荐使用这种。虽然代码会更多一些,相对方案2 的转换没那么清晰,但使用简单,不管多复杂的转换都能轻松拿捏。领域模型查询和保存都是基于聚合,因此大部分场景只需要 2个方法做相互转换即可,代码不会多太多。如果担心让仓储实现变复杂,可以增加模型转换器工具类。在分层模型中,仓储是用来隔离业务代码和技术实现的,数据模型就是一种技术实现,不管数据模型如何改变,从关系型变非关系型,都不影响上层业务。

性能如何解决

对于复杂的查询一般都需要结合几个聚合的数据,但如果都要整个聚合拿出来,性能肯定受影响。但又希望能充分利用面向对象的优势。有 2个方案。

  1. 懒加载。类似 hibernate 的懒加载,拿到的聚合的属性并非完全填充了,而是在 getX/setX 的方法做了代理,在调用 getX/setX 的方法的时候再去调数据库。这种方案缺点是要用好门槛较高,而且并不容易能优化到理想的性能。
  2. CQRS 命令查询职责分离。

CQRS 命令查询职责分离

分离的原因在于命令和查询的要求不同

  • 复杂度:命令的业务流程会比较复杂,例如下单,涉及到非常多聚合的行为和交互,容易出问题。而读一般就是 sql 的优化和数据的组装,逻辑不算特别复杂。
  • 重构难度:要对命令进行重构比较困难,因为命令涉及到多个状态的修改,只要一个状态改错了,就会产生脏数据。而查询的重构相对比较简单,只要查询出的数据和之前查的是一样的就可以了,就算有问题也不产生脏数据。
  • 性能:一般系统读多写少,命令的性能因为 qps较低,所以不需要做太多考虑。而读的 qps 一般会比较高,需要优化sql,增加冗余的列或者加缓存等等。

基于上面的考虑,我们期望命令涉及更偏可维护性,而查询则更偏向于性能。CQRS 和 CQS 的区别是多了个R,这个 Responsibility 我理解就是模型的意思,命令和查询是两个模型。刚好领域模型就适合命令,而数据模型则适合查询。

总结

领域模型和数据模型的演进方向和进度都不同,还是分开更好。领域模型和数据模型的转换用最简单的代码转换效果最好。性能问题可以使用 CQRS 解决。

相关推荐
CoderJia程序员甲5 分钟前
重学SpringBoot3-如何发送 Email
java·spring boot·后端·email
初晴~7 分钟前
【spring】参数校验Validation
java·c++·spring boot·后端·python·spring·validation
Jing_jing_X23 分钟前
心情追忆-首页“毒“鸡汤AI自动化
java·前端·后端·ai·产品经理·流量运营
wqq_99225027737 分钟前
springboot基于微信小程序的农产品交易平台
spring boot·后端·微信小程序
前端与小赵1 小时前
什么是RESTful API,有什么特点
后端·restful
LRcoding1 小时前
【Spring Boot】# 使用@Scheduled注解无法执行定时任务
java·spring boot·后端
码农飞飞3 小时前
详解Rust结构体struct用法
开发语言·数据结构·后端·rust·成员函数·方法·结构体
无忧无虑Coding7 小时前
pyinstall 打包Django程序
后端·python·django
求积分不加C8 小时前
Spring Boot中使用AOP和反射机制设计一个的幂等注解(两种持久化模式),简单易懂教程
java·spring boot·后端
枫叶_v8 小时前
【SpringBoot】26 实体映射工具(MapStruct)
java·spring boot·后端