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

数据模型和领域模型

数据模型比较好理解,我们常用 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 解决。

相关推荐
盖世英雄酱581364 分钟前
java 深度调试【第一章:堆栈分析】
java·后端
lastHertz21 分钟前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥22 分钟前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜27 分钟前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始31 分钟前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴44 分钟前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年1 小时前
用Go实现类似WinGet风格彩色进度条
后端
淳朴小学生1 小时前
静态代理和动态代理
后端
渣哥1 小时前
数据一致性全靠它!Spring 事务传播行为没搞懂=大坑
javascript·后端·面试