分析模式——可复用的对象模型

最近读了《分析模式——可复用的对象模型》一书,这本书算是DDD方面的一本启蒙书籍,阅读完成后还是受益良多,一方面是很多观点和平时的工作相互印证,很受启发,另一方面学到了一些业务建模的模式,本文主要总结一下收到启发的地方和建模原则,具体的建模方式举一个例子体会一下就好。

这里先简单说几个我最有收获的几个点:

  • 对象开发的一个重要原则是是软件的结构反映问题的结构。
  • 问题没有梳理清楚导致的耦合是没有办法通过设计模式来解决的,比如物流和订单,他们使用了同一个数据表,他们需要互相感知到对方,那么他们就一定在某种程度上的耦合在一起,属于本质复杂度,这种耦合是没办法通过良好的设计模式进行解耦的,即使它们是两个系统,他们也要一起改动。
  • 业务领域的划分也要做到高内聚,低耦合,最少知识原则,如果每个业务领域需要知道很多其他业务领域的知识,那么其实还是耦合的。
  • 不同领域之间如果非要有联系,也尽量减少双向的联系,每一个双向的联系都会为系统带来更高的复杂度。
  • 在系统开始阶段要尽量降低系统复杂度的同时,对修改保持开放,也就是开闭原则,除非十分必要,不要上来就引入很多第三方工具,要考虑这些工具的引入成本,维护成本,升级成本,甚至是机会成本,也不要上来就搞什么微服务,微服务最好是从一个成熟的系统中经过验证稳定的业务领域中孵化出来,否则只是单纯的技术架构上的微服务只会徒增系统整体的复杂性和风险。

绪论

我们可以把架构设计简单分为横向和纵向的划分,横向的划分,解决的是业务架构,为的是降低业务逻辑的本质复杂度,降低业务领域之间的耦合,而纵向的架构解决的是非功能性的需求,比如通过缓存层,消息队列等技术来提高稳定性,吞吐率等。

我们本文的重点就是解决横向的架构设计的一些原则。

分析的目的是为了理解问题,在我看来,这可不仅是一个使用用例来罗列需求的过程。在系统开发过程中,用例就算不是必不可少的,也是很有价值的,但是捕获这种用例并不意味着分析的结束。分析还涉及透过表面需求以提出反映问题内在机制的心智模型。

考虑编写一个模拟台球比赛的软件,可以通过描述表面特征的用例来评估这个问题:“玩家击打白球,使它以一定速度移动,接着,白球又以一定的角度击中了红球,使红球以一定的方向移动了一定的距离。”可以拍摄几百次这样的事件,并测量球的速度,角度和移动的距离。然而仅靠这些恐怕不足以写出好的模拟程序,要写好这个程序,需要透过表面现象去了解其背后的运动规律,包括质量,速度,动量等。

像台球比赛这样的问题并不多见,因为这些运动规律广为人知。然后在许多企业中,这一层面的基本机制还没有被很好地理解,需要我们去努力地发现。

模型无所谓对错,关键在于哪个模型更加合用。

模型的选择可能会影响开发出来的软件的灵活性和可复用性。对于台球比赛这种软件,你可能会主张使用爱因斯坦模型,因为开发出来的软件可以灵活到足以处理原子碰撞问题。但这样处理比较危险,因为引入太多的灵活性可能导致系统过于复杂,这是一种糟糕的工程实践

使用分析和设计技术的主要原因之一是为了让领域专家参与进来,而这对概念建模来说至关重要。有效的模型只能由真正了解该领域的人来构建,这些人是该领域的全职工作者。IT技能对于建模技能既没有帮助也没有阻碍。

分析技术应该与软件技术相互独立,理想情况下,概念建模应该完全独立于软件技术,就像上面说的运动定律那样。这种独立性可以防止技术阻碍人们对问题的理解,并且得到的模型对各种软件技术都同样有效。

与概念模型密切相关的是软件的接口而不是软件的实现。

一些建模原则

这里罗列一些书中的建模原则,其实这些原则和设计模式的的原则是相通的,二者只是关注点不同。

  1. 建模时应该是模型中变化最频繁的部分所影响的类型数量最小化。(这一点其实和设计模式的将容易变化和不容易变化的部分分开是相通的)
  2. 当为具有超类型的类型定义特性时,考虑将这些特性放在超类型上是否合理。
  3. 应该显式地将模型分为操作层和知识层。举个例子,配置类的文件或者某些我们自定义的可以解析的语义是知识层,它可以定义类之间的关系以及某些程序运行的原则,而具体如何实例化类属于操作层。
  4. 当多个属性通过可以在若干个类型中使用的行为相互作用时,应该将这些属性组合成一个新的基本类型。比如我们系统中涉及大量的不同单位的数据,我们可以将单位和数值从这些数据中抽取出来组合成数量类型。
  5. 操作层包含那些每天都发生变化的概念,这些概念的配置由知识层进行约束,其变化频率会低很多。这里的变化频率指的是运行时的变化。
  6. 如果一个类型有非常多类似的关联,那么将这些关联对象抽象成一个新类型,再创建一个知识层来区分它们。
  7. 要记录某个值的变化历史,应该为该值建立一个账户。
  8. 使用账户时,应该遵循如下守恒原则:所记录的项目不能被创建或者销毁,只能从一处转移到另一处,这样可以更加容易查找和避免漏洞。
  9. 想要了解一个计算是如何进行的,可以将计算结果表示为一个对象,该对象用于记住创建它的计算以及所使用的输入值。
  10. 当有多个等价的特性集可供选择时,选取领域专家认为最合适的那个,如果领域专家认为二者都非常有价值,那么就将二者都抽取出来,并将其中之一标记为派生。
  11. 将一个特性标记为派生,只是对接口的约束,并不影响底层的数据结构。
  12. 如果可以通过不同的条件来创建对象的集合,那就应该使用组合。
  13. 在将一个处理过程作为某个类型的一项特性时,应该为该过程提供一个抽象接口,以便通过子类化方便地改变其实现。例如,纯粹硬编码的实现是一个子类,各种参数驱动的方法是其他子类。
  14. 当多个属性在某个行为中进行交互,而该行为又可能在多个类型中使用时,应该将这些属性组合成一个新的基本类型。
  15. 如果超类型适用的领域较窄,子类型适用的领域较宽,那么就不应该使用泛化。
  16. 如果两个相似类型之间的差别通常可以忽略,那么就可以使用抽象超类型。如果两者之间的差别很重要,那么就不要使用抽象超类型。
  17. 如果使用一个抽象超类型不会给客户端带来更多的工作量,那么就应该提供这个抽象类型。
  18. 当某种信息既可能从一个信息源直接获取,也可能根据其他可用数据计算出来时,应该提供一个抽象接口,将从信息源获取和计算这两种方式分别作为其子类。
  19. 当面临几种替代方法时,首先选择最简单的方法,然后根据需要改为更复杂的方法。
  20. 当即中建模方案中难以抉择时,遵循领域专家的直觉。
  21. 只有在下述情况下才应该使用子类型:超类型所有特性都适用于子类型,并且从概念上,子类型的每个实例都是超类型的实例这一说法是合理的。
  22. 当以客户视角看到单笔交易可以被交易员分为多笔交易时,应该将产品和合同分开。产品与合同区分的关键在于,产品代表客户意图,而合同指的是对手方和主方之间的实际交易的内容。
  23. 不要重复有相同含义的基本关联。遵循这一原则,就可以获得职责划分良好的类型。
  24. 在职责分配上应保持一致。要小心那些有时对某事负责,有时又将该职责委托出去的类型。(这种行为可能是正确的,但总是应该对其保持怀疑的态度)
  25. 单向关联和双向关联之间做出选择需要在两个方面进行权衡,一方面是类型开发工作人员的工作量小(减少类型间的耦合);另一方面是类型使用者的使用方便性。
  26. 如果一个包只需要见到另一个包的一部分,可以考虑将后一个包分割成两个互相可见的包。
  27. 子类型化的本质在于可以在超类型对其毫不知情的情况下对其进行扩展。通常需要设计一些子类型来积累经验,然后再进行抽象。

举例两个分析模式

图中的每一个方框代表一个类型。

责任模式

  • 知识层中定义了责任类型,责任类型定义了什么样的委托方可选的责任方有哪些这个映射关系
  • 那么操作层就是实例化一个责任,在具体的责任重选择参与方的时候要遵循知识层的责任类型中的规范
  • 每个责任可能有一个时间段,对应的是一个活动。

观察者模式

  • 每个观察有自己的参与方
  • 观察可以子类型化为假定,推测和有效观察
  • 观察也可以子类型化为测量和分类观察,测量的结果是一个数量(数值+单位),分类观察的结果是是否存在。举个例子,血压是一个测量,而血压高是一个分类观察。血压这个测量可以推测出血压高这个分类观察,而一旦血压的测量是错的,这个推测链应该直接被连锁否定掉。
  • 在知识层,观察有自己的规程,也就是如何进行着观察
  • 知识层的现象类型,现象和观察概念,规定了操作层的观察可以是什么,如血压就是一个现象类型,增加是一个现象,血压增加是一个观察概念,同时观察概念之间是可以互相推测的。