领域驱动设计(DDD)在搜索团队中的工程化实践

DDD(领域驱动设计)简介:

    Eric Evans世界著名软件建模专家,2010年在其所著的《领域驱动设计》进行了详细的解释和介绍。参照1970年代的这一软件设计思想,形成了符合当前复杂业务应用场景的新“领域驱动设计”。

参考百度百科:https://baike.baidu.com/item/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1/3260671?fr=aladdin

    关于DDD的设计思路,原理,内容基本上分析的文章已经到处可见了,大家会谈到领域设计的几种模型,甚至会成为架构师岗位面试中设计思路面试题的一部分。

    但是!如何落地,有人落地了嘛?落地效果怎么样?到底能否实现?面对这些质疑,基本上可以认为每个人都可以自说自话。

   本文会介绍我们搜索团队内部对于DDD的一次工程化实践的过程,希望能够给大家一些参考。

面向本质:

     1.为何需要这样的模型?我们团队负责公司的全部搜索和列表展示场景的服务维护,日积月累,不同入口,场景,版本,业务模型导致冗余了非常多的业务流程代码。我们做了业务代码层的汇总分析,平均每个项目方法20-30行,每个方法中嵌套的if/else和get,set方法平均15行左右。我们发现随着业务的增加,代码的嵌套和耦合也指数倍增长。因此我们需要一套能够提高复用率的业务沉淀代码或架构。

    2.彻底的使用DDD还是局部使用?这个问题完全是玄学,就像业务系统技术栈选型一样,没有最适合,只有更适合。我们调研了美团在2017年的DDD工程化实践项目,得到了一个深刻的结论:那就是对于简单场景,正常spring基础下的三层架构已经满足就不需要使用额外的炫技策略,否则会导致代码跳来跳去反而增加了额外的复杂度;对于复杂场景,做好抽象、分治,选择和场景强关联的属性做轻度的DDD实现。

    3.同样是CRUD为什么他做的比我开心,比我好?我相信这个问题,正常的85%业务线上的开发同学都会遇到,也都会问自己,那么这个过程中会出现类似的两类人。一类,抱怨型:“天天CRUD,复制粘贴,我真的是醉了”;另一类,探索型:“他是怎么做的,为什么我的不如他,找找书籍,刷刷论坛寻找亮点,验证并实践”。我们团队的一个氛围是:除了少数人天赋异禀,大部分的人智商没有特别的区别,做得好无非就是方法论和学习的方式有区别而已。

面向实践:

   1.老员工其实是财富,一条生产线,哪里掉过坑,哪里最复杂大部分都在老员工的经验之中。这个老不是工作年限的长短,而是浸泡在一条业务线中的跟迭代开发的时间长短。经过长期和业务线中有丰富经验的开发沟通后,我们找到了搜索场景中最复杂,最重复,也是嵌套业务最深的几个模块。以我所在业务线场景我们汇总了:搜索的存储,数据状态的变更,多场景分页,渲染和组装层这四个核心突破点。

    1.1.数据的存储复杂点在于搜索引擎索引的构建,存储数据的结构化抽象,数据源头的多种类存储。

    1.2.数据状态的变更复杂点在于多场景对于数据修改频次高,一致性高的数据会出现索引大量的重建,导致实时查询效果差。

    1.3.多场景分页的复杂点在于推荐,主要展示,补充列表展示的多路查询数据重复展示,乱分页,无法分页。

    1.4.组装和渲染数据展示层的复杂点在于多个场景的适配关系管理较难,层层挖掘之后依旧难以确定最终表现层的模样。

    2.如何避免这种场景?这种场景会出现什么样的问题?从开发方面来说,代码可读性变差,可维护性变低,终于梳理清楚之后开发一般会采用加塞或者拼接if/else的方式处理业务逻辑。从测试方面来说,测试场景存在N个阶段一场景,M个阶段二场景,Z个阶段三场景出现测试用例数量T = N * M * Z 的情况,历史代码的回归变成快速迭代和敏捷开发的最大耗时点。如果说彻底解决,任何团队基本都是无解的状态,因此我们尝试将这个模型变更为 T = N + M + Z的效果。

   3.业务分层方面,我们识别了1.远程调用场景RPC,HTTP,MQ等场景类的数据调用;2.代码中内存值交换的数据调用。从经典的spring架构来看,远程或外部调用需要依赖于中间层架构的IOC,AOP来实现;而内存值交换则可以参考JDK中源码的设计思想,暴露最简单的使用接口,隐藏复杂的交换值和计算、校验逻辑。

  4.效果,代码的可读性变高,逻辑性增强,变更点减少。我们粗略的统计了后续业务开发的代码量基本上减少了30%左右。执行效果和逻辑也更加清晰。

场景介绍:

   目前我们公司app端有多种多样的商品,商品本身具备很多特殊的属性,比如上架,下架状态。例如拼多多的模式中还有多人拼单,抢单这些因产品属性衍生的特性。

    搜索过程中,商品或订单数据的存储,用户app端的列表陈列样式和展示则成为了主流互联网运营中的核心点。我们把这两个点归纳为:1.海量数据的存储;2.单一到多内容的展示表现形式。诚然,复杂的排序以及千人千面这种推荐算法模型并没有纳入到本次的实践和讨论中,我们也会在后续的业务实践探索中摸索更贴合的方式来尝试。

    1.海量数据的存储--本文探讨的也仅限于数据的流转过程中组装符合数据这一个环节,对于强一致性,多点存储,异常和事务的细节问题不进行详细探讨。

数据来源介绍

参考上图,目前这个系统已知的数据来源分为了三个主要的外部渠道,三个主要的根据业务模型适配的生成机制。

    a. 通过http调用获得的数据; 

    b.通过组织内部tcp服务调用获得的数据;

    c.通过组织内部消息队列获得的数据;

    d.系统自派生数据,例如创建时间,更新时间,消费时间等数据与系统产生运转关系过程中可以被存储的数据;

    e.系统计算数据,例如资金类统一单位分,统一数据类型,长度统一米单位等数据;

    f.系统由于统一数据流入的顺序和条件产生的不同状态参数,例如订单分为了下单,支付,成单,履约单,订单完成和基于过程出现的正向、逆向流转过程数据。

    那么,如果只针对一条数据,一个单一业务线,我们会发现从头到尾实现是一种简单而轻松的方式。但是在配套了复杂的业务场景之后,数据的流转变成了极其复杂和庞大的工程。但是存储数据的关键制约因素依旧是如何查询(OLTP)或分析(OLAP)。这里涉及到了深入业务场景的数据存储方式选型等拓展类的知识,我推荐读者可以阅读《数据密集型应用系统设计》一书。

    回归数据的存储流转过程,清晰的可以归纳为两大特点:内部自生成,外部依赖

    内部自生成特点与业务模型完全关联,但是完全可以近似看作是一种赋值和取值抑或改变值的操作。因此可以称为对象值交换,下文简称值交换。

    外部依赖特点需要预先定义常规的接口或者交互字段文档以做统一的序列化和反序列化基础,这个必不可少也是目前软件开发阶段重要的设计环节。但是在不同系统数据的流入流出环节,依旧存在值交换的过程,这是一定的,与内部自生成数据的区别在于约定和串联的关系。

    我们的常规做法:数据copy,对象的创建,赋值,序列化等操作,可以解释为构造方法创建对象或执行对象的get/set方法。往往真实的业务场景中,参数的合法性校验,有效性校验也捆绑其中。我相信读者如果浸淫crud多年,对于参数校验的流程和环节的痛苦感慨颇深。

拆解与抽象

    流程化的数据流,一定是可以抽象的,一定是可以拆解的。这是阅读大量JDK源码之后得到的实践经验。但往往过度的依赖spring全家桶的IOC思想,AOP思想,在实际操作过程中开发依旧喜欢大量的依赖注入+if/else+get/set方法使用。本文并不是说此类方法或方式有误,只是针对极其复杂的场景,过多的上述方式造成了代码的可维护性差,可拓展性降低,甚至基本没有可读性。

    JDK是怎么做的呢?我认为这就是领域设计的精髓。参考HashMap的源码:


局部HashMap变量和方法截图

    常规通用HashMap,我们高频使用的如构造方法,put,get,size等方法,那么他的参数合法性,存值,计算,取值,扩容等等其他方法是否需要额外的多次使用和实现呢?答案当然是否定的!我想说这个其实就是领域的抽象。隐藏掉复杂的处理流程逻辑,只提供简单可用的api,接口抽象如此,抽象类如此,轻度的DDD领域模型也应当如此。

    改变个思路:Java官方提供了JDK(java标准版开发包),上层 应用语言使用的各家公司是否也可以培育和产出符合自身业务特点的CDK(自己公司标准开发包)呢?

    按照这个思路,我们尝试性的抽象了小原子粒度的特征模块。这一层不依赖于任何业务层的服务,大量的纯对象类和对应对象的BO业务逻辑封装:

domain层--CDK(目标公司标准开发包)

业务执行步骤举例:

    1.商品新增;                           ------RPC/MQ消费对象BO执行

    2.商品自身属性值对象赋值;  ------BaseItemBO对象执行

    3.商品计算属性值对象赋值;  ------CulculateBO对象执行

    4.商品状态属性值对象赋值;  ------StateItemBO对象执行

    5.商品存储到数据库中;         ------StoreItemBO对象执行

    对于任意BO对象,它完成了对应值的校验,处理,赋值,取值等操作,我们会发现这些通用的处理单元是每一套业务都需要使用的,大部分可以通用,少部分独立于自身的业务。

    使用了这样的模式:业务逻辑关系,整洁而清晰,映射了真实业务场景的核心步骤,把具体的执行环节交给面向对象中的对象来完成,通用的做抽象和沉淀,不通用的运用封装,继承,多态独立实现。

2.单一到多内容的展示表现形式

    这个业务的场景,我们依旧沿着DDD领域设计模式的思想,做了最小化单元抽象。


列表展示中的数据单元图

    基础数据,既能使用到详情也可以少量陈列在列表展示中;比如商品图片,大小,尺寸等;

    特征数据,商品的属类,商品的相似品类等。

    渲染数据,展示文案字体大小,颜色,布局方式等。

    多个单商品汇总成为了一个列表集合,注意列表集合的排序结果不在本文进行讨论,理论上是可以依托于算法推荐,自然排名等属性自由组合。

关键抽象点举例:

     介绍一下列表特征中,分页BO的具体实现。分页是一个伪命题,也基于空间换时间的概念。多次少量的操作模式减少底层存储引擎和网络交互的资源消耗。


分页BO

    我们提供了两个核心的方法,generateResponsePageBO生成响应体分页内容方法,resoluteRequestPageBO解析请求体分页内容方法。其余的方法和属性,例如参数校验,是否可以查询下一页,分页大小的适配等,都依据公司的业务特性做了具体的隐方式包装。可以想像这个分页领域的模型,针对于自己公司的业务场景,持续不断的迭代是能够满足绝大多数的使用和适配的。

关于DDD领域模式设计的一些探讨

    我们抛开程序本身的特殊属性不谈,仅仅针对于这一次的实现过程,我们深刻的发现,最小化单元是最难的一步。经验在这里起到了绝对的作用,所谓技术沉淀应当也是如此。大家都在探讨重复踩坑的场景,但是依然绝大多数公司的开发方式和架构导致,新人难以避免的再次走已经有人走过的弯路。我相信软件开发没有捷径,只有沉淀能够帮助后续继承者少走弯路,该走的路,请放心,还点走。

    当这个想法已经形成的时候,我听到过反对和质疑的声音,诚然部分开发依然喜欢一条道走到黑,踩到坑里再爬出来继续踩新坑。理论的模型落地必然是无限的争吵中,一步一步的缓慢发展。缺点也是有的,无论市面上对于DDD领域模型探讨的如何,它仅仅是一种设计的思路,而非解决问题的手段。

    不成熟的建议:

    非复杂场景,对于Java语言的开发,我依然支持spring模式的三层架构理论。简单清晰解决就好,这一思想也符合快速迭代。

    复杂场景,可以尝试性的结合DDD领域设计模式进行抽象,最终的目标是彻底的实现分治。抽象和分治其实才是我们本次实践中最深刻的体会。

    鉴于尝试的效果还不错将这段历程记录下来,更多的同路人一起合作商讨,伴随进步。

你可能感兴趣的:(领域驱动设计(DDD)在搜索团队中的工程化实践)