(6) 基于领域分析设计的架构规范 - 关于重构与落地
本系列目录:
- 改变与优势
- 领域分析基础
- 读写隔离
- 充血模型之实体
- 充血模型之Service
- 关于重构与落地
不论大家是否认可我在这里提到的这一套小规范,既然你能看到这里,还是由衷表示感谢。核心内容已经介绍得差不多了,接下来还是看两个很实际得话题。
DDD落地之殇
DDD这类架构真正落地困难,我觉得,有几个原因:
- 规范多,尤其是充血模型,需要对业务有更清晰的认识
- 代码结构相比无状态扁平化开发,层次深,模块划分更严格,所以,至少从文件数上来说,是更多的(代码行数倒是不一定多)
- DDD并不是一个“刚性”需求,如果不按这个规范来做,系统一样可以跑起来,所以,很多规范处在一个“模糊”的界定上,让开发人员无法确定到底该如何规划代码。
正因为如此,即使团队技术Leader了解DDD,却在项目一开始的时候,由于考虑到DDD的规范过多,不便于协作,为了快速开发迭代,往往继续沿用最通用的写法。而一旦到了中期,业务复杂度上来了,当觉得领域分析有用武之地的时候,低头一看,系统的复杂度已经到了难以轻松驾驭的程度,更别说来一个180度的大转变了。
真的,这很现实,就是一个恶性循环。
所以,我们需要找到一个破冰的办法:
- Leader足够强,团队成员实力不错,也给予Leader充分信任,那么在一开始就采用DDD
- 在一开始依旧采用非DDD架构模式,但利用迭代的方式,将代码逐渐变成DDD
这两者没有绝对好坏,虽说第一种不常见,但依旧是我挺期待和欣赏的做法。
而可能更实用的是第二种,但新的问题又来了,如何逐步迭代改进成DDD,从哪里入手?
从现有代码迭代改进
我们不强求一步到位,但求一步步做出一些恰当地整理
以下给出一个循序渐进的参考方案:
1. 读写隔离
这个过程几乎不与业务挂钩,我们将OrderService
,ProductService
中的查询方法剥离出来,放在诸如OrderFinder
,ProductFinder
之中,具体编码细则规范参见之前的章节。 整个过程,一般来说,只要编译能通过,代码就不会有什么问题。
2. 提取Factory
在查询方法被剥离之后,就只剩下增/改/删了,其中增又是最特别的,前面在讲工厂的时候提到过,所以将一个实体的创建行为提取到一个XXXFactory
中,也不困难,稍微留意一下,也不容易引发业务问题。特别是在一个已经相对复杂的系统中,如果没法一下子分析透彻聚合根的归属问题,那么可以每一个实体都加一个Factory
,之后再逐步合并,虽说工作量稍大一点,但对于已经有点棘手的项目来说,也算是一个快刀斩乱麻的办法。
3. 提取{Action}Service
也就是提取我们前文所提到的,跨多个聚合的一个事务操作 到一个独立的Service
中。 或者还可以将要求降低一点,只要觉得代码量特别大(我个人觉得150行以上就很大了,何况有些还带了私有方法)的一个方法,都迁移到一个独立的Service
中,并采用前文提到的{具体操作}+Service的命名方式,一个Service
只负责一个复杂的方法。虽说不会非常精确,但重构后的效果还是会很明显的。
4. 建立领域聚合体系
经果上面几步操作,我相信你的代码里,曾经那些OrderService
,UserService
,已经只剩下相对简单的一些改/删操作了,其实,某种程度上,他们已经逐步出现了领域聚合对象的样子了。所以,如果你做到这一步,暂时停止了,也已经值得庆祝了!
那么如果还要继续前进一步呢?小项目还好,对于大项目,会有些难了。我相信这时候Order
,User
很有可能是被POJO/DTO存在的,为了保证平滑过度,我们肯定只能新建领域模型,那么可能会要采用诸如OrderDomain
来表示了。然后再将之前OrderService
里的方法迁移过来。
但如果你用的是Spring或者类似Spring的IOC框架,会有一个很现实的问题。由于实体是被ORM框架new出来的对象,是脱离IOC容器管控的,所以如果你要在一个实体中使用一些IOC容器里的组件,可能得要自己去通过IOC容器的上下文来手动获取,比如通过ApplicationContext
来获取,而不能通过@Autowired
来自动注入了。
目前我在工作中,借用版本迭代的机会,逐步执行第一步和第二步,确实也是由于项目太大太杂,即使这两步走起来工作量也不小,还要和同事协调好这种新做法沟通协作问题,确实这也是挺现实的一个问题~
关于版本迭代
唯一不变的就是变化。
我们经常得要“笑着”对着甲方或老板说:“我们会拥抱变换”,呵呵~ 很苦,但得要面对啊。
所以,假如我们有一个领域实体为订单明细,大家可以很容猜到,它也是属于订单聚合中的一部分,而非聚合根。订单明细的变动,都来源于订单的操作,比如订单编辑,order.modify()
。
某一日,产品需求发生变化,说:
- 我们的订单明细要单独展示一个管理页面,而不只是像以前那样,只能从某一个订单点击查看“详情”才能看到
- 同时,我们要能对订单明细进行修改,并且将这些明细的订单绑定关系进行修改
这时,你会很明确的发现,曾经的订单明细已经开始“昂首抬头做主人”了,不再“寄人篱下”了,因为它的查看入口不再附属再其他实体上,而且我们还得精确找到某一个明细进行修改。 独立,修改。
产品既然这样设计,意味着订单明细在整个业务系统中的身份已经发生变化,它的独立性也将越来越强,未来很多功能很有可能将围绕它而展开。所以,订单明细将成为一个独立的聚合,进而我们会做出一些变化:
- 订单明细将有自己的
OrderDetailFactory
- 订单明细将逐步建立自己的方法,出现如
orderDetail.modify()
- 曾经订单聚合中的一些操作,要迁移到新的
Service
中,因为他们是不同的聚合了,有些操作已经属于跨聚合操作
其中第(3)步,是难度最大的,因为要改动以前的代码,甚至要动到业务,不仅有开发难度,还有测试难度。
但是很遗憾的是,任何重构都是这样的。不论用什么框架,到一定阶段,我们都会面对这个问题。不在开发期间进行优化,那技术债务,只会变成成千上万的BUG来给我们上课。有的时候我们总吐槽自己又拿到的一个前人的项目,说代码多么烂。但是反思下来,现在自己手头的代码,重构过吗?是不是也会成为未来人的笑柄呢?
其实这个未来人可能很近,可能就是一个月后的你自己。(神情复杂的笑容)
关于下一阶段的内容
目前我更多的以单计应用的架构为例,还未提及太多分布式相关的内容。
分布式是一个双刃剑,与这个架构的协调,我还在梳理中,等好了,我会第一时间发出来。
其实我不在乎阅读量有多大~ 我把这个当作一个我自己的工作记录,就好了~ 谢谢大家~
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
(5) 基于领域分析设计的架构规范 - 充血模型之Service
本系列目录: 改变与优势 领域分析基础 读写隔离 充血模型之实体 充血模型之Service 关于重构与落地 Entity与Service,相爱相杀 好,接上一篇。 既然采用order.cancel()这种模式,那么一个新的问题来了: 所有的命令操作都要变成这样子吗?那曾经巨大的OrderService的代码,岂不是只是单纯挪了一个位置,放在Order里面了,除了上面所谓的可读性的优势,那还有什么用? 并不是,只是一部分放在实体类,其余的命令操作,依旧会采用一种Service来做。 所以,我们必然需要一个可以清晰量化的规范,来确定这些行为该放在哪里: 如果一个命令操作,只修改了一个聚合对象内部的相关数据,那么,就归属给这个聚合 比如,订单取消这个行为,需要做的事情有: 订单状态标记为取消 订单变更记录插入一条,“订单取消” 根据我们之前的图可以知道,这些修改操作,都在这个订单聚合内,很自然的归属给order 注意,我们反复强调了这里是“修改操作”,也就是说,如果需要我们在此操作期间,查询其他聚合的信息,只要不做修改,那就是允许的!就像下面这样: @Entity public class ...
- 下一篇
BeeGFS源码分析2-客户端概要分析
BeeGFS的客户端是由一个内核模块和两个系统服务组成的,这里我们主要分析内核模块。内核模块主要实现了一个Linux的文件系统,因此注册了一个文件系统类型。因为BeeGFS的目录树解析,是在父目录DEntry里找子目录或文件DEntry,逐级迭代完成的,因此在Mount文件系统时,需要从管理节点获取根元数据节点的ID,然后再向根元数据节点查询根目录的DEntry的信息,为后续的目录解析打下基础。 注册文件系统类型 init_fhgfs_client 内核模块初始化: // fhgfs_client_module\source\program\Main.c #define BEEGFS_LICENSE "GPL v2" static int __init init_fhgfs_client(void) { #define fail_to(target, msg) \ do { \ printk_fhgfs(KERN_WARNING, msg "\n"); \ goto target; \ } while (0) if (!beegfs_fault_inject_init() ) fail...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- Linux系统CentOS6、CentOS7手动修改IP地址
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2全家桶,快速入门学习开发网站教程