从零开始使用CodeArt实践最佳领域驱动开发(二)

4.划分子系统

  使用CA编码项目的核心结构是:由多个子系统组成多个不同的服务来提供项目的各种功能。请不要将这里提到的子系统与大家在别的项目实施方法里的概念混为一谈,CA里的子系统概念是完全不一样的,下面我们详细阐述这一点。

  同一事物在不同领域里的本质特征是不尽相同的,例如书在销售领域的关注点是价格、好评度、热销情况等。但在阅读领域里,书更多的关注点是页码、每页内容、段落注释等特征。因此,要想用常规的方法在不同领域重用同一个事物模型是非常困难的。CA为了解决这类问题将整个项目切分为多个子系统,每个子系统关注各自领域内的特征。这些子系统是真正实现业务逻辑的地方,子系统之间会存在一定的依赖关系,但是这种依赖关系是良性的,不会影响系统的重用性。也就是说,每个子系统都可以单独拿出来引用到别的项目子系统中扩展重用。开发人员可以根据需要将多个子系统组装在一起构成一个新的服务,这项服务适用于某一个特定领域,例如:

  文章子系统 + 汽车子系统 = 提供汽车文集的服务(汽车门户站点)

  相册子系统 + 用户子系统 = 提供用户管理个人相册的服务(社交项目)

  销售子系统 + 书籍子系统 = 书籍贸易(电商站点)

  从层次结构上来讲,服务属于应用层,直接对表现层负责。子系统里的领域对象及业务代码则属于领域模型层。应用层调用领域模型提供的领域方法以便完成业务需求。

  一个项目无论规模多么庞大都可以划分成多个规模量为1的子系统,由于这些子系统的代码量足够少,所以可维护性极高。与传统开发的模式相比,CA里的子系统特点如下:

  1) 子系统不是抽象的概念而是真实存在的代码集合。在.Net平台里一个子系统体现为一个程序集。

  2) 子系统内部仅关注于领域模型的建立,没有任何数据存储的代码。数据的存储由基础设施层里的数据仓储提供。这意味着你可以随时改变存储的机制:切换数据库类型、改变表结构、分布式部署数据库等持久化操作都不会影响到领域模型的改变。

  3) 子系统不仅仅用于一个项目,它可以被任意项目使用。以.Net平台为例,子系统有自己所在的解决方案。当其他项目要使用该子系统时,可以以项目引用、程序集引用等方式重用子系统,但绝对不是复制粘贴源代码到新项目里。子系统的源代码只有一份,升级子系统会让所有使用它的项目收益。

  4) 多个子系统可以集成工作,一个子系统里的领域模型是可以被其他子系统扩展的。这里说的扩展是指在不破坏原有代码的情况下,以继承、组合等方式扩充领域模型的能力。与这种方式相比,很多传统开发模式里所谓的“二次开发”就是把以前做过的代码、设计过的数据库表复制到新项目里,再更改源代码和表设计以满足新的需求,这根本就不是扩展而是重写。

  5) 子系统不能直接用于表现层,它们工作的场所是在应用层的服务里。你可以使用任意技术搭建服务。在.Net平台下可以部署在IIS里,也可以使用专用于CA的服务器端应用程序部署项目的服务。

  有了CA开发项目的结构说明和之前分析原始需求的结果,我们可以继续展开会议系统的编码工作了。

  根据前文所述,我们要先为“菜单”、“功能”、“用户”、“角色”等事物创建一个服务,服务会提供各种接口以供表现层调用,例如:创建菜单、新增功能描述等服务接口。请注意,把“菜单”、“功能”、“用户”、“角色”这些事物放在同一个服务里未必正确,我们会在后续的开发工作里基于各种原则将服务分离,创建多个服务、多个子系统。但是在眼前我们不必过多考虑这一点,大胆的去做吧。

  为服务命名是我们要考虑的第一件事。大家不要忽略命名的重要性,为服务命名、为子系统命名、为领域对象、领域属性、领域方法命名都是需要你认真对待的工作。经过一番思考后,我们认为“菜单”、“功能”、“用户”、“角色”等事物是一个项目里几乎必备的事物,是一切的源头。所以我们引用门户(Portal)这个词作为服务的名称,表示系统的入口,因此服务的全称为PortalService。

  PortalService可以接受表现层的请求,处理关于门户方面的调用命令并返回处理结果给表现层使用。这点大家可以联想下调用淘宝提供的接口,淘宝返回数据给你使用的场景。以.Net平台为例,我们为门户服务建立解决方案PortalService的结构如下图:

 

  

  1) 解决方案文件夹Framework里引用的是CA提供的部分类库,在后续教程里会详细说明这些库的用法。在这里我们只用知道引用的类库是构建服务必不可缺的。

  2) 解决方案文件夹Subsystems表示服务需要用到的子系统,目前服务没有引用任何子系统,稍后我们会创建。

  3) portal.services.codeart.cn是托管至IIS的门户服务站点,你也可以使用其他技术部署服务,在这里我们以站点为例。

  4) PortalService.Application是门户服务的应用程序集,在这个程序集里主要使用子系统提供的应用命令来完成服务的调用,后面会有详细的说明。

  5) PortalServiceTest是单元测试程序集。

  创建完门户服务解决方案后,我们需要为其添加子系统。正确的划分子系统是使用CA的一项重要工作。你可以从以下几个角度去分析如何找出子系统:

  1) 在已知的事物里,哪些事物是最独立的?独立是指构建该事物的模型不会依赖于其他事物的模型。由于菜单、角色都会涉及到功能的分配,它们的领域模型与功能肯定会有某种依赖关系,所以我们认为菜单和角色这两个事物不够独立。那么“功能”呢?描述系统的功能只需要一个简单的名称和描述即可,不会依赖任何其他事物而存在,所以”功能“足够独立。我们以“功能”为突破口找出潜在的子系统。

  2) 为独立的事物正名。只要是确定要为其建立模型的事物,我们都需要考虑它的名称是否合理。因为我们得到的事物是从现实世界里表面需求分析而来的,这样的事物并非真正贴切程序里的领域模型,在程序世界里有其独有的描述方式。“功能” 这个名称比较含糊,能代表的概念很多,不适用于程序命名。另外,我们在谈及到角色的时候,不是角色有哪些功能而是角色拥有哪些权限。所以,将“功能”正名为“权限”是一个不错的主意。我们统一语言后,会将之前分析到的需求更改为“可以为角色分配权限”、“可以为菜单设置哪些权限能够使用它”。

  3) 确定了独立事物的名称后,我们就能以此为基础假设要建立一个与该事物相关的子系统。在这个例子里也就是“权限子系统”。目前,该子系统需要提供哪些应用上的帮助我们还比较模糊,但是可以确定的是权限子系统需要提供创建权限、修改权限、删除权限等操作。权限子系统里面一定会有权限的领域模型。

  4) 事实上分析到第3步就可以编码完成权限子系统的第一个版本了,但是由于我们提供的是使用CA的教程,不可能完全演绎出真实项目迭代实施的每个细节,真要如此需要写一本独立的书籍了,也许以后我会抽时间去著作完成,但是在这里我们会浓缩下项目实施的过程,提前告知各位正确的设计方式。从第5点开始后面的内容都是我们在实际工作中反复提炼后得到的经验与教训。同样的,大家在领域实战的时候也会不可避免的犯设计上的错误,请无须担心,大胆的去尝试吧。

  5) “权限子系统”这个想法很好,从概念上讲几乎无懈可击,但是从务实的角度来考虑会有些问题。如果我们为一个领域模型去创建一个子系统,这样使用起来会比较麻烦。你试想一下,如果有“菜单子系统”、“角色子系统”、“权限子系统”,当我们要在服务里创建一个角色时,这个服务必须引用“角色子系统”和“权限子系统”才能完成工作,如果对象引用链比较多,你有可能需要引用的子系统数量远超过预期。例如:用户子系统会引用账户子系统、账户子系统会引用权限子系统和角色子系统、用户子系统还会引用地理位置子系统用以表示用户所在地。这时候服务要使用用户子系统就不得不多引用4个额外的项目,不仅麻烦也不利于维护更新。关于如何切断引用链,让子系统更加的独立的话题后面会有更详细的说明,这里我们只用知道尽量不要为一个事物单独创建子系统。

  6) 因此,我们需要将内聚性比较高的事物合并在一个子系统里。找出与权限模型紧密相联的事物,再结合子系统应该提供的职责去综合考虑是否将其他事物也纳入到权限子系统中。很明显,角色是与权限是密不可分的,角色不可能脱离权限独立存在,那样的话角色就没有意义了,我们使用角色的目的就是为了识别用户身份,所谓“身份”在程序里的体现就是拥有哪些权限。所以角色可以加入到权限子系统中。那么菜单呢?菜单的确依赖于权限模型,但是这种依赖关系不代表菜单一定要和权限在同一个子系统里。权限子系统的主要职责是提供身份识别,这和菜单自身没有任何关系。所以我们不必将菜单放入到权限子系统里而是考虑稍后建立“菜单子系统”再由“菜单子系统”引用“权限子系统”集成工作。

  7) 明确了权限、角色、菜单这三者所属子系统的关系后,我们再来分析“用户”应该被划分至哪个子系统。首先,用户这一事物足够的模糊也足够的复杂。说它模糊是因为在会议系统实施的前期我们还无法深刻的认识到用户会有哪些具体的行为,我们仅知道用户可以登录系统,可以创建会议,与会人可以参加会议、会议主持人可以管理会议,但是这些仅仅只是整个项目需求的冰山一角,用户可以参与的功能实在太多了。之所以说它复杂也正是因为用户涉及到的功能众多,为了向那些已知或未知的功能提供用户不同维度的信息以便功能能正常的使用,我们有可能会频繁的修改用户模型。因此,用户模型的可变化性是非常强的。如果把用户模型放入权限子系统,那么一旦用户模型改变就会引起权限子系统的代码改动。我们希望子系统尽可能的稳定,变化率足够的小。另外,用户只是使用角色、权限等模型来证明自己的身份,角色、权限并没有依赖于用户。换句话说,用户模型引用了角色和权限模型,但是角色和权限模型并没有引用用户模型。用户模型的任何变化都不会影响到权限和角色模型的改变。所以,用户和权限子系统里其它模型的内聚性非常低,我们不应该把内聚性低的事物放在同一个程序集里。

  8)综上所述,”用户”不应该放在权限子系统里。我们可以像设计“菜单子系统”那样建立“用户子系统”,再由用户子系统引用权限子系统来工作。经过一番尝试,我们发现这样做虽然可以满足需求,但是用户子系统里会编写大量关于“登录”、“身份识别”等与权限系统有关的代码。因为在身份识别的时候,角色和权限是不能绕过用户的信息单独提供给应用层使用的。使用者在登录的时候还需要提供登录名和密码的信息交由用户子系统判断是否正确,然后再得到用户的角色和权限。所有与身份有关的请求都需要经过用户子系统处理,而权限子系统仅仅是提供了角色和权限的数据而已,这样用户子系统过多的关注了权限子系统要考虑的本职操作。如果这样的话,我们还不如将用户和角色、权限放在同一个子系统里。而这又与我们之前的判断相违背。那问题究竟出在哪里呢?经过一番思考,我们认为缺少一个管理用户身份的事物,这个事物提供了帮助用户管理其身份和安全验证的职责,它是用户和权限、角色之间的桥梁,我们为其命名为“账号”。由账号来分担用户这个事物在身份识别领域里的职责,用户不必直接与角色、权限打交道。所以我们把账号、角色、权限放在权限子系统里,而把用户放在用户子系统里,用户模型引用一个账号模型,当需要识别身份的时候只用调用账号来工作就可以了。

  9) 最后,由于权限子系统里的模型已经不单单只有权限了,所以我们为权限子系统重命名为账户子系统(AccountSubsystem)。这样第一个子系统的分析工作就已完成。再次声明,因为篇幅原因我们浓缩了整个分析的过程。在实际工作的时候,我们是经过多次迭代和修正才完成了正确的系统划分。在这个过程里,我们每次都以小的增量重构项目,每次重构的结果都不仅满足用户的需求而且越来越贴近事物的本质特征。这使得我们的程序越来越健壮,越来越容易维护,每次修改都很轻松,不会引起连锁改动。大家在实际编码的时候尽管大胆的去设计,多多锻炼和积累自己的领域思想自然水到渠成,由量变到质变,你会发现自己的思维越来越敏捷,对错误的设计越来越敏感,思考问题的方式也异于常人,这时候就要恭喜你踏入了领域艺术家的境界,成为了一名真正的架构设计师。

  下面我们创建账户子系统(AccountSubsystem),账户子系统虽然被门户服务使用,但是子系统本身是独立于任何服务存在的。所以我们为账户子系统创建独立的项目解决方案:

  子系统的项目解决方案比服务的项目解决方案需要引用的程序集少很多。除了解决方案文件夹Framework里需要引用几个CodeArt提供的类库外,仅需额外创建一个AccountSubsystem的类库项目,稍后我们会在这个程序集里创建领域模型。AccountSubsystemTest则是针对账户子系统的单元测试。

  大家应该还记得,我们在门户服务的解决方案里也添加了一个专门针对门户服务的单元测试PortalServiceTest。严格的讲,有效的自动化测试越多越好,这样会给系统的维护带来极大的好处。但是测试是有成本的,编写测试用例、编码实现测试用例都是需要时间完成的。所以在项目开发的过程中我们会更多的关注服务的测试,因为服务的测试会覆盖到子系统的代码,测试服务也会间接测试到子系统,一举多得节约成本。只有当整个项目快结束的时候或者有空闲时间了,我们才会再来补充子系统的测试。

  所以,当我们创建了AccountSubsystem项目解决方案并将代码提交到代码库后,可以直接关闭该解决方案。打开PortalService,将AccountSubsystem子系统引用到PortalService的Subsystems解决方案文件夹里:

  至此,为PortalService创建账户子系统的工作就完成了。在下个章节里我们开始进行领域模型的设计工作。

欢迎各位加入CodeArt学习群共同进步,群号:558084219

程序员不是任何人的工具,更不是碌碌无为的码农。我希望每一位使用CodeArt的程序员都能成为程序世界里的王者,用创造力去构建自己的领域。即使遭遇重重困难也不气馁、受到他人阻碍时亦有不屈服之心、遇到不公正时能毫不畏惧地纠正,不向官僚献媚。
原文地址:https://www.cnblogs.com/codeart/p/7100178.html