cover_image

零售主商品MVC到DDD升级实践

宋志朋 SQB Blog
2024年03月26日 00:50

unsetunset一、引言unsetunset

在当今快速变化的零售行业,主商品作为核心板块之一,承载着日益增长的复杂性和市场需求的变化。这种复杂性与“熵增”定律类似:最初可能是一个简单的系统,但随着业务需求的不断迭代,系统的复杂度会越来越高。

如果早期架构不够合理,随着时间推移,系统的混乱程度会愈发严重,给后续开发和维护带来巨大困难。本文旨在探讨如何将零售主商品系统从 MVC 架构升级到 DDD 架构,解决传统架构在应对复杂业务时遇到的挑战,提高系统的灵活性,从而更好地满足业务发展。

unsetunset二、系统诊断unsetunset

探讨主商品架构升级之前,对当前系统进行全面的诊断是至关重要的一步。软件架构的设计与实现,应该遵循从高层的业务需求出发、自上而下的业务拆解、以及自下而上抽象聚合的原则,并选择合适的设计模式和软件设计原则来指导代码落地。诊断发现其主要问题集中在以下几个方面:

2.1、MVC 架构缺乏领域沉淀

当前系统采用 MVC 架构,贫血模型设计、面向过程式编程。如果从底层逻辑拆解,得到的公式便是 「读取数据」 --- 「计算、组装」 --- 「存储数据」。通过页面入口触发,经过各种业务逻辑处理,最后链接到底层的储存介质,使得页面 UI 与底层的数据结构深度绑定。

图片

当前台页面迭代升级时,简单增加存储字段看似能够满足需求,但这种做法也带来严重问题。首先,它阻碍了领域模型的有效沉淀,导致不同模型之间的关系弱化,过度依赖底层表结构来承载业务数据,代码缺乏对业务深层次理解的“灵魂”。一旦脱离事物的本我,频繁的业务规则很容易让架构偏离其应有的轨道。

另外,在与产品团队对接业务需求时,后端开发人员受技术思维影响,很容易聚焦在表结构字段,导致交流过于技术化。这种情况下,如果业务同学没有一定技术基础,双方的沟通就很容易产生偏差。

2.2、模块过度耦合

一个优秀的框架通常是把复杂留给自己,把简单留给客户,对外提供标准接口规范。但是,随着业务复杂化,一些研发同学图省事慢慢打破这个边界,既然内部可以穿插调用直接拿到数据,为什么还要走“标准” 接入流程。

各个业务组件的 controllerservicemanager  层开始代码交织, “破窗效应” 逐渐加剧,后续哪怕开发一个小需求,也会牵一发而动全身,非常痛苦。

2.3、表结构臃肿

无论是多么复杂的业务,最终都要持久化落盘存储。复杂的表结构不仅使得数据操作变得复杂,还会影响数据库的性能。优化表结构、减少冗余字段、设计更加合理的数据模型是提升数据库性能和简化数据操作的关键。

2.4、扩展性偏弱

一个优秀的技术方案不仅要实现功能,还要兼顾未来的系统扩展。举个场景,之前的「删除」操作是将主商品表的数据删除,然后插入到「删除日志表」,看似问题不大。

图片

当迭代进销存项目时,就尴尬了。一笔进销存订单包含多条商品明细,当订单确认操作后,如果商品被删除,商品数据会从 biz_spu 表中物理删除,并迁移到回收站 product_delete_log 表,此时的订单详情页就会出现部分商品查询不出来。

unsetunset三、DDD 主架构unsetunset

领域驱动设计(Domain-Driven Design)作为解决复杂业务系统设计的一种方法论,通过聚焦核心业务,实现业务与技术的高度协同。

根据各个子域的职责分为:核心域通用域支撑域。分析业务需求提取领域模型,并通过一系列的领域动作完成模型的数据扭转,整个业务驱动完全是围绕着【领域模型】在进行。通过领域模型与数据模型分离,将业务复杂度技术复杂度分离,区分彼此的职责,提高系统的可扩展性。

图片

3.1 业务分析

业务需求是我们工作的起点,只有深入的理解业务需求,才能抽象出相对合理的领域模型。
零售商品业务非常复杂,涉及:模版配置、商品创建、上下架、修改价格、筛选搜索、多包装、一品多码、排序置顶等功能,有单个操作,也有批量处理。我们从商家视角买家视角平台视角来分析商品的业务场景

商家视角

主要是围绕着商品生命周期的各种管理操作,以及数据统计、报表分析,通过强大的后台系统帮助商家更好运营自己的产品

图片

买家视角

电商的本质是买卖交易,买家做为交易的另一个重要角色,更多的读取商品各种配置参数,为用户下单做数据参考。

图片

平台视角

除了衔接好买、卖双方外,还要承担基础平台网关集成后台管理等能力,对接 商家APP、收银机、多功能业务团队等业务功能的接入。同时对产生的数据提供平台化的管理以及数据报表分析,做好对买卖双方的服务。

图片

3.2 领域建模

模型是对现实世界的映射,在现实世界中拥有什么特性,在软件世界中就拥有什么属性。建模过程本质就是一个整理信息、挖掘概念、发现事物内在联系的过程。常用的方法有用例分析法、四色建模法、事件风暴法 等,无论哪一种方法,其本质都是找名词动词,通过分析语句,找到里面的核心概念,并建立概念之间的联系,从而构建我们的领域模型。

图片

然后收集商品领域模型的属性字段,定义出 SPU 和 SKU 实体。实体具有唯一标识,是真实存在的。后期业务人员与研发同学沟通业务功能时,可以基于实体这个统一语言展开。实体本身是静止的,需要通过上层的业务操作驱动实体属性值变化。

图片

回归到工程结构上,我们需要将领域模型转换为软件实现。这里采用四层架构,分为接口层、应用层、领域层、基础设施层。将业务实体收拢在领域层的 model 模块下,统一管理,对外则通过 service 模块中的领域服务提供增、删、改、查等各种业务能力。保证领域层中的业务逻辑被合理封装,避免泄露到其他层。

不同的业务子域通过 package 形成限界上下文做隔离,如:进销存域、库存域、供应商域等,避免不同子域之间的耦合。底层的数据存储、缓存加速、以及外部服务对接全部集中在基础设施层,这个更多是技术视角来管控。Repository 模式对领域实体的持久化进行抽象,在不影响领域逻辑的前提下,可以灵活地更换底层数据库或引入缓存策略。

图片

unsetunset四、探究优化细节unsetunset

DDD 是工程设计的骨架,通过将系统分解为不同的限界上下文和领域模型,解决了复杂性管理和业务逻辑的封装问题。然而,优化工作不仅仅停留在宏观的架构设计上,它还深入到具体实现的细节。这些细节对系统的健壮性同样起着至关重要的作用。

4.1  领域模型与数据模型并非一一映射

领域模型是对具体业务的实体还原,要想持久化最终还是要落盘储存。这里就要涉及到数据模型的转换,常见的玩法一般是建表。

这里有一个例子,零售商品有多个价格字段,如:售价、进价、成本价、原价、会员价等,随着业务迭代,可能还会有更多的价格字段。

图片

之前采用建表方式 ,由于价格有多个类型,导致一个 SKU 对应多条价格记录,价格表随着商品数量增长快速膨胀。无论写、读,都要扫描多条行记录,资源开销很大。考虑到价格读多写少的特点,优化后的思路是采用泛化存储。废弃原来的价格表,将多个价格字段集中存储在一个 JSON 字段。由于支持反序列化,日后即使增加新的价格类型也能很好支持。

由于 JSON 字段在处理一些业务逻辑并不友好,所以我们把JSON的数据模型,转换成有业务语义的领域模型。这样,我们既能享受数据模型带来的扩展便捷,又不失领域模型对业务语义显性化带来的代码可读性。

图片

当然,并不是每一次都会对所有价格映射修改,比如:商品设置会员价,只修改 member_price 属性,这时就需要做 merge 操作。后台服务在接收到请求后,首先读取 sku 当前价格映射,然后局部修改会员价数据,最后更新到数据库中。为了避免并发修改带来的数据安全,这里引入了版本号做乐观锁。

4.2 数据结构的分分合合

数据的存储优化至关重要,尤其是当业务发展和数据量增长到一定阶段,原来的表结构可能不再适应新的需求。这就需要我们对表结构进行合理的分解和合并,以实现更高效的数据存取和管理。

图片
  • 数据聚合:对于一些频繁联合查询的表,考虑将它们合并为一个表,以减少查询时的 JOIN 操作,提高效率。比如:这里的【价格表】收缩为一个 JSON 字段放到 商品 SKU 表。【模板实体表】的自定义属性合并到 商品 SPU表 。
  • 数据冗余:适当的数据冗余可以减少业务的复杂度。例如:这里将商品的条码和扩展条码冗余了一份数据到【商品条码表】,并建立索引。当我们编辑商品时查询【商品条码表】校验唯一性决定是否执行后续操作。
  • 结构优化:将之前的一些过度设计回收,提高查询效率。比如:之前为了支持商户和门店两种角色设计的 【SPU 归属表】,实际业务中的价值不大,每次查询确要关联两张表,极为不便。优化后,我们将门店属性迁移回 【商品 SPU 表】
  • 垂直分割:将一个表中关联不大的字段拆分到不同的表中,减少锁竞争。比如:商品创建时可能依赖于标品逻辑,这里需要绑定标品id用于日后的数据分析。这里引入【商品扩展表】存储一些低频的数据,减少商品主表的行大小,提高查询效率。

优化不是拍脑袋,任何结构调整都应以业务需求为导向,这里不仅仅要考虑表、字段的新旧映射,还要考虑上线后对历史数据的迁移订正,确保数据的一致性和完整性。

4.3 非核心域剥离

根据 “分治” 思想来治理复杂系统,商品域拆分出 「进销存」「标品库」「库存」等多个子业务,但主商品做为核心域的关键部分,直接关联到用户体验,依然有复杂的业务逻辑。这里根据 “核心域” 和 “支撑域” 的架构定位,剥离出 “水位线” 和 “多包装” 这两个支撑域,大家各司其职,保持着弱依赖,让主商品的管理更加聚焦。

水位线:为了支撑收银机本地异构商品数据而设计的产物,门店会维护一个水位值,每当商品修改时,会关联门店的最高水位值。收银机则根据这个游标值,知道拉取哪些增量数据。

多包装:主要是为商家提供的小、中、大的商品库存转换工具,当小包装的商品库存不足时,可以根据这个关联关系来拆解大包装的库存实现库存调度。

图片

水位线 和 多包装整体还是商品域的范畴,但是他们拥有各自独立的领域模型、领域服务,内部只依赖主商品的 SPU 标识位,其余的都是各自相对独立的业务功能。页面展示上,如果需要商品标题等信息,只需要根据商品标识从主商品核心域的领域服务获取即可,整个架构是不是很清爽。

4.4 数据异构:多元化查询能力

目前的主商品数据采用 RDS 数据库存储,为了优化查询效率,我们对一些关键字段建立索引,然而,索引虽然能提高读写效率,却以占用额外存储空间为代价,按照阿里巴巴的开发规范,一张表的索引数量应控制在5个以内,这对于满足多变的业务查询构成了限制。

随着商家运营效率的提升,对于数据查询也提出了更高要求,如:商品类型、售卖状态、会员价状态、归属门店、进价状态,以及用户自定义的查询条件。为了解决这个挑战。我们引入了 Elasticsearch 中间件,通过数据异构架构来解决复杂搜索问题。

图片
  1. 保留RDS作为主数据存储:维持对事务性强、一致性要求高的数据操作依赖关系型数据库,保证数据的安全和完整性。
  2. 引入ES进行数据复制和扩展查询:通过订阅 MQ 消息,将 RDS 中的数据及时同步到ES。借助其强大的搜索能力支撑复杂的查询需求。
  3. 场景化查询优化:针对特定的业务场景,如商品推荐、价格分析等,我们可以在ES中建立专门的索引和查询策略,提升查询效率和精确度。
  4. 数据异构的监控和维护:实施数据异构架构,需要建立一套完善的监控机制,确保数据在RDS和ES之间的一致性,及时处理同步延迟和数据差异问题。

unsetunset结语unsetunset

在本文中,我们深入探讨了零售主商品系统从 MVC 架构向 DDD 架构升级的必要性,通过分析当前系统存在的问题,如领域沉淀不足、模块过度耦合、表结构臃肿以及扩展性偏弱。随后,介绍了领域驱动设计通过业务分析和领域建模,将复杂的业务需求转化为清晰、高内聚的模型和服务。

通过探究若干优化细节,从领域模型与数据模型的映射优化、数据结构的分解与合并,到非核心域的剥离,持续打磨优化系统,实现业务与技术的和谐共进,为业务发展带来持续的竞争优势。

unsetunset关于作者unsetunset

宋志朋,来自智慧经营开发部


继续滑动看下一个
SQB Blog
向上滑动看下一个