用DDD(领域驱动设计)重构单据审批系统(上)

用DDD重构单据审批系统(下)https://blog.csdn.net/wangleimj88/article/details/120929931?spm=1001.2014.3001.5502

起源

        最近接手了一个单据审批系统,该系统由多个微服务构成,其核心功能是单据审批与会计凭证生成。系统核心功能和大致流程如下图所示。

用DDD(领域驱动设计)重构单据审批系统(上)_第1张图片

        首先员工根据需要创建如采购单或借款单等单据,随后提交给由activiti实现的审批流程,在单据走完审批流程后会根据单据的类型决定是否进行支付。根据单据类型的不同,为单据生成凭证的时机有所区别,有的是审批完毕,有的是支付完毕。接下来就是为审批完毕或支付完毕的单据生成会计凭证。凭证的生成是根据单据类型等属性去DB中找出与其匹配的凭证生成规则,然后按照所匹配的规则生成凭证。最后通过WS接口将凭证发送给财务系统。从本质上看,这个系统终极目的就是生成支付凭证,其余一切功能都是为这个目的而服务的。

为什么重构

        接手系统后的第一个需求比较简单,是要根据推送财务系统的结果向系统中配置的相关人员发送邮件。我已经记不清用了多长时间找到的那个推送方法,并在那个调用推送接口后的一个合适的位置添加了一个发送邮件功能。仅仅这一个简单需求的开发和测试的过程就已经让我有了不爽的感觉,简单来说就是代码不易维护和业务扩展性很差。重构的想法也就是在这时萌生的。

        上面的需求上线后我大致对系统有了初步的了解,那么接下来就要上难度了。新类型的单据和新支付凭证生成规则的开发。在重构前,新建一个凭证生成规则的步骤如下。

        1. 继承一个抽象Builder,将生成凭证所需的数据在新Builder中组织好,这里的组织并不是单纯的拼装数据,重点是要按照一定的业务规则将单据中的数据进行处理后组织起来。

        2. 在系统的“凭证生成页面”中针对相应的单据类型添加凭证生成规则,这个规则是由产品提供的。规则中比较重要的是凭证摘要,其形式如“XX公司 日期 支付 XX公司 XX科目费用 XX元”,这个摘要还需在系统的字典页面配置一份。

        写到这已经实在忍不住到最后再吐槽这个设计的冲动了。既然设计成页面配置形式,那就让产品或业务人员来配置这个规则,但配置凭证摘要时又需要根据DTO中的属性来替换摘要中的占位符,比如上面那个摘要在页面中就得配置成这样--“{companyName}{bizDate}支付{supplierName}{subjectName}{amount}元”。产品和业务当然不会配置这种东西,还是得由开发人员来配置。既然不能由开发以外的人来配置,且不是配置之后便可立即改变系统行为,而仍须配合代码发布,从而才能让配置的新规则生效。因此,这个以页面形式来配置新的规则其实没有任何意义和优点。至此,重构想法已形成,至少要将这种无意义的页面配置形式改为代码形式。

        3. 将步骤1中组织好的数据发送给凭证微服务,凭证微服务负责将接收到的单据数据结合对应的凭证生成规则生成凭证后保存到数据库,接着推送给财务系统,如果凭证生成失败,比如没有对应的费用科目,就发送邮件通知有关人员进行处理。

        对于这个凭证微服务也是槽点颇多。首先凭证微服务跟负责组织单据数据的微服务,也就是那个Builder所在的微服务(我把它称之为门面微服务,因为它负责应答外部请求),它们用的是同一个DB实例。我相信当初那位把凭证单独拆分为一个独立服务的同行的想法是好的,但要拆就彻底一点,把DB也拆出来。这种只拆出一个服务的方式,除了增加一个故障点以外,完全没有达到拆分时所预期的内聚和维护效果,且随着系统不断的被一波又一波的开发人员修改,代码已经明显出现了坏味道,如临时性代码、满天飞的魔数、几百行的方法等等。其次,凭证生成的业务逻辑分散在两个微服务中,以前面提到的凭证规则需求为例,不管是修改还是新建凭证生成规则都需要修改这两个微服务,也就意味着要同时上线两个服务。

        当经过了上述那几个步骤的折磨后,一个可以成功生成凭证的规则终于上线了。就先不提测试的过程了,那更是横垄地拉车一步一坎儿。开发测试时间吗,回看了下周报,就这个不算复杂的需求用了将近6个工作日,其中调试和测单元测试占用了绝大部分时间。不过经这么一通折腾后套路已基本摸清,后续需求的上线时间也在不断缩短,但这种效率的提升是建立在开发人员对系统的熟悉程度之上,而不是系统本身的合理设计之上,这就像影视剧里的科学家,他可以在外人看来是混乱无序的实验室中迅速找到他想要的任何东西,而除了他之外的任何人都别想在这一片狼藉中理出头绪。因此当相似的需求交给其他人来做时,Ta就需要再趟一遍我之前路过的坑。本着人过留名雁过留声的原则,我决定重构,通过代码的形式将自己的想法作为非物质文化遗产留在这里。

为什么以DDD重构

用DDD(领域驱动设计)重构单据审批系统(上)_第2张图片 

        由于之前采用DDD的战略战术工具对几个项目进行了重构,在可扩展性和易维护性方面取得了很好的效果,但这只是以DDD重构的原因之一,最根本的原因就是上图这本DDD的开山之作的副标题所述,在维护这个系统的过程中逐渐意识到这个系统足够复杂,完全值得DDD出手拯救它。考虑到篇幅问题,于是决定再写一篇重构的详细过程。以下先简要介绍下重构的过程。(重构的详细过程在《用DDD(领域驱动设计)重构单据审批系统(下)》https://blog.csdn.net/wangleimj88/article/details/120929931?spm=1001.2014.3001.5502做了比较完整的介绍,并推荐了几本DDD方面的书籍。)

        1. 以“域”将系统重新划分为核心域和支撑域。核心域下只包括单据(Bill)和凭证(Voucher)两个限界上下文,它们是这个系统的核心业务概念。支撑域下则是核心域所依赖的各种功能,比如支付、预算、税务、员工管理、供应商管理、流程引擎等。

        2. 在核心域中建立单据和凭证限界上下文,其中凭证限界上下文是原凭证微服务。原有的service中的业务逻辑被整合到相应实体和值对象中,以充血模型代替事务脚本形式的代码,不仅让service变得清洁,也得到了充分封装的对象,进而为重用奠定了基础,通过聚合向外部提供调用,重构后的service只负责协调工作,而不是各种set get、判断业务逻辑、操作数据库、发送消息、调用接口等等。这样一顿操作下来,项目中轻松超过千行的service和超过百行的方法(这对于一个有追求的程序员来说是完全无法接受的)比比皆是。当然一个类的代码多并不是什么大问题,HashMap还两千多行呢。但请注意,类似HashMap的类是工具类,也就是说它不用跟随业务的变动而修改,它内部的改动源于对更好性能的追求,比如在链表与红黑树这两种数据结构之间的切换,因此这样的工具类也就十分稳定。但service不一样,它承载的是业务,而业务又是在不断变化的。在一个上千行的service中找到那个需要修改的方法,然后读懂、修改、测试。相信所有同行都有过类似的经历,这个过程的感受有多酸爽就不过多描述了。

        3. 提取支付完成、审批完成、凭证生成结果(成功 \ 失败)等领域事件,结合发布订阅模式从而提升业务可扩展性并降低代码耦合度。

        上述重构也是结合开发、测试、上线各环节所遇到的问题,在初步形成了重构的总体框架思路后,随着对业务领域的认识不断深入,并经过数轮迭代后得到了目前的成果。

        下图为重构前的工程结构,包的命名没有一个统一原则,有的是根据领域,有的是根据技术,如此的包结构无法很好的体现业务领域,会让刚接触这个系统的开发人员无法抓住系统的核心所在。

用DDD(领域驱动设计)重构单据审批系统(上)_第3张图片

         下面是重构后的包结构,目的是让代码自己说话,充分的体现业务领域。

用DDD(领域驱动设计)重构单据审批系统(上)_第4张图片用DDD(领域驱动设计)重构单据审批系统(上)_第5张图片用DDD(领域驱动设计)重构单据审批系统(上)_第6张图片

题外话

        在重构系统的过程中遇到的Java interface在使用上的问题想在最后聊一下,因为这种问题在开发过程中十分普遍。

        Java的interface的目的是什么,当然是多态,如果不能多态,它也就失去了存在的意义。之所以说这些,是因为在太多的业务系统中几乎所有的接口都只有一个实现类,比如一个service类必须对应一个同名的service接口,这是开发规范对于面向接口编程所做的规定,而面向接口编程的目的是隔离变化,运用到工程实践中时,就是当你想换一种功能执行的方式时可以做到神不知鬼不觉。service接口的价值也就体现于此。如果出于隔离变化的目的,试问:在一个系统的生命周期内,彻底替换一个业务功能的实现方式的可能性有多大呢?(现实中更多的是在原来的代码上进行修改)。如果这种可能性真的比较大的话,那么我们看到的大多数系统的中的service接口就应该有多个实现类,且这些实现类除其中一个之外,都被标记了@Deprecated,但事实并非如此。也就是说,service接口并未在隔离变化这个目的上体现其价值。

        其实没有发挥出接口隔离变化的作用也不是多大的问题(无非就是多了些没用的代码而已),当然也不是开发规范不应该如此规定一个接口的定义条件,其实最大的问题是对接口开发规范的刻板执行而形成的思维定势,导致了开发人员在需要接口的多态时反而不会写代码了。就像下面这样。

用DDD(领域驱动设计)重构单据审批系统(上)_第7张图片用DDD(领域驱动设计)重构单据审批系统(上)_第8张图片

 用DDD(领域驱动设计)重构单据审批系统(上)_第9张图片用DDD(领域驱动设计)重构单据审批系统(上)_第10张图片

        看出上面接口的问题了吗?这几个接口中的方法基本完全一样。至于为什么能写出如此魔幻的代码,个人觉得开发规范所培养出的思维定势绝对功不可没,当然还要加上后来者懒于思考的助攻(ctrl + CV),明明一个接口就可以搞定的事,硬是写出了十多个这样的接口。

        如果各位同行也觉得上面的代码难以接受的话,那么就试着去突破固有的思维习惯,给接口以多态。

你可能感兴趣的:(DDD,领域驱动设计,代码设计,经验分享,架构,重构,代码规范)