最近,身边好几位在创业公司的技术朋友都在抱怨,说他们公司的系统不稳定,代码年代久远,还找不到完整的设计文档,现在接手后连碰都不敢碰那些陈旧的代码。面对这些历史遗留问题,我给他们讲了一个身边真实的故事。
以前有家公司,业务发展特别快,现有的系统功能跟不上市场需求了。于是,他们收购了一家小公司,并让自己公司的技术团队去接手被收购公司的系统。结果呢,技术团队花了好大一番功夫梳理代码,发现那代码乱得不行,简直就是 “意大利面条” 式的代码,根本没法维护。没办法,他们只能下定决心重构这个系统。
半年后,业务又变了,这个系统要交给公司另一个部门的技术团队来维护。新接手的团队同样先梳理代码,梳理完后得出的结论是:这代码可读性极差,层级关系混乱,完全没办法继续开发新功能了,所以希望再对这个系统进行重构。这时候,公司老板找来了两个技术团队的负责人,问他们:“这个系统不是半年前刚重构过的吗?怎么换了个团队维护就要重构呢?你们这次重构了,就能保证下次别人接手不再吐槽、不再重构吗?”
听完这个故事后,朋友们沉默了一会儿,然后问我:“那我们该怎么办呢?现在每周因为这些遗留系统导致的问题,搞得我焦头烂额的。”
图1 混乱不堪的系统现状
不只是创业公司,这种情况在大公司也很常见,甚至可以说更加普遍。由于大公司的业务发展时间长、系统复杂度高,遗留下来的老旧系统数量更多,且这些系统的历史往往更为久远。
下图是某知名互联网公司对其线上事故原因进行统计分析后得出的结果。从图中可以看出,接近一半的线上事故问题,都可以追溯到业务代码逻辑的缺陷或不合理之处。这进一步凸显了代码质量和架构合理性在软件系统稳定运行中的重要性。
运维抱怨道:“这些历史遗留系统根本没法维护,动不动就宕机,三天两头出问题。”
开发人员也无奈地说:“老功能动都不敢动,怕出问题,新功能更是没法添加,每天都在忙着救火,搞稳定性治理,忙得焦头烂额。”
测试人员则表示:“一个接口或者消息,根本不知道有多少地方在使用,完全不清楚哪些部分会受影响,更别提会不会有漏测的地方,心里一点底都没有。”
我相信,大家在面对各种历史遗留问题时,如果详细罗列出来,恐怕都能写好几篇论文了。不过,这些所谓的“历史遗留问题”,从一开始就是问题吗?或者说,如果原开发团队还在维护这些系统,对他们来说这些问题还会存在吗?我觉得未必。那究竟是什么原因导致这些问题一步步演变成现在这种难以维护的局面呢?显然,压死骆驼的绝不是最后一根稻草。
图3 压死骆驼的稻草
因此,面对形形色色的历史遗留问题,我们首先要做的是透过现象看清本质。只有深入挖掘问题的根本原因,才能找到真正有效的解决方案,从而给出切实可行的具体措施来解决这些问题。
图4 U型思考模型
对于“历史遗留问题”,我们计划运用 U 型思考模型的四步法展开分析。我们期望从问题的根本本质出发,尽力摆脱仅针对表面现象进行应急处理的不良循环。
无论是历史遗留系统引发的订单丢失、账务对账错误等资损问题,还是内存泄漏、线程死锁等系统性能问题,以及老功能修改困难、新需求开发受阻等效率问题,这些可能都只是表面现象。为什么这么说呢?因为这些问题都是当前系统在特定时间、地点和条件下出现的具体表现,而非问题的根源所在。
举个例子,假设今天出现了一笔订单,由于支付超时失败,系统没能及时提醒用户重新支付,导致用户端一直显示“支付中”。如果我们仅把这些表面现象当作核心问题,可能会通过延长支付超时时间或增加失败重试机制来修改代码。然而,明天可能又会出现因支付失败重试机制导致的重复扣款问题。如此一来,我们就陷入了每天不断应对各种历史遗留问题的循环,疲于奔命。
或许有人会说,这都是当初架构设计不合理造成的,现在出现的各种问题都是这个原因。这种说法有其合理性,因为当下的许多问题确实可以追溯到架构设计的缺陷。但同时,这种说法也有局限性。我们不能简单地将所有问题都归咎于一个模糊的概念,因为这样做不利于我们深入挖掘问题的本质。这就像是小区里发生盗窃事件,我们不能仅仅归因于“人性本恶”,而忽视了小区安保松懈、外来人员随意进出等更直接的原因。
那么,面对历史遗留系统,我们真正需要解决的核心问题是什么呢?我认为,关键在于如何避免我们的系统在未来成为别人眼中的历史遗留问题。这个观点值得我们深入思考。
当我们确定要解决的核心问题后,接下来要深入洞察问题的本质原因。就像我在文章开头提到的例子,为什么我们接手别人开发的系统时,常常觉得代码混乱而不敢轻易修改?而当我们重构后的系统交给其他团队维护时,他们为何也会觉得代码难以理解?难道是我们比之前的人更厉害,而后面的人又比我们厉害?显然不是。这主要是因为每个人的编码习惯和设计风格不同。就像中国小学生和印度小学生做乘法的方式不同,我们可能完全看不懂印度小学生的计算过程,自然也不敢轻易修改。
在软件的开发和维护过程中,其生命周期往往从最初的理想状态逐渐向复杂、混乱和无序状态演变,最终因不可维护而被迫下线或需要重构。这种导致软件质量逐步恶化的因素,被称为软件的熵增现象。随着历史遗留系统的不断运行,代码功能被频繁修改和补充,时间一长,后续的开发者就越难理解最初的代码逻辑。
图6 生活中的熵增
熵的概念最早起源于物理学,用于度量一个热力学系统的无序程度。热力学第二定律,又称“熵增定律”,表明了在自然过程中,一个孤立的系统总是从最初的集中、有序的排列状态,趋向于分散、混乱和无序;当熵达到最大时,系统就会处于一种静寂状态。
因此,想让历史遗留系统保持稳定,既能顺畅维护,又能顺利支持新老功能的开发,关键在于以团队熟悉的风格来设计和开发系统,使代码与结构清晰直观,确保代码能如实体现架构设计,力求达到“代码即设计”的境界。总的来说,就是要保持架构模式的统一,确保代码清晰且如实映射架构设计。
在探索如何找到历史遗留系统问题的根本解决方案之前,我们不妨借助时光机,回到上世纪 60 - 70 年代,一同梳理计算机软件工程的发展脉络,答案其实就深藏于软件工程的演变历程中。当提及软件工程时,就不得不回顾软件历史上两次极具影响力的 “软件危机”。
软件危机是指在软件开发及维护过程中遭遇的一系列棘手问题,这些问题可能大幅缩短软件产品的使用寿命,甚至使其彻底报废。
第一次软件危机(20 世纪 60 - 70 年代) :当时软件开发主要依赖机器语言或汇编语言,针对特定机器进行设计与编写。软件规模较小,无需系统化开发方法,多为个人设计、编码与使用。程序依赖特定机器硬件特性,代码复杂难懂且不可移植,难以开发复杂功能软件。1968 年,北大西洋公约组织的计算机科学家在联邦德国召开国际会议,首度提出“软件工程”一词,标志着这门新兴工程学科的诞生,旨在研究和攻克软件危机。在此阶段,更高级的结构化编程语言如 C 语言(1972 年诞生)相继出现,结构化编程思想占据主导,提升了代码的可读性和可维护性。
第二次软件危机(20 世纪 80 - 90 年代) :此次危机源于软件复杂性的进一步提升。大规模软件动辄数百万行代码,涉及上百名程序员。如何高效、可靠地构建和维护此类规模软件成为新难题。例如,《人月神话》中提到的IBM 公司开发的OS/360 系统,虽投入巨大资源,仍延期交付且存在大量错误。当时人们期望软件代码具备可组合性、可扩展性和可维护性。尽管结构化编程改善了代码可读性,但从开发者视角出发,主要运用数据流图(DFD)进行系统分析,难以直观反映现实场景,导致开发人员与用户沟通困难,需求分析师应运而生。为应对此次危机,面向对象编程语言(如 C++、C#、Java 等)诞生,同时催生了设计模式、重构、测试、需求分析等更优的软件工程方法。
鉴于如今系统愈发复杂,需求不确定性增加,在当今 VUCA 时代(易变性、不确定性、复杂性、模糊性),回顾软件危机及软件工程发展历程显得尤为重要。面对复杂混乱的历史遗留系统问题,软件工程宛如一剂良方。维基百科定义软件工程涵盖 “软件开发技术” 和 “软件项目管理” 两方面。其中,“软件项目管理” 主要涉及项目管理知识体系,如 RUP 和敏捷 Scrum 等,暂非本次重点。“软件开发技术” 才是我们关注的核心,包括结构化编程、面向对象开发、MVC、领域驱动设计、整洁架构等开发方法。以下是按时间维度梳理的部分常见软件开发方法:
图7 常见软件开发方法
在软件开发中,有多种方法可供选择,团队可以根据项目和自身情况灵活运用。重要的是在团队内保持统一的架构模式,确保代码风格一致,避免因风格差异导致成员间难以理解彼此代码。
有些工程师只关注完成功能需求并交付,忽视软件的维护和扩展性,这是系统一两年后难以维护的常见原因。其实,软件开发不仅要写代码,更要重视需求分析,理解需求本质,与团队统一概念和语言,实现有效沟通。
需求分析是软件开发的关键阶段。开发人员应积极参与需求讨论,运用第一性原理思考,洞察需求本质。与团队对齐专有名词和关键问题,达成统一认知。这一阶段产出需求规约文档,对应面向对象分析(OOA)阶段,为后续设计和编码提供依据。
接下来是面向对象设计(OOD)阶段。根据需求规约文档,设计清晰合理的技术方案,确定系统的类结构、对象交互和模块划分,确保设计逻辑清晰,为编码提供明确指导。
最后是面向对象编码(OOP)阶段。需按照OOD阶段的设计方案编写代码,确保代码完整反映设计逻辑。注重代码的可读性和可维护性,遵循团队统一的编码规范。
通过遵循OOA、OOD和OOP的流程,团队可以更好地管理软件开发项目,提高代码质量,增强系统的可维护性和扩展性。
软件开发技术飞速进步,开发方法学也持续更新,比如水平分层从 MVC 三层架构到 DDD 四层架构、CQRS架构,垂直拆分从单体架构到 SOA、微服务架构(MSA)、服务网格(Service Mesh)等。万变不离其宗,这些方法学旨在应对系统复杂性,确保代码可开发、易维护,防止软件系统陷入 “危机”。
以下是某真实项目工程代码结构(公司类似工程众多)。正如那句调侃:“三个月前写这段代码时,只有上帝和我懂;如今,恐怕只有上帝还看得懂了。”
图8 某真实工程代码现状
软件开发大师 Bob 大叔(Robert C. Martin)在其著作《整洁架构之道》中,提出了一个衡量软件架构设计质量的标准:
一个系统架构的优劣可以通过满足用户需求的成本来判断。若在系统整个生命周期内,需求变更成本始终很低,那么这个设计就是好的。反之,如果每次发布都会增加后续变更的成本,那么这个设计就有问题。
关于整洁架构图的介绍如下:
图9 整洁架构图
1.Entities(实体) :包含核心的业务规则,拥有状态属性以及业务逻辑操作。
2.Application(应用层) :仅包含业务流程,代表各种用例场景,主要负责编排和调度 Entities 中的业务逻辑操作,具体的实现会在后续案例代码中展示。
3.接口依赖方向 :所有接口(Controllers/Gateways/Presenters)只能向圆圈内部依赖,不能反向依赖。例如,Controller 可以调用 Application,但 Application 不能调用 Controller 接口。对于数据库操作,以往常常直接将 Entity 领域对象传递到 Mapper 对象中,导致领域对象严重依赖数据库操作对象。在 DDD(领域驱动设计)中,应通过依赖反转来改变这种现象(整洁架构借鉴了 DDD 的很多概念,比如 Repository 等)。
4.Use Cases(用例) :是梳理和理解需求的重要工具,它代表业务场景,是架构设计中非常关键的概念,不懂得用例的架构师难以进行有效的架构设计。
现在,让我们重新聚焦于核心问题。我认为,大家都能完成功能需求开发,也都会使用各种开发工具,遵守编码规范。然而,为何仍有众多遗留系统问题困扰着我们?这些遗留系统,有时并非我们从其他团队接手的复杂代码,而是我们自己团队在半年或一年前编写的代码,如今却难以理解和维护。
因此,我希望每位程序员不仅要掌握“软件开发的工具和语言环境”,更要灵活运用“软件开发的方法”,实现知行合一。在此案例中,我们将积极践行《整洁代码》和《整洁架构设计之道》,致力于提升代码的可读性,降低服务组件的依赖耦合性,使变更的影响局限在最小的单元范围内。最后,通过一张图来简洁地总结我们解决历史遗留系统问题的思考路径和方法:
图10 系统化解决问题思考方法
在系统开发中践行整洁架构之道
在项目开发中,我们也希望工程代码结构清晰、整洁舒适。接下来,我将介绍整洁架构在会员系统中的实践经验,包括需求分析的用例梳理(OOA)、领域模型设计和程序设计(OOD),以及代码编写(OOP)等内容。
会员系统是企业或商家提升用户忠诚度和销售额的有效手段,其主要特点如下。
会员注册:用户可以通过会员系统注册为会员,会员系统将收集用户的个人信息,并为其生成唯一的会员账号(通常为手机号码)。
会员信息管理:对会员的详细信息进行管理,包括个人资料、会员状态和消费历史等。
奖励和折扣管理:基于会员的忠诚度、购买历史或其他标准为会员设计奖励和折扣。
积分和奖励:会员在满足一定条件的消费条件后可以获得积分或奖励,并可在未来的消费中使用这些积分或奖励。会员系统应在结账时显示可用的积分,并方便用户使用。
会员触达:通过电子邮件或短信等方式向会员发送促销活动通知。
了解项目背景后,通常下一步是编写需求文档,也就是PRD文档。但许多PRD文档是用自然语言编写的,这使得阅读和理解变得较为困难,还容易遗漏一些业务流程和场景。
那么,除了使用PRD这类自然语言编写的需求文档外,有没有更直观、易懂的方式来梳理业务需求呢?答案是肯定的,我们可以使用用例规约文档来实现这个目的。
第一步:确定干系人
根据PRD文档,以及和业务沟通交流,我们首先需要确定使用该系统的相关人员有哪些,以及他们对系统的目标和诉求是什么?以下为简化的“干系人-目标”图表:
表 1
干系人
目 标
描 述
企业经营者
(1)管理配置优惠折扣活动。
(2)查看促销活动的效果。
(3)制定会员等级规则。
(4)管理会员相关信息
企业经营者是会员系统项目的主要干系人,负责制定会员等级规则,设定奖励和折扣活动,管理会员信息和监督项目的实施
收银员
(1)查看会员信息和权益。
(2)帮助会员更好地注册。
(3)帮助会员享受权益或兑换奖励
收银员或服务员是企业与会员之间的桥梁。他们需要能够访问会员系统,以便为会员解决问题和提供支持
用户
(1)会员注册。
(2)为会员卡储值。
(3)享受会员权益,例如折扣等。
(4)参加促销活动
用户是最终使用会员系统的干系人。他们需要通过会员系统注册并领取奖励和折扣
技术开发者
(1)系统日志记录,便于排查问题。
(2)用户使用埋点分析
技术开发者可以提供对会员系统的技术支持。他们需要理解会员系统的工作原理,并能够帮助企业解决出现的问题
……
……
……
在梳理项目干系人及其目标时,我们要全面覆盖所有相关方,不能有所遗漏。例如,技术开发者的目标和需求对系统开发也至关重要,不能忽视。在这一阶段,项目干系人的目标可能还比较笼统,需要通过深入访谈来进一步细化和完善。
第二步:设计概要用例图
在完成“干系人 - 目标”梳理后,接下来可以从这些目标中提取用例名称。因为每个用例都是为了实现某个干系人的一个具体目标而存在的。同时,一个用例往往包含多个场景,比如系统在成功或失败等不同场景下的处理方式等。以下是一个简单的示例用例图:
图11 会员系统概要用例图
有了这样的概要用例,我们对系统需要具备什么样的功能和能力就有了一个比较明确的方向了,当然如果觉得这个用例太过于抽象了,那么我们可以再继续梳理下一层稍微详细点的用例,这里建议不要超过三层,因为超过三层的会显得过于细节化了,而我们在这里主要是为了梳理清楚实现用户目标系统应该具备的能力就足够了。对于每个用例更详细的将在接下来的“书写核心用例”中介绍。
第三步:书写核心用例
概要用例有助于我们从宏观上把握系统的功能,但对研发团队来说,利用它不足以进行研发工作。因此,在确定用例的优先级之后,我们应着手对核心且高价值的用例进行详细设计,并编写详细的用例。一个详细的用例通常包含以下几部分。
◎用例名称:简单明了地描述用例的主要功能。
◎参与者:与用例交互的用户或其他系统。
◎前置条件:在执行用例之前,系统必须处于什么状态。
◎后置条件:在执行用例之后,系统应该处于什么状态。
◎正常场景:描述用例在正常情况下的执行流程。
◎异常场景:描述用例在异常情况下的执行流程。
在编写详细用例时,我们必须特别注意覆盖所有正常场景和异常场景,以防遗漏场景。当然,我们不必为每个用例都编写详细的文档,只需对复杂的核心用例编写详细的文档,这样可以显著提升效率。例如,注册会员的详细用例如表2所示。
表 2
名 称
会员注册
描述
作为用户,我希望能够轻松注册为会员,以便享受积分奖励、折扣优惠等会员权益
参与者
用户:能够享受会员权益、折扣及参与促销活动,能够购买仅限会员购买的商品。
收银员:在用户注册遇到问题时,及时提供帮助
前置条件
企业经营者已经在系统中配置好了相应的会员折扣
后置条件
注册成功,赠送积分和折扣券
正常场景
(1)系统显示“注册”选项。
(2)用户选择“注册”选项。
(3)系统显示注册页面,请求用户输入必要的信息,例如姓名、电话号码、电子邮件等。
(4)用户输入所有必要的信息。
(5)系统验证用户信息的有效性。异常场景处理参考“异常场景一”。
系统验证用户的手机号码格式是否正确。
系统验证用户的电子邮件格式是否正确。
系统验证用户的手机号码是否已被使用。
(6)若用户提供的信息有效,则系统会保存该信息并生成唯一的用户ID。
(7)系统向用户发送一条短信验证码,以验证用户的手机号码是否正确。异常场景处理参考“异常场景二”。
(8)用户输入收到的短信验证码,确认自己的手机号码正确。
(9)在用户完成验证后,系统发送欢迎消息,并提供给用户其账户信息和相关优惠信息
异常场景
异常场景一如下。
(1)若用户提供的信息无效,则系统会显示错误的消息,并请求用户重新输入必要的信息。
(2)若用户输入无效信息超过5次,则系统会提示用户尝试其他注册方式。
异常场景二如下。
(1)若手机号码验证失败,则系统会提示用户检查其手机号码是否正确。
(2)若用户无法收到短信验证码,则系统会提供重新发送验证码的选项。
(3)若用户仍然无法收到短信验证码,则系统会提供其他验证方式,比如邮箱验证等。
(4)若短信验证码过期,则系统会提示用户重新发送短信验证码
特殊需求
个人信息安全,需要对用户的手机号码等做脱敏处理后存储
风险预估
无
其他
讨论项:一个人是否可以办理多张会员卡
以上详细用例展示了用例作为一种简洁、高效的结构化需求分析工具的价值。在梳理用户需求时,我们往往更关注正常场景,容易忽视异常场景。因此,在完成详细用例的编写后,我们必须重点讨论和评审其是否全面覆盖了所有异常场景。
完成用例梳理后,很多人可能会直接进入系统设计阶段。通常的做法是先设计数据库表结构和字段,接着根据前端界面和交互需求定义接口及其参数,最后依据流程图用代码逐个实现这些接口。这种基于数据库的开发方式被称为“事务脚本模式”。这里我们不深入探讨事务脚本模式的潜在问题,而是着重介绍如何利用前面梳理的用例来设计和提炼领域模型。领域模型是对领域内概念类或现实世界对象的可视化表示。
首先:领域和子域划分
通过与业务专家沟通,采用归纳法将功能相近的用例归纳并提炼共性。例如,将用户和收银员的注册会员、查看会员信息等功能归纳为会员管理域;将会员卡储值、核销会员权益等功能归纳为会员卡权益管理域;将用户参加促销活动、企业配置活动规则等功能归纳为营销活动管理域。这些领域划分结果如图12所示。
图12 会员系统领域划分
对领域的划分其实在与业务专家的沟通过程中就能很清晰地判断出来,通过用例归纳法大多是对子域进行细化和验证。而对于各个领域,我们还可以进一步细化。例如,对于营销活动管理领域,根据用户和企业经营者职责的不同,可以将其继续划分为活动规则配置、券与礼品配置、营销活动报表三个子域,如图13所示。
图13营销活动管理领域的子域划分
其次:寻找领域对象
划分领域是宏观层面的业务垂直切分,他在DDD中是包含在战略建模范围中的,并且领域还可以很好地帮助我们划分微服务。但是只划分好微服务不是我们的目的,我们希望能更好地开发出可读性高、易维护和易推展的代码。而面向对象编程思想中,最核心的就是如何合理的划分对领域对象,也就是说面向对象分析的精髓就是从领域到重要概念和对象的分解。
那么如何找到领域中重要的概念和对象呢?这里主要参考Craig Larman的《UML和模式应用》书中的三种方法:
1.重用和修改现有的模型。因为在许多常见的领域中都存在已发布的、绘制精细的领域模型和数据模型,比如像RBAC(Role-Based Access Control)权限管理模型等。这里推荐Martin Fowler的《分析模式》一书,在该书中总结了很多常用通常的领域模型。
2.使用分类列表方式。该方式有兴趣可以去研究学习下,这里不过多介绍。
3.通过识别名词短语寻找概念类,又称为用例建模法。该方式是我重点推荐的方式,我们可以通过对所有详细用例的文本进行分析,识别出其中的关键名词和名词短语,将其作为候选的概念类或属性。如下图所示步骤:
图14 用例建模法
名词短语法可以比较容易地找出领域对象和对象之间的关系,从而提炼出领域模型,具体可以采用以下步骤来提炼领域模型。
1.从用例集中找出名词短语。举例:不同等级的会员卡优惠不一样,这句话中的名词短语有“等级”和“会员卡”。
2.根据名词短语梳理领域对象或属性。举例:前文中的“会员卡”可以抽象为领域对象,而像“等级”可能就只是会员卡对象中的一个属性值了。
3.从用例集中找出动词和形容词。举例:一个人可以办理多张会员卡。其中“办理”为动词。
4.根据动词和形容梳理领域对象之间的关系。举例:前文中提到的动词短语“办理多张会员卡”,可以推测出“会员”对象和“会员卡”之间存在一对多的关系。
通过以上步骤,最后我们绘制出的会员领域模型如下图所示:
图15 会员模型
说到这里,可能会有人觉得架构师只是写PPT和文档、讲理论,其实不然。架构师不仅要会画图,更要写代码,而且代码要能精准体现设计的领域模型。接下来,让我们进入大家都很熟悉的代码实现环节。
我们需要先把架构设计做好水平和垂直划分,对应于DDD中提出的战略设计部分,他是架构设计中的核心稳定的主体结构,就像设计房屋大厦中的主体结构一样,一经设计,未来如果需要大变动,那么将会付出非常大的代价的。在软件的架构设计中,这个主体主要包括微服务如何划分,各服务之间的依赖关系是什么样的,以及各微服务内部的层次结构是什么样的。在会员系统项目中,各微服务和模块设计如下图所示:
图16 会员系统逻辑架构图
而微服务内部层次结构按照DDD的四层结构划分的。分层架构的一个重要原则是每层只能与位于其下方的层发生耦合。分层架构可以简单分为两种,即严格分层架构和松散分层架构。在严格分层架构中,某层只能与位于其直接下方的层发生耦合,而在松散分层架构中,则允许某层与它的任意下方层发生耦合。我们采用的是DDD的松散分层架构图如下。
图17 DDD架构分层
其中各层介绍如下:
用户接口层(User Interface):这是用户与系统进行交互的界面层,是系统的最外层,负责处理用户输入和展示系统输出。
应用层(Application):负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑,所以相对来说是较“薄”的一层。
领域层(Domain):负责表达业务概念,实现全部业务逻辑并且通过各种校验手段保证业务正确性,是最核心关键的部分。而什么是业务逻辑呢?它包括业务的流程、策略、规则、状态、以及完整性约束等,所以领域层是较“胖”的一层。
基础设施层(Infrastructure):这一层是系统的支撑层,提供了系统运行所需的基础服务和设施,为其他层的实现提供技术支撑。
其对应会员系统工程代码结构如下图所示:
图18 会员系统工程结构图
在前文中介绍了如何根据用例来分析和划分领域和子域,根据领域和子域创建了微服务和服务内的模块,并且提炼了对应领域的领域模型,接下来将根据核心详细用例继续设计系统内部的具体实现逻辑。根据详细用例,我们可以找到用户和系统之间的交互关系。在会员系统中,根据会员注册这个详细用例,我们采用UML时序图来设计用户和系统之间的交互关系,如下图所示: 整洁架构实践总结 本文在对历史遗留系统问题深入分析后,找到问题的本质解,制定切实有效的解决方案。通过运用整洁架构设计理念,对历史遗留系统进行全面的模型分析和架构重新设计,并借助DDD战术工具实现代码开发。在此过程中,整个团队严格保持统一的代码风格,从而使历史遗留系统焕然一新,变得整洁、清晰且易于维护。这不仅显著提升了系统的可读性和可扩展性,还为未来的功能开发和系统升级奠定了坚实基础。如果想了解更多关于技术债务治理和系统稳定性保障的方法,推荐阅读《高可靠系统构建指南:服务稳定性建设和技术债务治理》。这本书从应急响应、预防措施到主动发现和治理,全方位介绍了提升系统稳定性的策略和实践。
图19 会员注册交互图
在上图中只反映了用户与系统之间的交互关系。因为用户只关心如何使用系统达到自己的目的,所以我们在前期优先设计用户与系统之间的交互关系,而且此时系统实现对于用户来说还是“黑盒”,后续可以继续对该“黑盒”(新会员注册对象NewMemberRegister)进行完善。在这里,用户和新会员注册对象之间的两次交互对应分层架构中的接口层(可能有两个接口),而新会员注册对象对应分层架构中的应用层。为了实现用户与系统之间的两次交互,在应用层的新会员注册对象中就需要具备对应的业务流程,对于其详细的业务流程,可以继续使用UML流程图或时序图来完善设计。这里使用UML时序图来完善新用户注册的详细业务流程,下图所示为关于新用户注册的业务流程时序图。
图20 会员注册时序图
在通过UML时序图和流程图完成“新会员注册”用例对应的系统设计后,接下来我们将使用DDD中提供的战术设计工具来设计高内聚和低耦合的具体代码。Eric Evans在他最经典的《领域驱动设计:软件核心复杂性应对之道》一书中提供了以下具体的工具来实现这一目标。
实体(Entity):实体是一个不由自身属性定义而由它自身身份定义的对象,它是具有状态和行为的。实体对象具有唯一性并且是可持续变化的,也就是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。它的唯一性由唯一的身份标识来决定的,而它的可变性也正反映了实体本身的状态和行为。
值对象(Value Object):只包含元素属性的不可变对象。值对象是将一个值用对象的方式来表述,进而表达一个具体的固定不变的概念。比如某个地址(Address)对象,它不用唯一身份标识id来决定它的唯一性,它只用通过固定不变的概念来表示一个具体的地址就好。
应用服务(Application Service),是用来表达用户故事(User Story)和用例(User Case)的主要手段。应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样的方式能很好的隐藏领域层的复杂性及其内部实现机制。应用层除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,调用外部系统或者向其他系统发送事件消息等。另外,应用层作为展示层与领域层的桥梁,展示层使用VO(视图模型)进行界面展示,与应用层通过DTO(数据传输对象)进行数据交互,从而达到展示层与DO(领域对象)解耦的目的。
领域服务(Domain Service),当领域中的某个操作过程或转换过程不是实体或值对象的职责时(比如跨多个领域对象的操作),我们便应该将该操作放在一个单独的接口中,即领域服务。领域服务是用来协调领域对象完成某个操作,用来处理业务逻辑的,它本身是一个行为,所以是无状态的,状态由领域对象(具有状态和行为)保存。
模块(Module):是指提供特定功能的相对独立的单元,也就是对功能的分解和组合。模块的用途是通过分解领域模型为不同的模块,以降低领域模型的复杂性,提高领域模型的可读性。
聚合(Aggregate):聚合是由聚合根(ROOT ENTITY) 绑定在一起的对象的集合,是领域对象的显示分组,来表达整体的概念(也可以是单一的领域对象),它的宗旨是为了支持领域模型的行为和不变性,同时充当一致性和事务性边界。聚合根通过禁止外部对象对其成员的引用来保证在聚合内进行的更改是一致性的,所以它的难点一般在于一致性的维护上:聚合内实现事务一致性,聚合外实现最终一致性。
工厂(Factory):工厂是用来封装对象创建所必需的知识,它们对创建聚合特别有用。一个对象的创建可能是它自身的主要操作,但是复杂的组装操作不应该成为被创建对象的职责,因为组装这样的职责会产生笨拙的设计,也很难让人理解。而工厂可以帮助封装复杂对象的创建过程,并且当聚合根建立时,所有聚合包含的对象也随之建立了,整个过程是又是原子化的。
仓储(Repository):仓储是对聚合的管理,它介于领域模型和数据模型之间,主要用于聚合的持久化和检索,同时对领域模型和数据模型进行了隔离,以便我们关注于领域模型而不需要考虑如何进行持久化。
接下来使用这些领域驱动设计中提供的战术建模工具来完成会员系统领域模型的代码开发。
首先创建微服务的项目工程,然后在项目工程的领域层创建会员领域模型的领域对象,最后将各个领域对象按照不同的职责分配到对应的微服务中,如下图所示。
图21 会员系统工程结构图
在上图中创建了会员(Member)对象与会员卡(Card)对象等领域对象,并且会员对象与会员卡对象为一对多的关系。在具体的实现过程中,代码必须真实地反映领域模型,若在代码实现中发现之前设计的领域模型不合理,就需要及时地调整设计中的模型结构,尽量保证模型和代码始终一致。在创建领域实体对象后,我们还需要在应用层实现业务流程,并调用相应的领域对象来完成业务逻辑处理。会员系统案例中会员注册用例场景的业务流程编排逻辑代码示例如下:
public class NewMemberRegister { @Resourceprivate RegisterRepository registerRepo;/ 会员注册业务流程实现 @param memberInfoDTO注册信息 @return注册结果/public RegisterResultDTO registerMember(MemberInfoDTO memberInfoDTO){RegisterResultDTO resultDTO = null;// 第1步,判断用户是否已是会员boolean isExist = registerRepo.isMemberExist(memberInfoDTO.getPhone());if(isExist){resultDTO = new RegisterResultDTO(RegisterConst.MEMBER_EXIST,RegisterConst.MEMBER_EXIST_MSG);return resultDTO;}// 第2步,创建会员聚合对象Member member = MemberFactory.createMember(memberInfoDTO);// 第3步,执行注册会员业务逻辑。例如,根据储值金额的不同,开通不同等级的会员卡boolean isSuccess = member.applyMemberCard(memberInfoDTO.getMoney());if(!isSuccess){resultDTO = new RegisterResultDTO(RegisterConst.MEMBER_APPLY,RegisterConst.MEMBER_APPLY_MSG);return resultDTO;}// 第4步,持久化聚合根数据boolean isSave = registerRepo.saveMember(member);if(!isSave){resultDTO = new RegisterResultDTO(RegisterConst.MEMBER_SAVE_ERROR,RegisterConst.MEMBER_SAVE_MSG);return resultDTO;}// 第5步,返回会员卡办理成功的消息resultDTO = assembleRegisterResultDTO(member);return resultDTO;}private RegisterResultDTO assembleRegisterResultDTO(Member member){// 把领域对象转换为传输对象DTO,此处省略代码return resultDTO;}}
该实现代码对应时序设计图(图20),主要在应用层的 NewMemberRegister 对象的registerMember()方法中实现会员注册的业务流程编排。对领域对象的创建则由具体的工厂类实现,因为会员实体和会员卡实体为一对多的关系,所以这里使用了单独的工厂类(MemberFactory)来实现,同时,会员实体充当聚合根对象。代码示例如下:
public class MemberFactory { / 工厂方法,默认绑定一张会员卡,若创建多张会员卡,则请使用createMoreMember()工厂方法 @param memberInfoDTO @return Member对象 / public static Member createMember(MemberInfoDTO memberInfoDTO){ // 创建Member对象,并为其赋值 Member member = new Member(); // 省略Member赋值代码 Card card = new Card(); // 省略Card赋值代码 // 绑定一张会员卡 member.bindCard(card); return member; }}
具体的会员注册业务逻辑主要被封装在Member和Card实体中。例如,在新用户注册场景中,根据储值金额开通不同等级的会员卡并为之赋予相应权益的业务逻辑,对应的实现代码被封装在Member实体中。代码示例如下:
public class Member { /电话号码/private String phone;/姓名/private String name;/会员卡列表/private List
对实体对象的持久化操作,则采用RegisterRepository实现,从而使领域对象和数据库操作解耦,也保证了聚合根内对象的数据一致性。代码如下:
public class RegisterRepositoryImpl implements RegisterRepository { private MemberMapper memberMapper; // MyBatis的mapper对象private CardMapper cardMapper;// MyBatis的mapper对象@Overridepublic boolean isMemberExist(String phone) {return memberMapper.findByPhone(phone);}@Overridepublic boolean saveMember(Member member) {MemberPO memberPO = assembleMemberPO(member);List
至此,我们利用 DDD 工具实现了会员系统中的会员注册用例。通过这种方式编写的代码,不仅整洁清晰,完整体现了设计模型,还能追溯到对应的需求分析,从而确保了实现与设计的一致性。
尊敬的博文视点用户您好: 欢迎您访问本站,您在本站点访问过程中遇到任何问题,均可以在本页留言,我们会根据您的意见和建议,对网站进行不断的优化和改进,给您带来更好的访问体验! 同时,您被采纳的意见和建议,管理员也会赠送您相应的积分...
时隔一周,让大家时刻挂念的《Unity3D实战核心技术详解》终于开放预售啦! 这本书不仅满足了很多年轻人的学习欲望,并且与实际开发相结合,能够解决工作中真实遇到的问题。预售期间优惠多多,实在不容错过! Unity 3D实战核心技术详解 ...
如题 ...
读者评论