5.领域模型设计
下面我们创建账户子系统(AccountSubsystem),账户子系统虽然被门户服务使用,但是子系统本身是独立于任何服务存在的。所以我们为账户子系统创建独立的项目解决方案:
子系统的项目解决方案比服务的项目解决方案需要引用的程序集少很多。除了解决方案文件夹Framework里需要引用几个CodeArt提供的类库外,仅需额外创建一个AccountSubsystem的类库项目,稍后我们会在这个程序集里创建领域模型。AccountSubsystemTest则是针对账户子系统的单元测试。
大家应该还记得,我们在门户服务的解决方案里也添加了一个专门针对门户服务的单元测试PortalServiceTest。严格的讲,有效的自动化测试越多越好,这样会给系统的维护带来极大的好处。但是测试是有成本的,编写测试用例、编码实现测试用例都是需要时间完成的。所以在项目开发的过程中我们会更多的关注服务的测试,因为服务的测试会覆盖到子系统的代码,测试服务也会间接测试到子系统,一举多得节约成本。只有当整个项目快结束的时候或者有空闲时间了,我们才会再来补充子系统的测试。
所以,当我们创建了AccountSubsystem项目解决方案并将代码提交到代码库后,可以直接关闭该解决方案。打开PortalService,将AccountSubsystem子系统引用到PortalService的Subsystems解决方案文件夹里:
至此,为PortalService创建账户子系统的工作就完成了。我们可以开始考虑如何构建账户子系统的领域模型了。在讨论这一话题之前,我们先来看看关于CA里领域模型的基本概念。
概念1:领域对象,领域模型里的一切对象都应该是领域对象。所谓的领域对象就是指遵守领域规则的对象,这是它与普通对象最大的区别。领域规则主要由以下几点组成:
1) 领域对象永远都不会为null。在详细说明这一点之前,大家一定要知道这个规则跟数据库里的字段不能为null没有任何关系。到目前为止,我们都没有提到如何使用数据库技术,这个教程之后的内容也不会讲述与数据库有关的话题,因为这些都与领域模型的建立没有一丁点的关系,没有数据库一样可以成功建立领域模型。各位一定要摒弃以前根深蒂固的开发思路,放空自己的思想,重新接收领域模型里的概念。
言归正传,在领域的世界里,一切对象都是有其存在的意义的。什么叫存在的意义?这体现在职责上,一个对象可以履行某项职责那么这个对象就是有意义的存在。在领域世界里绝对不会出现不具备任何职责的对象。大多数情况下,对象需要提供方法以便其履行职责(.Net里的属性也是方法,只是被包装了而已)。
每年企业都会向政府交纳一定的税收,这是企业的职责之一,所以领域世界里的企业模型会有一个计算纳税金额的方法,该方法会根据企业的盈利计算出交税的金额。如果我问你,你创办的公司今年要交多少税?你可以通过该方法计算结果,告诉我需要交纳10万元。那么,你没有公司呢?你根本就没有自己创办的公司,那你要如何回答我这个问题?回答“很抱歉,我没有公司”吗?错,程序的世界是理性的世界,一切都是严谨的,计算纳税金额的方法定义返回值是浮点数,那么这个方法在任何情况下都应该返回一个浮点数(抛出异常例外)。当我问你的公司要交纳多少税,你只能告诉我具体的数值,我不管你是否有自己的公司,那是你自己要考虑的事情。所以,你应该回答“0”。
分析到这里,大家发现一个问题没有,一个没有创办公司的人却也可以知道“自己创办的公司”今年要交纳多少税收,只不过由于他实际上没有公司,所以计算的结果为0。因此,判定事物能否履行某项职责和这个事物的实例是否存在没有必然的联系。我们只要设计了名称为Corp的企业领域模型,在Corp类里定义了方法CalculateTax(计算税收),那么在任何情况下,Corp的实例都应该可以成功的调用该方法,至于计算的结果由Corp内部去处理,外界不用考虑这些细节。所以,即使不存在编号为135的Corp对象,我们依然可以根据编号135找到一个为Empty的Corp对象,调用该对象的CalculateTax方法会计算并得到税收值为0的结果。
所以,在领域的世界里,我们规定对象可以为Empty但是永远不能为null,因为null表示不存在,不存在的对象无法履行任何职责。我们衡量对象是否存在的唯一依据就是职责,一个没有职责的对象根本就不会被设计出来,所以领域世界里没有不存在的对象,而Empty则表示对象是存在的,只不过数据都是空的,空对象依然可以履行职责,只是履行职责的结果会和非空对象执行的结果有所不同。下图是体现这一规则的INotNullObject接口定义:
2) 每个领域对象都具有验证固定规则的能力。固定规则与业务规则不同,这组规则不会随着使用对象的场景的变化而变化,它是对象自身固有的规则。例如人的年龄不可能有上万岁,汽车的轮胎个数也不会有上百个,这些都是领域模型里的固定规则,不会随着人或汽车这一事物在不同的使用场景里而发生规则的改变。CA通过实现ISupportFixedRules接口来完成领域对象验证固定规则的能力:
3) 我们可以明确的知道领域对象的仓储状态,领域对象的仓储状态与它在仓储中保存的数据映射有关。当新建一个领域对象时,该对象的仓储状态就是“新建”的;使用完领域对象,将其存入仓储后,该对象的状态就是“干净”的;我们从仓储中获取一个对象,并更改了对象的属性值,但还未提交给仓储再次保存的时候,该对象的状态就是“脏”的。对象的仓储状态与对象的持久化操作息息相关,所以CA明确规定每个领域对象都能提供和更改自身的仓储状态,该领域规则由IStateObject诠释:
除了以上3项规则外,我们在设计领域对象时还需要遵循一些其他的领域规则,这些规则会以约定的形式给出而不是显示的接口,这在后面的实践中会详细讨论。所有的领域对象都实现了IDomainObject接口:
概念2:实体对象。这类对象具有可区别性,可以与其他事物区分开来。不同颜色、款式的衣服肯定是不同的实体,但是同样颜色、同样款式的衣服就一定是同一个实体吗?在现实世界,人们可以感性的判定事物的唯一性,比如我买的衣服和你穿的衣服就算一模一样,但是它们理所当然的不是同一件衣服。然而在程序世界一切都是理性的,所有判断都是要有根据的。
领域实体对象的职责之一就是帮助程序辨别不同的对象实例,区分事物的唯一性。每个实体对象都需要提供唯一的标识符来标识自己的存在。我们能够通过该标识符找到唯一一个对应的实体,这是实体对象的重要特征。通过标识符可以引用到一个对象,这也是实体对象常被称为引用对象的原因。
我们可以为衣服设置一个叫做编号的唯一标示符。我买的衣服编号为1,你穿的衣服编号为2,就算衣服的其他属性值都相同,但是由于唯一标识符不同,所以这两件衣服在系统看来是不同的实体。当我调用洗衣机的方法去洗我的衣服(编号1),不会对你的衣服(编号为2)造成影响。也就是说,随着时间轴的推移,实体对象的状态会由于各种领域行为的发生而导致了改变,但我们依然可以根据标识符找到目标实体并关注状态变化的情况。以洗衣为例,我的衣服(编号1)变的干净了,因此我很满意。而你的衣服(编号2)依然那么脏,但与我无关。
这正是我们使用实体对象的根本原因:追踪目标对象状态的变化情况,以便其行为更清楚且可预测。也就是说,当我们需要持续关注一个事物的变化情况时,我们应该将该事物的模型设计为实体对象。以下是CA里实体对象必须实现的接口(该接口不必程序员实现,CA提供了实体对象的基类):
概念3:值对象。如果一个对象的所有属性都是用于从某种角度来描述另外一个事物的状态时,这个对象就是值对象。
继续以衣服为例,我们可以把衣服的颜色、图案、尺码这3个属性提取出来,作为一个单独的值对象叫“外貌”,再由衣服去引用这个“外貌”。这样我与你的衣服虽然不是同一件衣服,但是外貌特征可以相同,都是白色、花型图案、XL码的体恤衫。由于衣服是实体对象,我们依然可以清楚的区分衣服的唯一性,但是我们的衣服共用了相同的外貌特征。所以,值对象是没有唯一性判断依据的,它只是多个描述事物状态值的综合载体,如果两个值对象的属性值都相同,那么我们认为这两个值对象是同一个对象,因为他们描述事物的结果是一样的。使用值对象需要遵守两个领域规则:
1) 值对象的所有属性都是只读的,只有当构造它的时候才能传入值,构造完毕后,值对象的属性将无法更改。这意味着当我想把衣服染成红色的时候,我只能新建一个“红色、花型图案、XL码”的外貌特征,并将衣服的外貌属性设置成新建的这个值对象。我不能更改现有“外貌”的“颜色”属性,因为一旦更改了,你衣服的外貌也被更改了,因为我和你的衣服都是引用的同一个外貌特征值对象。因此,在CA里,值对象的设计必须保证属性是只读的。
2) 我们要保证值对象里的所有属性都是从同一个角度去描述事物的。使用值对象的一个很重要的原因就是将复杂事物的属性提取出来,单独作为一个对象管理,这样可以提高程序的可维护性。例如在订单这个事物里涉及到的信息会很多:购买的商品、商品数量、优惠活动、购买人、支付方式、发货方式等。其中“收件人所在的省份城市”、“详细地址”、“邮编”这三项信息都与收货地址有关,我们可以把这类描述收货地址的属性集中起来,设计一个地址(Address)的值对象来管理他们。订单就可以使用该对象来承担与收货地址相关的职责。因此,如果一个值对象的属性是杂乱无章的,不能从同一个角度描述事物的特点,那这个值对象是没有存在价值的。
概念4:内聚模型。一个模块内部各个元素彼此结合的越紧密则它的内聚性越强。也正是由于这些元素结合的非常紧密,他们往往也只负责某一项任务,这也就是所谓的单一职责原则。
我们之前创建了门户服务,因为我们认为菜单、角色、权限等事物是整个项目运行的基础,这几个事物之间或多或少存在某些联系,有一定的内聚性,所以将这几个事物划分到一个服务里共同驱动门户服务工作。
另外,角色、权限、账号三者的关系远比菜单更加紧密,即使没有菜单对象,他们仍然可以共同担负起身份识别的职责。因此我们将他们纳入到账户子系统中,账户子系统可以脱离门户服务独立被其他服务使用。所以,账户子系统是一个比门户服务更高的内聚模块。子系统的内聚性要远高于服务。
那么在子系统内部呢?子系统内部我们依然可以按照类似的思路,根据对象之间的紧密程度划分出更高的内聚模型,这就是我们要熟悉的第4个概念:领域模型里的内聚模型。在详细讨论这个概念之前,我们需要搞清楚如何判断对象之间是紧密的。如果对象A引用了对象B,当A在履行某些职责的时候,需要对象B的支持,我们就说A依赖于B。这在程序实现里常常表现为当调用A的方法MA时,MA内部又调用了B的方法MB来协助MA顺利的执行,这种情况下A和B的关系就比较紧密。那么,有没有比这种AB关系更加紧密的关系呢?有的。如果对象A的生命周期依赖于对象B的生命周期,也就是说,只有B出现在了程序里,A才有可能出现,而当B被销毁了,A就一定会被销毁。对象A的生命周期始终依赖于对象B的生命周期,那么这种关系就是更加紧密的,这也就是内聚模型里的对象之间的关系。使用内聚模型的领域规则如下:
1) 每个内聚模型里都会有且仅有一个内聚根,内聚根是一个实体对象。这意味着你可以通过唯一标识找到该内聚根。
本章节未完,稍后更新全部内容。。。。。。