畛域驱动设计外围是围绕着畛域模型, 其实是一种面向畛域模型的设计形式. 咱们通过大量与领域专家的沟通合作, 失去畛域模型. 通过代码落地, 让畛域模型能够落地并失去验证.
畛域模型始终在咱们的产品开发的生命周期外面承当了很重要的作用. 因而咱们先看看畛域模型是什么, 咱们应用畛域模型的收益是什么.
既然畛域模型这么重要, 咱们先有个直观的意识, 它是长什么样子的.
什么是模型?
畛域驱动设计外围是围绕着畛域模型的, 那什么是模型? 上面这幅图是一个城市的鸟瞰图, 外面蕴含了十分多的信息, 包含河流, 修建, 公路等等, 但如果我想坐地铁从 A 点到 B 点, 那能够怎么做呢?
我能够找一个地铁线路图, 这个地铁线路图其实就是一个模型.
那模型有什么特点呢? 咱们能够总结一下
- 形象的, 通过精简的, 只为解决一部分问题. 例如他只能解决我坐地铁的问题, 解决不了我开车的问题.
- 具备业务概念或规定的元素. 例如地铁线路图外面有站点, 有线路等.
- 蕴含元素之间的关系. 例如一条线路下面有多少个站点, 站点的绝对间隔是如何的.
畛域分析模型长什么样?
畛域驱动设计蕴含了畛域分析模型和畛域设计模型, 咱们先看看什么是畛域分析模型.
下面就是一个简略的订单相干的畛域分析模型, 他具备一些业务概念, 例如订单, 订单项等, 也蕴含了他们之间的关系, 这个关系能够是依赖, 关联, 聚合, 组合, 继承, 实现. 最重要的是他能满足我增加缩小订单项, 并批改订单项下面的商品和商品数量的需要.
畛域分析模型是个简略的类图吗? 例如 Mybatis 外面的 Mapper 对象能不能在畛域模型下面进行表白?
答案是 否定 的, 因为畛域模型关注的是业务畛域的概念, 而非软件技术相干的概念. 关注的是业务而非具体的代码实现.
大家是不是发现畛域模型和咱们常说的架构有点相似. 架构其实也是一种模型, 但畛域模型关注的是如何解决业务问题, 也就是性能需要.
而架构则是为了解决非功能性需要, 例如分层架构是为了解决可读性问题, 可维护性的问题. 微服务架构是为了解决划分弹性边界, 让资源失去最无效的利用. 微内核架构是为了解决可拓展的问题.
畛域模型和代码实现区别是怎么子的?
那咱们试着用个例子来阐明用畛域模型来设计的益处.
首先需要如下
用户登录胜利会依据不同角色加积分
角色分为一般和 VIP
一般加 1 分, VIP 加 2 分
用 事务脚本 实现一下
这个需要并不简单, 咱们能够用一个办法就解决了
public void login(String username, String password) throws Exception {User user = userMapper.selectByUsername(username);
if (Objects.equals(user.getPassword(), password)) {switch (user.getRole()) {
case GENERAL:
user.setPoint(user.getPoint() + 1);
break;
case VIP:
user.setPoint(user.getPoint() + 2);
break;
default:
}
} else {throw new Exception("明码不正确");
}
}
但如果需要有变怎么办?
password 须要加盐存储
减少 VVIP 角色, 积分加 3
咱们可能须要批改 password 判等的办法, 须要再 switch 外面减少一个 VVIP 角色. 咱们曾经隐隐闻到代码的坏滋味了. 尽管新需要很容易实现, 但咱们须要批改原有的代码, 改变的范畴波及到整个办法. 通过相似打补丁的形式去批改代码一时爽, 始终打补丁一爽快. 但当办法体变成上百行代码时大家都不违心去了解原有的代码逻辑, 只能始终打补丁.
这种就是 事务脚本 的实现形式.
笔者的某个共事已经保护一个迭代了 3 年的零碎, 代码曾经不堪入目, 有新的需要要批改, 大家都不谋而合的在原有代码下面进行修修补补, 不违心去重构, 惟恐重构的改变范畴太大, 危险太高.
用 畛域模型 实现一下
咱们试着用 畛域模型 的形式实现一下这个需要.
咱们能够先把畛域模型剖析进去
用户有分数, 明码, 和角色, 不同的角色会有不同的积分策略, 因而我思考对积分策略进行形象来晋升可拓展性.
具体的代码实现如下
public class User {
public Password password;
public Role role;
public Point points;
public void addPoints(Point points) {this.points.add(points);
}
public void login(String password) throws Exception {if (this.password.isCorrect(password)) {PointsStrategy pointsStrategy = role.getPointsStrategy();
addPoints(pointsStrategy.loginSuccessPoint());
}else{throw new Exception("明码不正确");
}
}
}
应用了 充血模型 把登录的逻辑封装在了 User 对象外面, 登录作为 User 的一个行为裸露给内部应用.
login 外面的明码判等的实现细节被封装在 Password 外面
应用了接口去获取登录胜利的积分.
咱们从可读性和可拓展性和可测试性角度去剖析一下这段代码.
可读性: 这段代码是有层次感的, 逻辑清晰, 易于了解. 这因为对代码进行了正当的形象和封装. 例如登录其实会形象成一个登录的办法, 对于调用方来说封装了登录逻辑. 明码判断也是形象成一个判等办法, 把具体判等的实现细节封装在了 Password 外面.
可拓展性: 应用了接口的形式便于拓展新的积分策略, 合乎开闭准则.
可测试性: 新增一个积分策略或者批改明码判断的逻辑并不会对登录的办法有什么改变. 咱们只须要独自对明码判等的逻辑进行测试, 只对新增的积分策略进行测试即可.
看上去畛域模型的实现形式碾压了事务脚本, 但咱们却不容易去应用畛域模型呢? 这是因为模型的建设是艰难的. 下面的例子是对拓展点提前进行了捕捉, 业务逻辑也绝对简略, 因而能够设计出比拟正当的模型, 但理论的场景可能很难分辨对拓展点进行把控. 而模型的品质对畛域模型的可维护性至关重要. 其实畛域模型没有对错之分, 只有好坏之分, 好的畛域模型能够很好的表白你的业务, 满足所有的需要(用例), 在这根底上还能具备肯定的拓展性, 能撑持业务后续的倒退.
其实 DDD 有很大部分就是解决建模的问题.
咱们先比照事务脚本和畛域模型的特点 回头再看看怎么建设好的畛域模型
事务脚本 | 畛域模型 | |
---|---|---|
需要剖析 | 只思考当初 | 面向未来 |
开发速度 | 快 | 慢 |
可读性 | 越简略越好 | 越简单越好 |
拓展性 | 差 | 优 |
事务脚本是采纳过程式代码, 基本上按流程写即可, 无需提前做过多的设计, 因而开发速度会很快. 当代码逻辑简略时, 能够高深莫测就看到业务的实现细节, 可读性也是比拟好的. 因为没有提前进行设计, 因而拓展性就会比拟差.
而畛域模型是基于业务后续的倒退, 须要进行适当的封装和形象的. 须要提前思考业务的拓展点是什么, 这个逻辑是哪局部的职责. 因而开发速度会比较慢, 当业务比较复杂时, 因为有了封装和形象, 实现细节会被暗藏到下一个档次, 让代码看上去依然十分清晰, 且易于拓展.
如何结构出好的畛域模型?
DDD 强调开发须要深刻理解问题域, 问题域并不是说你性能的实现细节, 而是你要解决的是谁的什么问题, 你才可能设计出一个好的模型. 那如何更好的理解咱们业务呢? 答案是 领域专家. 领域专家并不是一个 title, 而是对某个畛域的业务比拟理解的角色. 它能够是产品经理, 也能够是在这畛域开发了很久的开发人员都是能够的.
咱们常见的产品经理经常在 问题域 中进行摸索, 他们会去发现用户的痛点, 痒点或爽点, 而后想出某个性能去解决用户这个问题. 而给到咱们开发的通常就是一堆性能列表和交互稿.
而咱们开发则会去思考如何应用 UML 去剖析性能, 而后进行数据库设计, 最终通过代码的形式去实现. 这样存在的问题是咱们所思考的问题是割裂的, 一个是在思考如何解决用户问题, 另一个则在思考如何实现. 看上去分工明确, 各自有各自善于的畛域. 但却存在着十分重大的问题.
我举个最近工作中的例子
我正在开发的产品老师能够创立多个课程, 一个课程外面能够创立多个班级.
有一个数据统计分析的需要, 是心愿能够展现某个 ” 班级 ” 的一些流动的统计数据.
红色框框是 ” 班级 ” 筛选器, 事实上, 在原有的需要外面, 还蕴含一个 “ 全副 ”, 其实就是还须要依照 ” 课程 ” 的维度进行统计.
剖析: 减少 ” 全副 ” 站在开发的角度无疑是须要减少一个维度的数据统计, 工作量, 复杂度都会有所增加, 为什么会有 “ 全副 ” 这个需要呢? 我就去问产品
我 -> 产品: 这里加个 ” 全副 ” 的目标是什么?
产品 -> 我: 因为老师有评金课的需要, 而评金课须要到课程的维度.
我 -> 产品: 这个统计数据是为了让老师晓得某个班级的学习状况的, 便于对某个班级进行治理. 而金课是为了展现老师的教学质量的, 应该是更宏观一点的数据指标才对. 两者的目标不同, 数据指标也应该不一样.
产品 -> 我: 的确是这样子的, 只是目前还没思考分明金课的指标是什么, 因而临时加了一个 ” 全副 ”.
我 -> 产品: 减少一个 ” 全副 ” 会减少一个维度的统计, 减少开发工作量和复杂度, 如果还没想好, 那能够思考临时先不做了.
而 畛域模型 就是一个很好的沟通合作的工具和产出物. DDD 强调畛域模型不是靠开发一个人想进去的, 而是要和产品, 甚至测试, 交互等团队成员一起打磨, 最终达成共识的.
开发在沟通的过程中对业务有了更深的了解, 而产品也对实现的逻辑有了更深的了解. 大家能够别离从技术视角和业务视角去探讨, 从而对某个解决方案是否能真正解决问题, 实现的影响范畴, 拓展性等进行剖析, 失去性价比更高的解决方案.
咱们这么做并不是为了能够砍需要, 争取早点上班. 只是好刚放到刀刃上, 咱们开发的价值并不在于实现了多少个性能, 而在于能为用户解决多少个问题.
畛域模型只是用与剖析吗? 能够落地的吗?
thoughtworks 的徐昊说已经有人问他: 怎么做能力更 DDD 一点
他答复说: 模型和实现关联了, 就够了.
他的含意按我的了解是: 畛域模型须要和代码是同步的, 如果只是把畛域模型作为剖析的工具, 模型会缓缓和具体代码实现割裂, 后续保护畛域模型就没什么意义了.
在 DDD 外面, 模型不是两头产物, 而是外围产出物, 甚至比代码还要重要.
如果把畛域模型当成修建的设计图
那代码就是屋子自身
咱们从畛域模型外面就能够理解到代码的构造, 而代码外面也能推导出畛域模型来. 当畛域模型随着业务的倒退须要产生重构时, 也意味着代码也要进行重构了. (后续会有实现局部的文章来对畛域模型的落地进行介绍)
总结下 DDD 的老本和价值和实用的场景
沟通和了解
沟通老本高: 须要大量的沟通, 不仅业务流程要梳理分明, 而且包含模型的设计, 是每个业务概念都须要达成共识. (明面上的业务概念是比拟容易的, 难是难在隐含的业务概念, 须要独特开掘, 例如用户订阅了极客事件. “ 订阅 ” 看上去是个动词, 但实际上在模型外面是个名词, 代表用户和专栏之间的关系).
了解老本升高: 大量的沟通达成的共识让下次的沟通能够更顺畅, 开发对业务的了解也会加深, 大家缓缓造成了高效且精确的 对立语言.
实用场景: 业务简单, 开发很难本人能够分明的把模型精确示意进去.
重构和拓展
重构老本高: 比照事务脚本就能够发现, 事务脚本只须要加几行补丁就解决的问题, 畛域模型可能须要改变好几个类. (笔者正踩坑中, 当设计出一个蹩脚的模型, 重构的模型老本很高).
拓展老本升高: 正当的模型会预留一些拓展点, 便于后续进行拓展, 从而升高拓展老本.
实用场景: 迭代速度比拟快. 迭代速度快意味着开发的性能会越来越多, 开发的老本很大水平上会依赖代码的可维护性, 良好的模型能够实时表白以后的业务状况, 并预测将来的倒退, 晋升代码的可维护性.
难度与提高
学习老本高: 须要有面向对象的设计能力, 并且有比拟好的形象能力和结构化的能力能力设计出一个比拟好的模型. 另外 DDD 和咱们平时常见的分层架构, 设计形式不同, 目前没有太多规范的可参照的实现, 因而很考验大家对概念的了解. 谬误的了解可能会导致形形色色的实现形式, 而且无异于代码的可维护性.
团队能力晋升: 大家在和领域专家沟通的过程中加深了对业务的了解, 在和团队之间对于实现形式是否正当进行强烈碰撞的时候加深了对概念的了解. (这点笔者感触挺深的, 团队不再感觉写业务是件无聊的事了, 而是会有更多的思考)
实用场景: 团队气氛好. 大家都违心进行沟通和新模式的摸索, 违心接受建模谬误须要重构的危险.
畛域驱动设计好不好用, 难在哪里?
咱们团队当初曾经在使用 DDD 来进行开发了. 但 DDD 波及很多货色, 咱们也在一步一步在摸索. 下面提到老本和收益是咱们目前最实在的感触. 总体从各个后盾开发的反馈看来成果都是不错的, 大家都认可这种开发思维. 我感觉要施行目前有 3 个难点
- 须要和领域专家 (产品经理, 测试等) 沟通合作, 独特打磨模型. (未摸索)
- 如何让模型更好的演进, 保障重构过程中的品质. (筹备摸索)
- 如何让标准达成共识, 落地过程中可能有什么未知的问题. (摸索中)
咱们团队第三点是做得不错的, 能落地是一个很好的开始, 后续也会缓缓把短板补齐. 心愿能把教训分享给大家.
下一篇会介绍一下 DDD 外面的概念, 概念的了解对后续的落地有十分大的影响, 了解 why 是外围. 而后会开始介绍咱们团队的落地状况, 两头遇到了什么问题, 思考过程是如何的, 最终如何解决, 大家再一起来探讨一下.