前言正如领域驱动设计之父EricEvans所著书名,领域驱动设计(DomainDrivenDesign)是一种软件核心复杂性应对方式.当我们解决真正的业务问题时,会面临非常复杂的业务逻辑。即便是同一个东西,多个子业务单元所代表的含义也不完全相同。例如,商品详情页上下文中的“商品”一词是指“商品的基本信息”;在单个页面的上下文中,它指的是“购买物品”;而在物流页面的上下文中,则变成了“正在运输的货物”。DDD的核心思想是让正确的领域模型发挥作用。所谓“技术行业有专攻”,DDD引导软件开发人员将不同的子业务单元划分到不同的子领域,并在每个子领域内对事物进行建模,以应对业务的复杂性。一、重构折扣中心的背景我们在实际开发过程中遇到过这种情况。最初,由于业务逻辑比较简单,为了快速实现功能,综合考虑成本和风险等因素,我们会为业务统一创建一个大模型,每个模块使用相同的模型。但是随着业务的发展,各个子领域的逻辑越来越复杂,修改这个大模型就会成为灾难。有时改变A子字段的逻辑是显而易见的,但不知何故影响B或C。子字段的在线功能。优惠中心就是一个例子。折扣中心主要负责管理马蜂窝各业务线的促销活动,以及计算不同用户的折扣结果。“商品管理”和“优惠管理”作为两个不同的业务单元,最初设计时共享单一的产品模型,由产品模块管理。问题随着业务的发展,优惠形式不断推出,业务形式逐渐多样化,业务方的需求也越来越个性化。因此,折扣中心后期出现了一些具体的问题,无论是在功能上,还是在制度上。:1.在功能上,不够灵活。折扣信息作为商品信息的属性配置在商品管理模块中。例如,为了引导用户使用app,需要设置A类折扣,可以通过在商品信息编辑页面添加A类折扣配置项来实现;如果某商品的A类优惠需要在0:00生效,商科学生必须在电脑前设置等待0:00更新商品信息,才能推出促销活动。另外,如果要打造适用于所有产品的折扣,按照之前的模式,所有产品都必须设置一次,这几乎是无法接受的。2、从系统角度看,商品信息中存储的优惠信息不易扩展,优惠信息通过商品管理模块的接口输出。如果要增加新的折扣类型,需要在产品信息相关的表中添加字段,产品表会越来越大;如果要迭代打折的逻辑,可能会影响商品管理模块的功能。3、不利于迭代。由于折扣信息只是商品的一个属性,没有自己的生命周期,因此很难统计折扣集在某一时刻的投入产出比,从而指导后续的功能优化。重构折扣中心预期的系统层次。在系统层面,将折扣相关的业务逻辑分离出来,分别设计和实现;在应用层面,折扣中心将有自己独立的后台,负责管理折扣活动;还会有一个独立的折扣计算接口,负责C端用户使用折扣时的计算。2、为什么选择DDD避免模型贫血基于传统MVC架构开发功能时,Model层本质上是DAO层,业务逻辑通常封装在Service层,然后Controller通过调用Service完成对外功能层。在这种模式下,数据和行为分为两层:模型和服务。我们把这种只承载数据而没有业务行为的模型称为“贫血模型”。在与业务方理解需求的过程中,我们使用的对象是真实业务的映射,是行为和属性的综合。需求确定后,在开发过程中,我们人为的把行为和数据拆分成两部分,做了一个转换。随着需求的迭代和人员的变动,开发看到的代码和业务方的需求越来越不一致,导致很多代码没人知道业务逻辑对应的是什么。这种现象称为贫血模型。带来的“健忘症”最终导致了维护成本极高的大型泥潭系统。领域驱动设计的核心是基于业务逻辑建模,避免贫血模型,减少设计和开发过程中业务信息的丢失和转换。在业务逻辑迭代的过程中,系统可以通过调整相应的业务模型来完成迭代。3、落地过程中的关键点:业务逻辑抽象要基于业务逻辑建模,必须合理抽象。由于业务表象千差万别,产品经理和软件设计师需要与业务专家深入交流,从离散的信息中抽象出业务的内在逻辑。例如,旅游业务中销售的产品与标准产品不同,有些折扣没有考虑人群。例如,使用优惠券,可以享受所有类型的库存;但是像N人N折这样的优惠,成人价可以享受,儿童价和单间是不行的。基于这个特征,我们对折扣中心的产品模型进行了抽象,抽象出“是否可以参与计算件数”和“是否可以参与价格计算”两个通用属性。这样既实现了基于业务逻辑的建模,又不会陷入业务逻辑千变万化的表象。3.1战术设计第一步:统一语言,提炼关键词。准确的语言对于对齐产品、运营、开发等各方的需求非常重要。我们需要将优惠逻辑中的概念抽象成各方都能理解的文字,以达成共识。作为开发者,对领域的了解普遍比较少。要想抽象出一种合理的语言,让产品方和业务方都能理解,就需要充分了解业务背景和需求。在熟悉业务和需求的过程中,提取了一些关键词,这些关键词是最初的领域概念和通用语言。例如:优惠类型:表示优惠规则和对应的优惠方案。比如早鸟优惠就是早买多少(preferentialrules),减多少/折扣多少(preferentialplan);促销活动:有完整的生命周期,需要包括时间、平台、人员、产品等(限制维度)某优惠类型的使用过程信息;折扣发现:根据指定的商品、人物、平台,找出可用的折扣活动列表服务;折扣计算:根据指定的商品、人员、平台、购买数量,计算一次购买行为可享受的折扣金额及折扣明细;折扣顺序:各种折扣类型按顺序计算,如果有折扣,顺序不同,计算结果会不同;折扣是相互排斥的:有些折扣是相互排斥的。比如你用了金卡4折,就不能用马蜂窝优惠券了。第二步:抽象领域模型根据单一职责原则,一个领域概念对应一个领域对象。领域对象分为实体和值对象:实体:实体是有状态的和唯一标识的,包含属性和行为;值对象:值对象是无状态的、只读的,包含属性和行为。区分实体和值对象对系统设计具有重要意义。实体是我们需要关注和设计的,而值对象只是使用它的“值”。这可以简化系统的复杂性并专注于核心领域对象。不难理解,推广活动无疑是一个实体,推广类型是一个值对象。但也有一些业务行为不能归结到某个实体或值对象上。它们可以归类为领域服务:领域服务:领域服务本质上是没有状态的操作,通常用于协调多个实体。实体和值都属于领域对象,领域对象之间的交互逻辑不能放在领域对象内部,必须由服务来实现,从而有效地保护了领域模型。有一些领域逻辑,比如“offer排序”,“offer互斥”,涉及到多个offer类型,即多个领域对象。如果也设计成领域对象,就会打破单一职责原则,所以我们把这部分跨越多个领域对象的业务逻辑放到了“领域服务”层。第三步:抽象领域对象之间的关系。将相关领域对象显式分组,表达整体概念(或单个领域对象),即“聚合”。例如,优惠活动是优惠类型和优惠范围的集合;优惠类型是优惠规则和优惠方案的集合;优惠规则是限制维度的集合;优惠方案是优惠手段的聚合:聚合的主要作用是对领域对象进行分组,唯一的外部接入点是聚合根,避免了处理领域对象之间的一一对应关系,只需要处理与聚合和聚合之间的关系。第四步:走遍场景,调整领域模型领域模型的调整贯穿于整个设计开发过程。随着业务的调整,领域模型也需要调整。比如折扣中心后期推出了会员卡的折扣类型,所以需要将折扣类型的优惠券的显示调整为与会员卡互斥的和不与会员卡互斥的两种优惠券与会员卡互斥。第五步:简化设计,降低系统复杂度建模的本质是对真实事物的简化和抽象,引导我们忽略与问题域无关的事实,提取??与问题域密切相关的信息。以折扣中心为例。在最初的方案中,我们设计了折扣类型管理的功能,根据不同的折扣规则和折扣方案,自动组合不同类型的折扣类型。但可以预见的是,未来优惠的种类会有所限制,每种优惠类型都会有自己的特殊配置,比如N人优惠中的每N人/第N人;早鸟提前N天等。也就是说,基本没有根据折扣规则和折扣方案自动生成折扣类型的使用场景,所以去掉了这个设计。再比如,我们最初是在促销活动的维度上设计的折扣限制。经过权衡,为了降低系统的复杂度,我们最终在折扣类型层面实现了。以“抢蜜蜂”折扣类型为例。它的规则是所有的蜜蜂抓取活动只能被一个用户抓取一次。促销活动的维度不需要做这个限制,可以在优惠种类层面进行控制。3.2策略设计策略设计处理不同限界上下文之间的拆分和集成逻辑。限界上下文相对抽象。结合我们文章开头提到的“商品”在不同语境下的例子,如果不解释其所处的语境,就无法准确描述同一个词的含义。“语境”其实就是“语境”,对应不同的“子领”。同理,如果领域模型不是在有限的上下文中设计的,设计出来的领域模型是不明确的,会同时支持多种上下文。这里需要说明的是,如果你是从零开始搭建一个全新的电子商务系统,首先要做的就是战略设计。折扣中心是建立在现有大型电商系统的基础上,相当于重构其中一个子领域,所以我们会先做战术设计,然后在完整的情况下考虑与外部其他公司的集成电子商务系统。环境之间的关系,即战略设计。区分折扣中心的内部场景折扣中心包括两个不同的子业务单元:B端用户的促销管理和C端用户的折扣计算:促销处理是增删改查促销活动、配套统计等业务;推广活动在这里是一个实体,具有完整的生命周期,在线、离线等状态,可以创建和删除;折扣计算处理一个订单可以享受哪些折扣,扣多少钱的问题;在这个场景中,促销活动是一个值对象,它只提供计算折扣所必需的参数。折扣中心在整个电子商务系统的环境中与外部系统集成。作为一个子域,折扣中心处于其自己的有界上下文中。使用折扣中心服务的详情页面和订单页面都在自己的限界上下文中,所以在调用折扣中心时,需要设计它们之间的上下文映射方法。调用方和被调用方使用的策略设计方法通常有以下几种:client-supplier:适合同一个团队之间的协作,上游会有严格的自动化测试,确保给下游的数据必须符合约定;Follower:适用于不同团队的协作,上游不关心下游的标准,下游完全接受上游给的数据;防腐层:适用于上游不关心下游标准,但下游不甘心“互惠”,加一层做转化处理,保持下游系统独立性;开放主机服务:适用于中台(通用能力平台),对接方多,业务重复度高,已有完善的测试机制和通用模型。根据我们的实际情况,不同团队的开发者可能会调用折扣中心,折扣中心不希望被不同的上游侵入到内部设计中,所以“客户-供应商”和“依附”模型都不适合;另外折扣中心的抢先体验方会比较少,而且会不断迭代,所以不适合使用“开放式托管服务”。综合考虑,防腐层的设计更适合折扣中心。下图展示了折扣中心的业务架构。中间的应用服务层采用防腐层设计,体现折扣中心与外部系统集成时的上下文映射关系:3.3架构实现折扣中心选择经典的分层架构。从上到下分别是用户界面层、应用服务层、领域层和存储层。图中不同颜色块分别对应外部服务、应用服务、领域服务、聚合根、实体、值对象、仓库。用户界面层:处理和最终用户交互逻辑;应用服务层:负责将领域层返回的数据封装转换为用户界面层;领域层:折扣中心的核心逻辑在这一层,包括领域对象和领域服务。存储层:存储层负责将内存中的领域对象落地到存储介质中,同时负责从存储介质中获取原始数据后,为领域层构建领域对象;该层对域层隐藏了底层存储细节。虽然存储层在领域层之下,但是我们在实现过程中采用了依赖注入的方式,将存储层的具体实现注入领域层。四、存在的问题及近期计划1、价格层面的优惠目前,公司内部没有统一的商品中心,各条线对商品的定义差异较大。例如,自由行产品包括旅行日期、价格类别(成人价格、儿童价格)和套餐类别;而火车票产品则包括座位、等级、目的地、出发点等等级。如果折扣中心抽象出一个通用的商品等级来适应各个业务线,其实是折扣中心需要为商品定义一个标准,但这个标准很可能与后续商品的标准定义不一致中心。折扣中心即将进行重大改造。因此,最终的解决办法或许还得通过推动建立统一的商品中心来解决。2.性能问题领域驱动设计的缺点是类的增加。目前折扣中心的技术栈是基于PHP的。PHP是一种解释型语言。即使采用DDD模式下的OPCode等缓存技术,执行阶段的耗时相比其他静态数据类型语言来说还是比较大的。因此,未来计划使用Java技术栈对折扣中心进行重构,以优化性能。五、总结本文介绍了基于DDD重构的马蜂窝电子商务折扣中心的一些实践经验。DDD的思想也帮助我们在业务迭代的过程中更加合理的设计架构。当然,是否采用业务驱动设计的思路,还要看业务和团队的实际情况。随着马蜂窝业务的快速发展,我们会在架构设计上做更多的探索,继续与大家交流。本文作者:徐兴旺,马蜂窝电子商务研发平台服务团队技术专家。(马蜂窝科技原创内容,转载请注明出处并在文末保存二维码图片,谢谢合作。)
