1

二代水务系统架构设计分享——DDD+个性化 - 天行健君子以自强

 1 year ago
source link: https://www.cnblogs.com/fengjq/p/17604906.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

二代水务系统架构设计分享——DDD+个性化

C/S架构的单体桌面应用,可以满足客户个性化需求,易于升级和维护。相比于一代Winform,界面要求美观,控件丰富可定制。

依托.Net6开发平台,采用模块化思想设计(即分而治之的策略),每个模块采用DDD分层设计。前端选用WPF + Prism框架,后端选用ABP + EF框架,数据库选择SQL Server。

核心领域:包含用户管理、客户管理、表具管理、方案管理、抄表管理

通用领域:包含权限、菜单、个人中心、参数配置、审计日志、数据字典

支撑领域:包含数据查询、统计报表、消息管理、STS安全、工单、自动升级

通过业务拆分,水务领域已经被划分为若干子领域(即模块)。每个模块可以看成是一个限界上下文,在此边界内可以进一步拆分更细粒度的单元(我们把最小的业务单元叫做聚合)。一个模块也可能只有一个聚合,如:数据字典。

先拿简单的用户管理来说,用户(User)、角色(Role)、组织(Organization)三者关系紧密、彼此协作。一个用户可以拥有多个角色,一个组织可以拥有多个用户,因此可以归属到用户限界上下文,它们既是实体又是聚合根。

再拿复杂一点的客户管理来说,客户可分为预付费和后付费两种。对于预付费客户来说,可以按量或按金额购水。对于后付费客户来说,每月会生成一笔月账单,客户需要及时充值,否则就会欠费。因此,在客户这个业务边界内,可以确定客户(Customer)、客户类型(CustomerType)、交易记录(Transactions)、月账单(MonthlyBilling)等领域对象。其中水费的计算是一个非常复杂的业务逻辑,涉及到阶梯价格、附加费、债务、当月历史售水交易等多个领域实体,所以需要借助领域服务来完成这笔交易。

系统设计包含两部分:全局设计局部设计

全局设计是从框架角度来进行整体设计。系统框图如下:

895729-20230807101301633-200074053.png

虚线框部分为笔者设计的Lapis.Framework框架,文件组织结构如下:

895729-20230807111742106-703904730.png

其中,Laison.Lapis.XXX文件夹代表基础业务模块(Laison.Lapis.Shared除外),如:审计日志、消息管理等,每个模块就是一个独立的C#解决方案。Laison.Lapis是一个底层类库,封装各种通用基础功能,供上层调用。Laison.Lapis.Shared称之为共享模块,用于封装和业务无关,可以被业务模块共享使用的功能,如: 抽象基类、接口、虚方法等。因为是单体应用设计,所以模块之间少不了相互引用的关系。模块划分越多,引用关系也会变得越复杂。

局部设计是从业务角度来进行模块设计,每个模块按照DDD来分层:基础设施层、领域层、应用层和表现层。下面是用户管理模块(Laison.Lapis.Identity)的分层设计:

895729-20230804155020416-805518028.png

     Laison.Lapis.Identity.UI:表现层,包含前端页面、控件和一些UI逻辑。
     Laison.Lapis.Identity.Application.Contracts:应用接口层,负责定义应用接口和DTO。
     Laison.Lapis.Identity.Application:应用层,用于实现应用接口,包含各种应用逻辑。
     Laison.Lapis.Identity.Domain:领域层,负责业务逻辑处理,包含实体、值对象、仓储接口等领域模型。
     Laison.Lapis.Identity.Domain.Shared:领域共享层,包含了一些枚举和常量的定义,可供其它任意层调用。
     Laison.Lapis.Identity.EntityFramework:实体框架层(EF),包含实体的映射配置和仓储的实现。
     Laison.Lapis.Identity.HttpApi.Host 和 Laison.Lapis.Identity.Shell:即宿主和壳,分别用来启动前/后端应用程序。

在ABP框架的世界里,每一分层就是一个模块单元,它是代码层面的模块,而Identity是业务层面的模块,后者包含前者,最终以DLL的形式来呈现。

系统如何运行

每个模块负责自己的业务功能,各司其职,又彼此依赖。模块划分越细,内聚性越强,代码的复用性就越高。 当系统要完整运行所有功能时,只需要像搭积木一样,把各个模块进行组装就行,做到即插即用。为了方便代码复用和管理,笔者采用Nuget包的形式将业务模块引入到项目中使用,这在开发初期似乎没有什么问题。

但随着系统不断迭代,模块数量也再随之增长。假设1个模块会生成6个DLL(采用DDD分层),那么20个模块就会有120个DLL,听上去有点吓人。每次模块发生变化,就要重新打包、发布、升级120个Nuget包,这显然不符合常理(非常耗时)。后来笔者采取了一种折中的处理方式:将通用稳定性强的模块保留在Lapis.Framework中,把易于变化的模块下沉到项目中管理,这样就能避免之前的尴尬,但却牺牲了模块代码的复用性。项目最终代码结构如下:

895729-20230807112456204-1347187231.png

个性化定制

企业个性化功能定制是一个普遍而又绕不开的话题,假如能用一套代码把所有客户的需求全部覆盖,当然是最理想的状态(现实几乎不可能,除非通用标准化)。所以笔者从设计一开始就是围绕如何提高代码的复用,将差异化分散到项目中的思路来展开,这也是采用模块化设计的初衷。至于个性化如何来实施?根据笔者已知的做法有以下两种:1. 通过版本管理开分支的方式,2. 简单粗暴,每个项目都单独复制一套标准版,然后在上面做定制化修改。 除此之外,笔者似乎没有想到更好的办法。至于选择哪一种,就要根据项目实际情况来考量。

无论是模块化设计还是DDD,都是强调从业务领域角度出发,根据业务的发展,合理划分业务边界,采用分治策略,降低业务和软件开发的复杂度,持续调整现有架构,以保持架构和代码的生命力。 因此,世界上并不存在一劳永逸的架构,软件设计本身就是一个平衡取舍的过程,只有合适的才是最好的,这也许就是架构设计的魅力所在吧!如果你有好的建议或不同想法,欢迎评论区留言。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK