前言

为啥想学习这本书,之前就有同事分享过,但是因为完全听不懂,就没有去学。但是因为在准备晋升的ppt时,看到其他同事写的ppt,就发现区别太大了,他是站在更高的视野更高的角度来思考项目、思考业务、思考软件实现的。而我在写我的部分时,也越来越发现,如何定义自己做的项目,如何把三维世界的对象、问题、事件转化成代码,如何更好地描述问题,我一直都没有答案,期望能有一些对于真实问题建模的理论支撑.
一直思考什么样的代码算是好的代码,好的设计,到现在,入行马上两年,一些个人的想法,好的设计,1.首先要在功能上能满足业务需求,2是性能,能覆盖足够的异常场景,合理的时延等,3是可扩展性。可扩展性就意味着在设计时,需要面向未来,全面地了解业务,了解问题。而对本书的期待,就是能获取第三个点的一些答案。
该书分为四个部分
1. 让领域模型发挥作用:领域驱动开发的基本目标&术语定义
2. 模型驱动设计的构造块:消除模型和实际运行的软件之间的鸿沟
3. 通过重构来加深理解:有价值的模型,是在迭代重构中产生的
4. 战略设计:作为一个整体应用于系统的三个原则:上下文、提炼和大规模结构

第一部分让领域模型发挥作用

模型:对现实的解释,一种知识形式,对知识进行有选择的简化和有目的的结构化,是经过严格组织并精心选择的抽象知识。

1.消化知识

通过头脑风暴的方式和领域专家进行领域知识的学习,将现有信息转化成类图,在讲述自己已知的点和领域专家指出的问题的过程中修改、重构类图,使其能更正确地描述问题。
建模不仅仅需要对实体进行建模,还需要对规则、行为进行建模,因为他们也是领域的中心。
举例:策略模式
将业务规则没有定义地写在主流程中,其他非开发人员无法理解,也没有站在更高的层面去理解这段代码。
抽象出策略类,包含重要的业务规则。

2.语言的交流和使用

领域模型:软件项目的公共语言的核心,是人们头脑中形成的与项目有关的概念的集合,用术语和关系反映了领域的深层含义,为领域量身剪裁的,十分精确。
通用语言:项目需要使用通用的,共享的团队语言。如果不使用通用语言,领域专家使用自己的语言,而技术团队使用自己的语言来从设计角度讨论领域。翻译成本高,即使有深刻的理解,也无法记录到文档中。
通用语言应该包含类名称和主要操作,有些术语用来讨论模型中已经明确的规则,还有一些术语则来自于施加于模型上的高级组织原则。团队一致应用于领域模型的名使该语言更丰富。
团队在交流活动和代码中坚持使用这种语言,并在讨论中使用模型的元素,以及模型中各元素的交互来大声的描述场景,将各种概念结合到一起,找到更简单的表达方式来讲出要将的话,应用于图和代码中。
需要让你设计的模型被领域专家理解,并判断是否合理。
讨论时,图可以帮助表达和解释模型,文档可以解释模型的概念,帮助在代码的细节中指引方向。

3.绑定模型和实现

模型驱动设计:模型需要在分析和程序设计阶段都能发挥良好的作用。如果一个模型不能忠实地描述领域的关键概念,或者对于程序猿来说不太实用时,必须要重新设计。程序代码就是模型的表达,在修改代码时候需要有清晰的认知,修改代码就是在修改模型。
为什么Net是个抽象类。
因为Net分为普通的Net和总线,普通Net会有自己的布线规则,比如说从哪个组件的哪个引脚,线的宽度(最小最大)是啥。
总线:总线连接更多的组件和引脚,和普通net相比可能会需要有更宽的线,起始组件以及承载的功能也会不一样。
但是他们都有自己的规则,规则都为rule类来描述。
普通的net/总线都可以通过assignRule来添加规则,通过assignedRules()来获取自己的规则集合


服务定义了net的行为,工具类提供有用的接口。

建模人员参与程序开发:不参与开发的问题
1.传递模型过程中会丢失某些意图
2.模型与程序实现和技术互相影响,不参与无法获取反馈
建模人员参与开发的两个基本要素:模型要支持有效的实现并抽象出关键的领域知识(重复表达)

第二部分模型驱动设计的构造块

介绍一些标准的模式

4.分离领域

分层架构:分层原则是层中任何元素都仅依赖于本层以及其下层元素。

日常开发使用三层架构,表现层、业务逻辑层、持久层,业务逻辑一般是在service里写的。领域驱动设计的关键在于,领域层负责表达业务的概念(model)、业务的状态信息(status)、以及业务规则(service)。领域层=service+dto

不同层的设计原则,具有内聚性且只依赖于其下层,与其上层保持松散的连接, 上层可以保持对下层的引用,而下层想和上层通信则需要通过其他的方式,比如回调。需要保证在连接时,只关注本层的关注点。
smart ui, “anti-parttern”
需要在用户界面实现所有的业务逻辑,简单的输入以及展示,可以使用smart ui

5.软件中要表示的模型

主要讨论了三种模型:entity,value Object和service。
还讨论了module

5.1关联

对象之间的关联会使建模和实现之间的交互更为复杂
三种方法使关联更易于控制

  • 规定一个遍历方向
  • 添加限定符,减少多重关联
  • 消除不必要的关联


图5.1 领域更关心一个国家有哪些总统,而不关系一个总统是哪个国家的,从双向关联变成领域关心的单向关联
图5.2 一个国家会有多个总统,但是加上限定条件,阶段,由一对多变成一对一。

5.1:将关联限定于领域中所偏向的方向
简化的目的:清除那些对当前工作或对象模型不重要的关联
如果发现了关联的约束,就应该将这些约束添加到模型和实现中,使得模型更精确和易于维护

5.2 模式:ENTITY

定义:不通过其属性定义的,而是通过一连串的连续时间和标识来定义的对象。
实体的基本概念是一种贯穿整个生命周期的抽象的连续型

5.2.1 entity 建模

建模时不要太关注属性,而要关注entity对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为必须要的属性。应该将行为和属性转移到与核心实体关联的其他对象中。除了标识问题之外,实体往往通过协调其对象的操作来完成自己的职责。
选取entity的属性时,关注哪些属性是用来匹配和区分实体的。不同的领域需要的不一样。

设计实体的标识
属性组合
为每个实体附加一个在类中唯一的符号,且该符号不可改变

5.3 模式:value object

很多对象没有概念上的标识,它们描述了一个事务的某种特征。
定义:用于描述领域的某个方面本身,而没有概念标识的对象称为 value object(值对象)。
value object被实例化后,用来表示一些设计元素。我们只关心这些元素是什么,而不关心他们是谁。
举例,地址
在邮购公司中,需要用地址核实信用卡并投递包裹。地址是订单实体的一个属性。关心的领域是订单,是购物,投递是由快递公司来处理的。此时地址是一个value object
而在快递公司,不同的地址,决定了包裹该去往何处,此时地址是一个entity

我们只关心一个模型元素的属性时,应该把它归类为value object。
value object 通常作为参数来在对象之间传递消息。比如日常开发中经常用的,查询参数,各种vo

5.3.1 设计value object

共享和复制
复制:可能导致系统被大量的对象阻塞,但是在分布式系统中,却可以减少相应时间。以空间换时间
共享:一个实例,多处调用,减少空间,但是分布式系统响应时间更长,时间换空间
使用共享的场景

  • 节省数据库空间或者减少对象数量是一个关键要求时
  • 通信开销很低(中央服务器)
  • 共享的对象被严格限定为不可变时

value object的应用
数据库中通过增加副本来降低查询开销,降低响应时间

5.3.2 设计包含value object的关联

value object没有标识,应该尽量完全清除value object直接的双向关联

5.4模式 service

不是事物的对象,由一些操作组成

一个银行应用软件,其中转账相关的,是领域业务知识,它应该被设计到领域层,因为转账是包含复杂的业务规则,而且是能够表征这个领域的。而将交易转换并导出到一个excel中,是应用层的service。“文件格式”在银行领域中是没有意义的,不涉及到业务规则。

将“转账“操作强加在account对象上是很别扭的,因为这个操作涉及两个账户和一些全局规则。
思考:
不同层次的service在命名和结构上会有什么区别?

5.5 模式:Module(也称为package)

module之间是低耦合的,但是在module的内部是高内聚的。
module是一种表达机制,module的选择应该取决于被划分到模块中的对象的意义。如果说模型讲述了一个故事,那么module就是这个故事的各个章节。模块的名称表达了其意义。
选择能够描述系统的module,并且使之包含一个内聚的概念集合。
找到一个可以作为module基础的概念,基于这个概念组织的module可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立的理解和分析这个概念。
module及其名称反映出领域的深层知识。
尽力避免重构module,module的更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具的使用。
一些重构原则:一个类确实依赖于另一个包中的某个类,而且本地module对该module并没有概念上的依赖关系,那么或许应该移除一个类,或者考虑重新组织module。

5.5.2 基础设施驱动的打包存在的隐患

对象的一个最基本的概念是用数据的操作逻辑来封装数据。数据的属性无法直接访问,但是可以通过方法来访问,为什么要这么做呢?这样暴露给外界的都是方法,可以在方法里做一些逻辑。还有呢?
分层以及分层打包,会使得模型变得分散,开发人员难以还原模型原来的样子。
不要在领域模型中添加任何与领域模型所表示的概念没有紧密关系的元素。模型中的设计元素的任务是表示模型。

6.领域对象的生命周期

6.1 模式:aggregate

在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则。而且紧密关联的各组对象也要遵守一些固定规则,然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地相互干扰,从而使系统不可用。

我们需要用一个抽象来封装模型中的引用。aggregate是一组相关对象的集合,作为数据修改的单元。每个aggregate都有一个根(root)和一个边界(boundary)。边界定义了aggregate的内部都有什么。根则是aggregate中所包含的一个特定的entity。在aggregate中,根是唯一允许外部对象保持对他的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他entity都有本地标识。但是这些标识只有在aggregate内部才需要加以区别,因为外部对象除了entity之外看不到其他对象。

思考:为什么要保持这种聚集呢?这样也是在设计的时候,就尽可能的降低和其他模型对象不必要的耦合,定义清楚聚集关系和边界。避免在修改时带来很大的困难。

举例:领域:汽车修配厂的软件,使用的汽车模型
汽车是具有全局标识的entity:在修理时需要知道这辆车的唯一编号
如果我们想知道每个轮胎的里程数和磨损度,我们可能不会关心这些轮胎在这量汽车上线文之外的标志,如果更换了轮胎并将旧轮胎送到回收场,那么软件不需要跟踪它们,没有人会关心他们的转动历史。
汽车修配厂软件的语境,人们只会在数据库中查找汽车,然后临时查看下这部汽车的轮胎情况
汽车是agggregate的根entity,但是轮胎知识处于这个aggregate的边界之内。
思考:我理解就是这些配件,只需要关注他们的型号,而相同型号之间没有区别,不需要感知。但是汽车不一样,汽车有所属人,具体是哪个4s店卖出的。所以必须要关注到即使是不同型号的不同个体之间的差异,这些通过entity标识来区分。

固定规则:在数据变化时候必须保持不变的一致性规则

  1. 根entity具有全局标识,负责检查固定规则
  2. 根entity具有全局标识,边界内的entity具有本地标识,这些标识只有在aggregate内部才是唯一的
  3. aggregate外部的对象不能引用除根entity之外的任何内部对象,根entity可以把对内部的entity的引用传递给他们。但是这些对象只能临时使用这些引用,而不能保持引用。
  4. 只有aggregate的根才能直接通过数据库查询获取,所有其他对象必须通过关联的遍历才能找到
  5. aggregate内部的对象可以保持对其他aggregate根的引用
  6. 删除操作必须一次删除aggregate边界之内的所有对象(由于第三条)
  7. 提交对aggregate边界内部的任何对象的修改时,整个aggregate中的所有固定规则都必须被满足

    采购订单系统

    这个简单的模型会存在以下的问题
    如果只使用行锁,使用者无法感知到全局的变化,导致破坏业务规则。但是如果使用表锁,会导致一直需要等待。

    采购订单(po)
    项目(line item)

可以在模型中加入以下业务知识

  1. part在很多po中使用(会产生高竞争)
  2. 对于part的修改小于对po的修改
  3. 对price的修改不一定会传播到现有的po,它取决于修改价格时po处于什么状态

其中第三点表示,对price的修改有可能会因为业务规则限制,导致price修改失败。
part只表示价格,而项目item中会有quantiy和price,数量和价格。
思考:还是没有明白为啥要这样设计

6.2 模式:factory

在创建一个对象或者创建整个aggregate时,如果创建工作很复杂,或者暴露了过多的内部结构,可以使用factory进行封装。
复杂对象的创建过程不应该交给客户端,这样客户必须要要知道对象内部结构的一些知识。
对象的创建本身可以是一个主要操作,但是被创建的对象并不适合承担复杂的装备操作。让客户端负责创建会使客户端的设计陷入混乱,而且破坏被装配对象或者aggregate的封装,导致客户与被创建对象的实现之间产生过于紧密的耦合。
增加模型,和以往的模型不同,不对应于模型中的任何事物,但是确承担领域层的部分职责。
factory:负责创建其他对象的程序元素。
应该将创建复杂对象的实例和聚合的指责转移给一个单独的对象,它是领域设计中的一部分,提供一个封装所有复杂装配操作的接口,而且这个接口应该不需要客户引用被实例化的对象的具体类,在创建aggregate时要把它作为一个整体,并确保它满足固定规则。
工厂设计方式:factory method、abstract fatcory、builder
好的工厂需要满足以下两个基本需求

  • 每个创建方法都是原子方法,在无法正确地创建出这个对象时,应该抛出异常,或者调用某种其他机制,以确保不会返回错误的值
  • fatcory应该被抽象为所需要的类型,而不是创建出具体的类。

6.2.1 选择factory及其应用位置

这一节主要讨论,factory用在哪些需要隐藏细节的地方。这些决策通常与aggregate有关。
前者使用后者的对象,在后者的对象上创建一个factory method


通过这个账户创建了一个交易订单
brokerage account 佣金账号
factory可以避免客户与具体类之间的耦合。
trade order不在aggregate中,但是创建它需要用到brokerage account中的一些信息,所以由brokerage account创建
规则:把访问限制在aggregate内部,并确保从aggregate外部只能对aggregate临时引用
思考:临时引用是啥意思?

6.2.2只需要使用构造函数的情况

原因:factory可能会使一些不具有多态性的简单对象复杂化
以下情况最好使用简单的、公共的构造函数

  • 类是一种类型,不是任何层次结构的一部分,没有通过接口实现多态性
  • 客户关心的是实现,可能是将其作为选择strategy的一种方式(啥意思)
  • 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建
  • 构造并不复杂
  • 原子操作,且满足被创建对象的所有固定规则
    不要在构造函数中调用其他构造函数,构造函数应该保证绝对的简单
    举例:java的集合类库,

7 使用语言:一个扩展的示例

前面介绍的模型,示例中一次只应用一种模式,但在实际的项目中,必须将他们结合起来使用。

7.1 货物运输系统简介

假设我们需要为一家货运公司开发一个新的软件,最初的需求包括3项基本功能
1.跟踪客户货物的主要处理部署(什么意思?)
2.事先预约货物
3.当货物到达其处理过程中的某个位置时,自动向客户寄送发票

如果是我来设计,最开始我应该只会设计出:Cargo、Customer、Handing Event和Delivery History。下面这些Delivery Specification、Location、Carrier Movement都不会考虑到。为什么作者会设计这些类呢?下面会讲解设计Delivery Specifaction的原因。
使用Delivery Sepecification抽象出来的优点如下:

  1. 如果没有Delivery Specifaction,Cargo对象需要负责提供用于指定运送目标的所有属性和关联的详细意义,Cargo对象会变得混乱,导致难以理解或修改
  2. 当模型作为一个整体来解释时,这个抽象会让我们轻松且安全地省略掉细节。不必关注内部实现,只用知道有这个么个模型就好了
  3. 有更强的表现力,Delivery Specifaction清楚地说明了Cargo运送的具体方式,没有明确规定,但是必须完成Delivery Specifaction中规定的目标

customer在运输中所承担的部分,是按照角色来划分的,比如 shipper、receiver、payer等等。customer和cargo是多对一的关系。这个地方理解有点障碍,一个cargo(货物)应该会有多个customer才对。的确是这样,看上面的定义。

Carrier movement表示由某个Carrier执行的,从一个location到另一个location的旅程。Cargo被装上Carrier后,通过Carrier的一个或者多个Carrier Movement,就可以在不同的地点之间转移。

Delivery History 反映了Cargo实际上发生的事情。

一般情况下,模型的精化、设计和实现,应该在迭代开发过程中一起进行,本章中,是从一个相对成熟的模型开始,视线中采用构造块模式,一切修改完去由需求来驱动。

7.2隔离领域:应用程序的引入

目的:为了防止领域的职责与系统的其它部分的职责混杂在一起,应用layered architecture把领域层划分出来

比较直接可以看到的三个用户层的应用程序功能

  1. tracking query,访问某个Cargo过去和现在的处理情况
  2. Booking application,允许注册一个新的Cargo,并使系统准备好处理它
  3. Incident Logging Application,记录对Cargo的每次处理

这句话有点不太理解是啥含义:应用层是协调者,只是负责提问,不负责回答,回答是领域层的工作。为啥只是提问?不是回答?意思是不是说,应用层只是告知,需要做什么,实际的执行是领域层的事情?

7.3 将 entity 和value object区别开

对每个对象进行考虑,考虑这个对象是必须被跟踪的实体还是仅表示一个基本值。

customer

customer表示一个人或者一个公司,应该是被标识以及被跟踪的,即两个不同的customer之间是需要进行区分的。使用公司提供的id进行唯一标识

cargo

两个完全相同的cargo也是需要区分开的,就像超市里卖的方便面,即使两包一样的,每一包上面都需要有自己的编号,用来在付款的时候扫码&出库。所以cargo(货物)也是一个entity,需要标识,使用公司给分配的id。

handling event 和 carrier movement

这两个独立的事件是它们可以跟踪正在发生的事情。反映了真实的事件,这些事件是不能互换的。因此他们是entity。每个carrier movement可以通过一个代码来识别,来源于运输调度表。(每一次调度就是一个carrier movement,可以获得一个调度id)

handing event 可以通过 cargo id 创建时间和事件类型来标识。handling event需要记录下来,某个货物在某个时间,做了某些事情。两个handing event之间是不同的,那它需要标识么?

Delivery History

delivery history是不能互换的,每一个cargo都会有自己的delivery history,但是,有可能有多个啊。为啥书里写的是标识是从cargo那里借来的,这里可以留一个todo

Delivery Specification

表示了cargo的目标,但这种抽象并不依赖于cargo。实际上表示某些delivery history的假定状态。如果有两个cargo去往同一个地点,它们可以用同一个delivery specification。因此delivery specification 是一个value object。

role和其他属性

role表示了有关他所限定的关联的一些信息,但是它没有历史或者连续性,因此它是一个object。可以在不同的cargo/customer关联中共享它。

其他属性(时间戳/名称)都是value object。

7.4 设计运输系统中的关联

双向关联在设计中是最容易产生问题的。只有深刻地理解领域后,才能确定遍历方向,理解遍历方向能够使模型更深入。

  1. customer不直接与cargo关联,而是通过对customer进行角色的划分来与cargo进行关联。
  2. 如果我们的要求是对一系列货船进行跟踪,那我们需要carrier movement到handling event的关联,但是我们的业务只需要跟踪cargo。只需要从handling event遍历到carrier movement就能满足我们的需求。
  3. 有一个循环引用。cargo知道他的delivery history,delivery history中保存了一系列的handling event,而handling event反过来又指向cargo。

在Delivery History中提供一个List对象,并把Handling Event都放到这个List对象中。

7.5 aggregate边界

Customer、Location和Carrier Movement都有自己的标识,且被许多Cargo共享,它们在各自的aggregate 中都是根,这些聚合除了包含他们的属性外,可能还包含其他比这里讨论的细节更底层的对象。

Cargo也是一个明显的Aggregate根。

Cargo可以把一切只因Cargo存在的事物包含进来,如Delivery History,没人会在不知道Cargo的情况下直接查询Delivery History。而Handling event是和Delivery History关联的,所以Handling event也应该包含在Cargo聚合内。Delivery Specifatcion是一个Value Object,它应该也包含在Aggregate中。

但是单独把Handling event抽出来看,查找装货和准备某次Carrier movement时所进行的所有操作,存在脱离Cargo的操作,所以Handling event也是一个aggregate的根。

7.6选择Repository

在我们的设计中,有5个entity是Aggregate的根。因此在选择存储库时只需要考虑这个五个实体。其他对象都不能有Repository。

为什么handling event需要在一个“低争用”的事务中创建?

考虑实际场景

预订订单时

  1. 通过Booking Application进行预订,用户需要选择一些承担不同的角色(托运人和收货人)的Customer,因此需要一个Customer Repository。指定货物的目的地时还需要一个Location,因此还需要创建一个Location Repository。
  2. 用户需要查看物流进度,通过Activity logging application来查找装货的Carrier movement。需要一个Carrier Movement repository。(这里为什么不是Delivery history?Carrier Movement更像是交通工具的移动信息,Delivery history是针对于cargo的么,那我觉得也应该是delivery history呀?Delivery history的定义是什么?运输历史,具体cargo执行了哪些动作么?比如装货卸货?如果能看到handling event的枚举就好了)
  3. 用户需要系统告知哪个Cargo已经完成了装货。

7.7 场景走查

需要经常走查场景,以确保能够有效地解决应用问题&复核这些决策

7.7.1应用程序特性举例:更改Cargo的目的地

会有这样的场景,customer打电话说需要更改货物的目的地。Delivery Specifaction是一个value object,创建一个新的,再使用Cargo上的setter方法把旧值替换成新值。

7.7.2 应用程序特性举例:重复业务

相同的customer的重复预订,往往是类似的。因此他们想要将旧的Cargo作为新的Cargo的原型。应用程序可以允许用户在存储库中查找一个Cargo(看上去像是历史订单的功能),然后选择一条命令来基于选中的Cargo来创建一个新的Cargo。

Cargo是一个entity,而且是Aggregate的根,因此在复制它的时候要非常小心,其边界里的每个对象或者是属性的处理都需要仔细考虑。(此时就能体现出Aggregate的好处了,只要根变动,其他aggregate内的实体和值对象都需要变更!)

  • Delivery History:应该创建一个新的、空的Delivery History。原有的Delivery History并不适用
  • Customer Roles:应该复制Map,map里保存了对customer的引用。复制引用,而不是customer本身。有点没get到。复制后,应该保证和原来的Cargo引用相同的一些Customer对象。它们是Aggregate边界之外的Entity。
  • Tracking ID:必须提供一个新的Tracking ID,在创建新的Cargo时创建。

复制了Cargo Aggregate边界内部的所有对象,并对副本进行了一些修改,但是并没有对边界之外的对象产生任何影响。啊 聚合&边界的好处~~~

7.8 对象的创建

7.8.1 cargo的factory和构造函数

可以在Cargo上创建一个Factory方法

public Cargo copyPrototype(String newTrackingID)

或者可以为一个独立的Factory添加以下方法

public Cargo newCargo(Cargo prototype, String newTrackingID)

也可以把获取id(自动生成)的过程封装起来,只需要一个参数

public Cargo newCargo(Cargo protoType)

这些Factory返回的结果是完全相同的,都是一个Cargo,其Delivery History为空,且Delivery Specification为null

但是构造函数也是必要的。

从上面对aggregate的分析,分析实体/值对象之间的关联关系,可以得知。Cargo和Delivery history是互相关联的。它们必须要互相指向对方,才算是完整的,因此他们必须被一起创建。可以用Cargo的构造函数或者Factory来创建Delivery History。Delivery History构造函数将把Cargo作为参数。

public Cargo(String id) {trackingID = id;deliveryHistory = new DeliveryHistory(this); //在创建Cargo时就创建DeliveryHistorycustomerRoles = new HashMap();
}

得到一个新的Cargo,带有一个指向它自己的新的Delivery History。

7.8.2 添加一个Handing Event

货物在真实世界中每次的处理,都会有人使用Incident Logging Application来输入一条Handing Event记录。

Handing Event是一个Entity,需要把定义其标识的所有属性传递给构造函数。定义其标识的所有属性,指的是能够唯一标识它的。前面了解到的,Handing Event是通过Cargo的ID、完成时间和事件类型来唯一标识的。Handing Event唯一剩下的属性是与Carrier Movement的关联

public HandingEvent(Cargo c,String eventType,Date timeStamp) {handled = c;type = eventType;completionTime = timeStamp;
}

在entity中,那些不起到标识作用的属性,通常可以过后再添加。Handing event的所有的属性都是在初始事务中设置的,而且过后不再改变,为每个事件类型的Handing Event添加一个简单的Factory method,会很方便,且会使客户代码具有更强的表达能力。loading event确实涉及一个Carrier Movement。

public static handlingEvent newLoading(Cargo c,CarrierMovement loadedOnto, Date timeStamp) {Handling result = new HandlingEvent(c, LOADING_EVENT, timeStamp);result.setCarrierMovement(loadedOnto); //设置CarrierMovementreturn result;
}

可以把反向指针的创建封装到Factory中,并将其放在领域层中。但是可以看另一种设计,完全消除了这种别扭的设计。

好奇怪,应该是通过Cargo找到其对应的Delivery history,然后把handling event塞到Delivery history里面。

7.9 停下来重构,Cargo aggregate的另一种设计

由于在添加Handling Event时需要更新Delivery History,在这个事务中会涉及到Cargo Aggregate。如果在同一事件其他用户正在修改Cargo,那么Handling event事务将会失败/延迟。

这一个小节也是为了解,cargo -> delivery history -> handling event -> cargo这个循环引用的问题。

将handling event持久化,在创建时,入参包含cargo id。这样就可以实现handling event到cargo的引用了。上一个小节考虑的是,通过cargo查到对应的history,再在这个history的handling event map里添加。这也太奇怪了,怎么会有这种思路呢?这种做法就涉及到一个聚合外部的模型,正在试图修改聚合内的模型。

如果使用将handling event持久化的思路,可以在创建的时候进行查找,没有就进行创建,并且持久化。用户在使用应用程序的时候,如果想添加一条handling event,那他一定首先是知道对应的Cargo是啥的。

7.10 运输模型中的Module

前面的讲不太好的模型,没看太懂是啥意思,只学习下这个更好的领域模型吧。

先解释下uml图:其中空心菱形标识聚合,比如contact和customer就表示,一个customer has a contact。一个用户有多个合同。

实线&箭头,表示关联,一个类知道另一类的方法和属性。其中customer agreement 关联route specification。用户协议是和specification关联的。

通过现有的模块划分,可以很好的描述现实世界

公司给customer shipping(customer模块和shipping模块的是有关联关系的)。因此向他们寄出bill(billing和customer也有关联关系)。公司的销售和营销人员,与customer协商并签署协议,因此协议是两者之间的桥梁。操作人员负责将货物shipping到指定的目的地,后勤办公人员负责billing(处理账单),并根据Customer协议开具发票。这里理解,后勤办公人员和操作人员都是customer的一个对象。

7.11 引入新特性:配额检查

现在完成了初始的需求和模型,要添加第一批重要的新功能。

公司需要为管理效益,制定销售计划。需要根据货物类型、出发地和目的地,或者任何可以作为分类名输入的其他因素来制定不同类型货物的运输配额。这些配额构成了各类货物的运输量目标。这样利润较低的货物就不会占满配额而导致无法运输利润较高的货物。同时避免预订量不足或者过量预订。

配额检查需要检查cargo repository以及去销售管理系统里拿到销售信息。

7.11.1 连接两个系统

销售管理系统不是根据这里所使用的模型编写的。如果booking application和它直接交互,我们的应用程序就需要兼容另一个系统的设计。我们创建另一个类,让它充当我们的模型系统和销售管理系统之间的翻译,只对我们应用程序所需要的特性进行翻译,并根据我们的领域模型重新对这些特性进行抽象。这个类作为一个anticorrpution layer。会在14章进行讨论

7.11.2 进一步完善模型:划分业务

我们需要定义cargo的类型,来使模型可以支持配额的获取。

分析模式可以为建模提供思路,通过enterprise segment(企业部门单元) 来划分。


Allocation Checker将充当enterprise segment和外部系统的类别名称之间的翻译。

Cargo Repository还必须提供一种基于Enterprise Segment的查询,这个是必要的,因为要查询各个部门的配额。

如果只是当前的这个模型,booking application 将会需要对enterprise segment的配额和已经预定的数量和新cargo数量的和去做比较,这个业务逻辑会耦合在应用层,但是其实它应该是领域层的东西。

也没有清楚地表明booking application是如何得出enterprise segment

这两个职责都是属于allocation checker。可以通过修改接口,将这两个服务分离出来。


图里的顺序的确是对的。1.获取enterprise segment 2、获取已经预定的cargo的数量,需要去cargo repository 里进行查询。3.进行比较

另外值得注意的是sales management system的逻辑是封装在了allocation checker里的,这也就意味着booking application是不需要感知sales management system的逻辑的,一切只需要和allocation checker去交互。同样sales management system也是。

能够预测到的方法是allocation checker可以拿到enterprise segment(这个好奇怪,这个值不应该在sales management system里,而应该在其他领域里,是不是因为销售系统本来就应该有每个部门的配额?这也是销售计划的一部分?),并且封装是否能够预订的逻辑。

allocation checker可以被看作是一个 facade(外观模式https://www.runoob.com/design-pattern/facade-pattern.html)

7.11.3 性能优化

考虑通信开销,可以把enterprise segment 缓存到服务器中,但是被缓存的数据必须保持最新。

7.12 小结

有可能会对其中的一个设计产生疑惑,那就是为什么不把enterprise segment的划分职责分给Cargo呢?如果enterprise segment的所有数据都来源于Cargo,那么这样的设计看上去是一个不错的选择,但是出于不同目的,可能需要对相同的entity进行不同的划分。当以税务会计的角度或者是当销售策略发生变化时,配额的enterprise segment的划分都可能发生变化。因此Cargo必须知道allocation checker。但是这完全不在其职责范围内。

正确的设计是让知道这些规则的对象来承担获取这个值的指责,而不是把这个职责施加给包含具体数据的对象。这些对象可以被分离到一个独立的strategy对象中。然后将这个对象传递给cargo。

第8章 突破

通过一系列的修改可以得到更符合现实且更符合用户那些最重要的需求的模型。模型变得简单了,其功能性及说明性却增强了。

8.1一个突破的故事

背景:作者在给一家投资银行开发一个大型应用程序的核心部分,该程序用于管理银团贷款。

解释银团:假设intel需要建造一座价值10亿的工厂,就需要申请巨额贷款,但任何一家借贷公司都无法独立承担,于是这些公司就组成银团。集中资源来支持这种巨额信贷。投资银行通常在银团里担当领导者的角色。负责协调各种交易和其他服务。作者的项目就是需要开发这样一个用于跟踪和支持以上整个过程的软件。

8.1 华而不实的模型

其中这个领域其实就主要有三个角色,银团作为中间商,接受各大银行的投资,并且放贷给需要的人。inverstment的属性有,投资者(各个公司(银行)),投资比例(double类型)。loan的属性有money,表示实际贷款金额?increase和decrease方法都可以更改它amount的值。facility的属性是limit,limit表示额度。实际生活的例子,信用卡额度3w,facility的limit是3w,但是你刷信用卡1w,其中loan就是1w。

loan和facility都可以表示贷款

facility更侧重于表示额度

loan表示贷款金额?

没get到loan investment是干啥的

但是这个模型在出现一些其他但是重要的需求时,无法满足

其中一种需求:在提取贷款时(我理解是实际放贷过程),信贷股份仅仅是房贷方投入金额指导原则,借款者要求提取贷款时,银团领导者会通知所有成员支付自己的股份。

收到通知后,投资者一般会按照自己的股份来支付,但是有时也会与其他银团成员协商。以求少投入些(或多投入些),因此在模型里添加了loan adjustment以反映这个事实。

8.1.2 突破

实际业务场景,loan和facility的股份可以在互不影响的前提下独立发生

解释:facility的总额是1亿美元,而借款者从中提取的第一笔loan金额是5000万美元,且3个放贷方按照各自原先承诺的facility股份来支付。
此时贷款者又贷了3000w,但是这次b公司不参与,由a额外承担剩余的股份
当借款者还钱时候,是按照实际loan的股份来进行偿还的。利息也是按照loan股份来进行分配的

但是借款者为享有facility权而支付费用时,比如信用卡的年费,这笔钱是按照facility来支付的。

这样带来的疑问是,不同的银行给了借款者不同的贷款额度,但是最后却不是按照这个额度来实际出资的,这样不会有影响吗?有点奇怪。

8.1.3 更深层模型

加深理解:投资和loan投资是“股份”这个常规基础概念的两种特例。信贷股份、贷款股份、支付比例股份,这些都是股份,股份无处不在。

share pie:股份份额

prorate:按比例分配

transfer:公司之间份额的转移

share和share pie之间是聚合关系,一个share pie有多个share。一个股份份额会有多个股份。

share类里包含,owner,返回值是company,amount,金额,返回值是有小数的

Percent pie 百分比继承share pie,加起来是1

amount pie也继承share pie,plus是增加,minus是减少。返回值都是amount pie。

这个继承关系没有特别搞懂。为啥有这个继承?直接把minus和plus放在share pie里不行吗?

有点明白了,percent pie指的是facility的股份份额

amount pie指的是实际loan的股份份额

与股份模型搭配的新贷款模型

8.1.4 冷静决策

讲他们老大为他们顶住压力,做正确的事情

8.1.5 成果

作者讲第一个版本交付后,他的神经衰弱也有了好转,原来不止我一个人会因为工作神经衰弱

8.3 关注根本

不要试图去制造突破,那样只会让项目陷入困境。通常只有在实现了适度重构后才有可能出现突破

8.4 越来越多的新理解

开发过程中发现,提取货款、缴纳费用等业务是由一些重要的规则控制的。这些逻辑都分散在了facility和loan的各种方法中。经常在讨论中出现的术语,比如“交易”,代表一次金融交易,没有体现在模型中,反而隐藏在了复杂的方法里。

Position:仓位

share pie:股份份额和position之间是聚合关系,position有share pie

facility和loan竟然是继承了position

transaction:交易

其中 facility investment、facility trade、drawdown、interest payment、fee payment、principal payment都是继承自transation

其中drawdown是提取借款的意思,那为啥模型里写的是由facility.sharePie按比例分配的股份,可修改呢?执行position.sharePie.plus(sharePie),增加facility的比例。不理解,facility是不应该由提取借款来有所变更的,只有loan应该会随着drawdown来变化。

9.将隐式概念转变为显式概念

深层建模的第一步,就是要设法在模型中表达出领域的基本概念,随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。

需要实现对领域的底层模型的挖掘,第一步首先是需要是被某种形式存在的隐含的概念,无论这些概念有多么原始。

9.1 概念挖掘

要挖掘出大部分的隐含概念,需要开发人员去倾听团队语言,仔细检查设计中的不足之处,以及与专家观点矛盾的地方,研究领域相关的文献,并且进行大量的实验。

9.1.1 倾听语言

不同于原来“名词即对象”的概念,听到新单词只是个开头,我们还要进行对话,消化知识,这样才能挖掘出清晰实用的概念。

如果领域专家/软件设计者都在使用不在通用语言里的词汇,软件设计者需要警惕,那说明我们的模型还有需要改进的部分。思考、行动、改进。

示例:

团队已经开发出了可用来预定货物的有效应用程序,现在他们开始开发“作业支持”应用程序,此程序可帮助工作人员管理工作单,工作单用于安排起始地和目的地的货物装卸以及在不同货轮之间的运转。

现状:预订程序使用一个路线引擎来安排货物行程。运输过程的每段行程都作为一行数据存储在数据表中,指定了装载该货物的航次(某一货轮的某一航次)ID、装货地点以及卸货地点。
明确工单的作用:这些工单用于安排起始地和目的地的货物装卸以及在不同货轮之间的运转。

所以工作人员是需要,什么时间、在什么地点、有什么货物需要装上或者卸载的。

有点没太get到区别,所有的leg其实是都需要落库的哇,这样在工单应用程序里,员工才能知道有哪些航次,起始地和目的地是什么,如何安排人员装货和卸货。
把显式的Itinerary对象作为模型的一部分,带来的益处

  1. 更明确地定义Routing Service接口
  2. 将Routing Service从预定数据库表中分离出来
  3. 明确了预定应用程序和作业支持应用程序之间的关系(即共享Itinerary对象)
  4. 减少重复,因为Itinerary可同时为预定报表和作业支持应用程序提供装货/卸货时间
  5. 从预定报表中删除领域逻辑,并将其移至独立的领域层
  6. 扩充了通用语言,使得开发人员和领域专家之间或者开发人员内部能够更准确地讨论模型和设计

哦~原始的模型里,根本没有航程和航段的模型哇,这样子怎么能work呢?

9.1.2 检查不足之处

有些概念是需要挖掘的,这个时候,应该让领域专家参与到讨论中来。

探索利息计算的模型


金融公司,主要经营商业贷款和其他一些生息资产。公司开发了一个用于跟踪这些投资和收益的应用程序,通过一项一项地添加功能来使它不断的发展。每天晚上,公司会运行一个批处理脚本,用于计算当天所生成的利息和费用,并把它们相应地记录到公司的会计软件中。

accrual:累计,增加

ledger:分类账
夜间脚本会通知每个assert执行calculateAccuralsThroughDate(),其返回值是Accural的集合,而其中每笔金额都会过账到指定的分类账中。

新模型有几个优点

  1. 术语,"应计费用(accrual)"使通用语言更加丰富
  2. 将应计费用从付款中分离出来
  3. 将领域知识(如过账到哪个分类账)从脚本中移出来,并放到领域层
  4. 将费用和利息统一,既能够符合业务逻辑,又能消除重复代码
  5. 把accrual schedule作为费用和利息的一种新的形式简单添加到模型中

accrual schedule是什么东西

Ledger:总账

Daily compound interest: compound 混合物

9.1.3 思考矛盾之处

思考矛盾的地方,如果能达成统一,那就会透过问题领域的表面获得更深层次的理解

9.1.4 查阅书籍

查阅书籍理解一些专业领域的概念

9.1.5 尝试,再尝试

讨论过程中,会尝试六七种不同的思路,找到一个看起来足够清晰且实用的概念,并在模型中尝试它

9.2 如何为那些不太明显的概念建模

面向对象范式会引导我们去寻找和创造特定类型的概念,所有事物(即使像是“应计费用”这种非常抽象的概念)及其操作行为是大部分对象模型的主要部分。

9.2.1 显式的约束

约束是模型概念中非常重要的类别。它们通常是隐式出现的,将他们显式表现出来可以极大的提高设计质量。

将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这种约束。现在这个约束条件就是一个“有名有姓”的概念了。可以用这个名字来讨论它。

下面这些信号,表明约束的存在正在扰乱它的“宿主对象”的设计

  1. 计算约束所需要的数据从定义上看并不属于这个对象
  2. 相关规则在多个对象中出现,造成了代码重复或者导致不属于同一族的对象之间产生了继承关系。
  3. 很多设计和需求讨论是围绕这些约束进行的,而在代码实现中,它们却隐藏在过程代码中。

如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出,但是在模型中却不明显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合。

思考:约束/策略,也可以成为一个类,来显式的处理

9.2.2 作为领域对象的过程

过程应该被显式地表达出来,还是应该被隐藏起来,区分方法很简单,它是经常被领域专家提起,还是仅仅被当作计算机程序机制的一部分?

约束和过程是模型概念中,应用范围很广的概念,当我们使用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。

specification这个概念看起来很简单,但是应用和实现起来却很微妙,因此在本节中会有大量的细节描述。

9.2.3 模式:specification

常见的场景,返回值为bool的方法,比如在同一个Invoice(发票)类中,还有另外一个规则anInvoice.isDelinquent(); delinquent:拖欠债务的。它已开始也是用来检查Invoice是否过期,但是仅仅是开始部分,根据客户账户状态的不同,可能会有宽限期政策。一些拖欠票据正准备再一次发出催款通知,另一些则准备发给收账公司。invoice作为付款请求是明白无误的,但是它很快就会消失在大量杂乱的规则计算代码中。Invoice还会发展出对领域类和子系统的各种以来,而这些领域类却和invoice的基本含义无关。

使用逻辑编程范式的开发人员会用一种不同的方式来处理这种情况。这种规则被称为“谓词”,奇怪的词汇,没有get到它的含义,谓词是指计算结果为“真”/“假”的函数,并且可以使用操作符(and 和 or)把它们连接起来以表达更复杂的规则。通过谓词,我们可以显式地声明规则,并在invoice中使用这些规则,前提是必须使用逻辑范式。

尝试使用对象来实现逻辑

业务规则通常不适合作为entity或者value object 的职责。而且规则的变化和组合也会掩盖领域对象的基本含义,但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。

那应该怎么办呢???

逻辑编程提供了一种概念,即“谓词”这种可分离,可组合的规则对象,但是要把这种概念完全用对象实现是很麻烦的。同时,这种概念也非常笼统。在表达设计意图方面,它的针对性不如设计那么好。

可以借用谓词概念来创建,可计算布尔值的特殊对象。那些难于控制的测试方法,可以巧妙的扩展出自己的对象。它们都是一些小的事实测试,可以提取到一个单独的value object中。没get到这句话是啥意思。是事实测试,就可以构建一个value object,然后把这些判断都收口到value object中嘛?而这个新对象则可以用来计算另一个对象,看看谓词对那个对象的计算,是否为“真”。

把原本嵌入在invoice类中的判断规则,抽出成一个规则类,其中的判断方法,入参则是invoice。

这个新对象就是一个规格,规格中声明的是限制另一个对象状态的约束。规格中声明是的限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。specification有多种用途,其中一种用途体现了最基本的概念,这种用途就是:specification可以测试任何对象以检验它们是否满足制定的标准。

为特殊目的创建谓形式的显式的value object。speicification就是一个谓词,可以用来确定对象是否满足某些标准。

规则很复杂时时,可以扩展这种概念,对简单的规格进行组合,就像用逻辑运算符把多个谓词组合起来一样。基本模式不变,并且提供了一种从简单模型过渡到复杂模型的途径。

specification将规则保留在来了领域层,由于规则是一个完备的对象,所以这种设计能够更加清晰地反映模型。利用工厂,可以用来自其他资源的信息,对规格进行配置。之所以使用factory,是为了避免invoice直接访问这些资源,因为这样会使得invoice和这些资源发生不正确的关联,invoice的职责和这些资源无关。

该例子中,可以创建delinquent invoice specification 拖欠发票规格,来对一些发票进行评估,这个规格用过后,就丢弃掉了,这样只需要指定评估日期就可以了。

9.2.4 specification 的应用和实现


specification最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下三个目的中的至少一个目的,我们可能需要来指定对象的状态

  1. 验证对象,验证它是否满足某些需求或者是否已经为实现某个目标做好了准备
  2. 从集合中选择一个对象(查询过期发票)这不就是验证吗?
  3. 指定在创建新对象时,必须满足某种需求。

这三种用法,我认为是三种场景(验证,选择和根据要求来创建)从概念层面上来讲是相通的,如果没有如specification这样的模式,相同的规则可能会表现为不同的形式,有可能会相互矛盾的形式。大概有点get到了,如果不封装在一个类里,有可能不同的应用场景,都会出现这个规则,但是不同的场景,使用的规则表现形式还不一样。这样就会丧失概念上的统一性,通过应用specification模式,我们可以使用一致性的模型。

//继承了InvoiceSpecification 类
class DelinquentInvoiceSepecification extends InvoiceSpecification {private Date currentDate;public DelinquentInvoiceSpecification(Date currentDate) {this.currentDate = currentDate;}//判断是否是拖欠债务的public boolean isSatisfiedBy(Invoice candidate) {int grecePeriod = candidate.customer().getPaymentGracePeriod();Date firmDealine = DateUtility.addDaysToDate(candidate.dueDate(), gracePeriod);return currentDate.after(firmDeadline);}}

需要判断时,实例化一个DelinquentInvoiceSepecification类,用这个类的isSatisfiedBy方法来判断。

选择(或查询)

验证是对一个独立的对象进行测试,检查它是否满足某些标准,

sql置于repository中,而使用哪个查询则由specification来控制,规格中并没有定义完整的规则,但是包含了specification 的基本声明,指明了什么条件构成拖欠。

现在,repository中包含的查询非常具有针对性,可能只适用于这种情况。虽然这可以接受,但是根据拖欠发票在过期发票中所占数量的不同,我们可以选择一种更通用的repository解决方案。

有点没看懂这段代码,本来在sql查询的时候,就加了条件,但是为森么在规格类里,还是会遍历&判断呢?

又看了一遍,看上去是把sql查询方法变得更加通用了,但是还是没有太get到。

示例:

仓库包含各种各样的化学品,目标是编写出一个软件,寻找一种可靠安全而高效地在容器中放置化品的方式。
可以从验证问题开始着手,这种方式让我们必须显式地描述规则,同时也提供了一种测试最终实现的方式

每一种化学品都有一个容器specification

将这些规格编写成container specification,就可以提出一种把化学品混装在容器中的配置方式。并测试它是否满足这些约束条件。

Container specification中的方法isSatisfied()用来检查是否满足所需要的containerFeature。

每个化学品都设置一个自己容器

tnt.setContainerSpecification(new ContainerSpecification(ARMORED));

Container对象中的方法isSafelyPacked()用来保证Container具有Chemical要求的所有特性。

可以编写一个监控程序,来监视库存数据库并报告不安全的状况。

还是没太get到。
下面是设置易爆化学品的客户端示例代码

tnt.setContainerSpecification(new ContainerSpecification(ARMORED))

Container 对象中的方法isSafelyPacked()用来保证Container具有Chemical要求的所有的特性

//检查容器中的所有的化学品 是否都能被安全的装在他的容器里?
//没有uml图好难理解啊
boolean isSafelyPacked() {Iterator it = contents.iterator();while(it.hasNext()) {Drum drum = (Drum) it.next();if (!drum.containerSpecification().isSatisfiedBy(this)) {return false;}}return true;
}

监控程序,监视库存数据库并报告不安全状况。

Iterator it = containers.iterator();
while(it.hasNext()) {Container container = (Container)it.next();if (!container.isSafelyPacked()) {unsafeContainers.add(container);}
}

打包程序:这个服务可以接受Drum和Container集合并将它们按照规则进行打包。

示例:

//里面包含了各种业务规则,将这些业务规则放入了领域层,并为其构建了模型。
public class Container { //Container就是这个容器private double capacity; //容量private Set contents; //Drums public boolean hasSpaceFor(Drum aDrum) {return remainingSpace() >= aDrum.getSize(); //容器空间只会管药物的size么?为什么不管specifictaion的大小?}public double remainingSpace() {double totalContentSize = 0.0;Iterator it = contents.iterator();while (it.hasNext()) {Drum aDrum = (Drum) it.next();totalContentSize = totalContentSize + aDrum.getSize();}return capacity - totalContentSize; }//是否能容纳 提供住宿public boolean canAccommodate(Drum aDrum) {//aDrum.getContainerSpecification就是,把这个规则放入aDrum中return hasSpaceFor(aDrum) && aDrum.getContainerSpecification().isSatisfiedBy(this);}
}
public class PrototypePacker implements WarehousePacker {public void pack(Collection containers, Collection drums) throws NoAnswerFoundException {//为每一个drum寻找对应的container,和最初构想一致。Iterator it = drums.iterator();while(it.hasNext()) {Drum drum. = (Drum) it.next();Container container = findContainerFor(containers, drum);}}public Container findContainerFor(Collection containers, Drum drum) throws NoAnswerFoundException {Iterator it = containers.iterator();while(it.hasNext()) {Container container = (Container)it.next();if (container.canAccommodate(drum))return container;}throw new NoAnswerFoundException();}
}

10 柔性设计

为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要要让人们乐于使用,而且易于作出修改,这就是 柔性设计(supple design)

10.1 模式:intention-revealing interfaces

目的展现接口?

客户开发人员想要有效的使用对象,必须知道对象的一些信息,如果接口没有开发人员这些信息,那么他就必须深入研究对象的内部机制,以便于理解。这样就失去了封装的大部分价值。

如果开发人员,为了使用一个组件,而必须要去研究它的实现,那么就失去了封装的价值。

当我们把概念显式地建模为类或者方法时候,为了真正从中获取价值,必须为这些程序元素赋予一个能够反映他们的概念的名字,类和方法的名称为开发人员之间的沟通创造了很好的机会,也能够改善系统的抽象。

设计中所有公共元素共同构成类接口,每个元素的名称都提供了一次揭示设计意图的机会。类型名称、方法名称和参数名称组合在一起,共同形成了一个intention-revealing interface(释义接口)

命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的,这样可以使客户开发人员不必去理解内部的细节,这些名称应该和通用语言一致。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。

10.2 模式 side Effect-free function

无副作用的功能

副作用这个词暗示着,“意外的结果”,但是在计算机科学中,任何对系统状态产生的影响都叫副作用。为了便于讨论,我们把它的含义缩小一下,任何对未来操作产生影响的系统状态的改变都可以称之为副作用

副作用这个词强调了这种交互的不可避免性。

多个规则或者计算组合的相互作用所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及所有派生操作的实现。

如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就收到了限制

如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。

这就要求开发人员既保持接口的抽象,又能安全地预见结果的抽象。

尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令隔离到不反悔领域信息的,非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到value object中,这样可以进一步控制副作用。

minxIn()方法中

public class pigmentColor{public PigmentColor mixedWith(PigmentColor other, double ratio) {//一些复杂的颜色混合逻辑,使用一个新的颜料对象的创建结束,这个颜料对象的创建使用红、蓝、黄色}
}public class Paint {public void mixIn(Paint other) {volume = volume + other.getVolume();double ratio = other.getVolume()/volume;pigmentColor = pigmentColor.mixedWith(other.pigmentColor, ratio);}
}

函数的计算结果很容易理解,也很容易测试,因此可以安全的使用或者与其他操作进行组合,由于它的安全性很高,因此复杂的调色逻辑真正被封装起来了。

10.3 模式:assertion

10.4 模式:Conceptual contour

模型或者设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复,外部接口无法全部给出客户可能关心的信息。另一方面,把类和方法分解开也不行,这会使客户更复杂。迫使客户对象去理解各个小部分是如何组合在一起的。

粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。

通过反复重构最终会实现柔性设计,随着代码不断适合新理解的概念或需求,conceptual contour也就逐渐形成了。

高内聚低耦合这一对基本原则都起着重要的作用,这两条原则既适用于代码,也适用于概念。在做每个决定时,都要问自己:这是根据当前模型和代码中的一组特定关系作出的权宜之计呢?还是反映了底层领域的某种轮廓。

寻找在概念上有意义的功能单元,这样可以使得设计既灵活又易懂。

把设计元素(操作、接口、类和aggregate)分解为内聚得单元。在这个过程中,你对领域中的一切重要划分的直观认识也要考虑在内。在连续的重构过程中,发生变化和保证稳定的规律性。并寻找能够解释这些变化模式的底层conceptual contour。使模型与领域中的那些一致的方面相匹配。

Accrual schedule:应计时间表

增加新的需求:利息付款和手续费付款实际上使用相同的规则。新模型可以很自然的使用payment类。

payment类里包含:date(日期)、amount(金额)、legerName(贷方姓名)

如果使用原来的模型,两个payment history类之间必然会出现重复(这个难题可能使得开发人员意识到payment类应该被共享,这样就会从另一条途径得到类似的模型),新元素之所以能够很容易就被添加进来了,真正的原因是经过前面的重构,设计能够很好地与领域的基本概念产生吻合。

10.5 模式:standalone class

孤立的类

互相关联使模型和设计都变得难以理解、测试和维护。而且,互相依赖性,很容易越积越多。

每个关联都是一种依赖性,要想理解一个类,必须理解它与哪些对象有联系,与这个类有联系的其他对象还会与更多的对象发生联系。这些联系也必须要弄清楚,每个方法的每个参数的类型也是一个依赖性,每个返回值也都是一个依赖性。

  • 如果有一个依赖关系,我们必须同时考虑两个类以及它们之间的关系的本质。如果某个类以来另外两个类,就必须考虑这三个类中的每一个、这个类与其他两个类之间的相互关系的本质,以及这三个类可能存在的其他相互关系

  • module和aggregate的目的都是为了限制互相依赖的关系网。当我们是别处一个高度内聚的子领域,并把它提取到一个module中的时候,一组对象也随之与系统的其他部分解除联系。这样就可以限制呼吸那个联系的概念的数量。但是即使把系统分成了各个module。如果不严格控制module内部的依赖,也一样会让我们耗费很多精力去考虑依赖关系。

  • 我们应该对每个依赖关系提出质疑,直到证实它确实表示对象的基本概念为止,这个仔细检查依赖关系的过程从提取模型概念本身开始。然后需要注意每个独立的关联和操作,仔细选择模型和设计能够大幅度减少依赖关系—常常能减少到0。(这怎么可能呢??)

  • 低耦合是对象设计的一个基本要素,尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变成完全孤立的了,每个这样的孤立的类都极大的减轻了因理解module而带来的负担。

  • 目的是不是消除所有的依赖,而是消除所有不重要的依赖,当无法消除所有的依赖关系时,每清除一个依赖对开发人员而言都是一种解脱,使它们能够集中精力处理剩下的概念依赖关系。

  • 尽力把最复杂的计算提取到standalone class(孤立的类)中,可能实现此目的一种方法是把具有最紧密联系的类中的所有value object建模出来。

10.6 模式:closure of operation

闭合操作

在适当的情况下,在定义操作时让它的返回类型与其他参数类型相同,如果实现者的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。

开发时,尽量不引入其他类型,增加开发者的理解负担。

10.7 声明式设计

声明式设计:把程序或程序的一部分写成一种可执行的规格(spcification).

使用声明式设计时,软件实际上是由一 些非常精确的属性描述来控制的。声明式设计有多种实现方式。比如,可以通过反射机制来实现,或者在编译时通过代码生成来实现(根据声明来自动生成传统代码),

限制:

  1. 很难扩展到框架之外
  2. 破坏了迭代循环

10.9 切入问题的角度

本章给出了一系列技术,用于澄清代码意图,使得使用代码的后果变得显而易见,并且解除模型元素的耦合。

介绍几种主要的方法,然后给出扩展的示例。显示如何把这些模型结合起来使用,并用于处理更大的设计。

10.9.1 分割子领域

如果模型的某个部分可以被看作是专门的数学,那么可以把这部分分离出来。如果应用程序实施了某些用来限制状态改变的复杂规则,那么可以把这部分提取到一个单独的模型中,或者提取到一个允许声明规则的简单框架中。重点突击某个部分。

10.9.2 尽可能利用已有的形式

在商业领域,会计的一些概念,是使用已久的,直接使用就行

股份/借款的例子,缺乏概念模型:share pie

最开始设计成entity,share pie的标识在loan内部

后来改成value object
变成操作闭合,不增加share pie,或者向它添加share,而只是把两个share pie加起来,结果是一个新的,更大的share pie,prorate()操作,按比例的。

11 分析模式的应用

分析模式:是一种概念集合,用来表示业务建模中的常用构造,可能只与一个领域有关,也可能跨越多个领域。

分析模式不是技术解决方案,只是用来指导人们设计特定领域中的模型

基本没get到是啥子意思

12 将设计模式应用于模型

设计模式:并不是像链表和散列表那样可以被封装到类中并供人们直接重用的设计,也不是直接用于整个应用程序或者子系统的复杂的、专用于领域的设计。本书中的设计模式是对一些交互的对象和类的描述,我们通过定制这些对象和类来接觉特定上下问中的一半设计问题。

为了在领域驱动的设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度看它们是技术设计模式,从模型的角度来看它们就是概念模式。

通过composite(组合)和strategy(策略)这两种模式来讲解,用一些经典的设计模式来解决领域问题

12.1 模式:strategy(也称为policy)

定义了一组算法,将每个算法封装起来,并使它们可以互换。

我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照strategy设计模式来实现规则,或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。

传统上,人们把strategy模式看作是一种设计模式,这种观点的侧重点是它替换不同的算法的能力。而把它看作领域模型的侧重点,是其表达概念的能力,这里的概念通常是指过程或者策略固资规则。

解决条件判断太多的方法是,把这些起调节作用的参数分离到strategy中,这样它们就可以被明确地表示出来,并作为参数传递给routing service。

领域中一个至关重要的规则明确地显示出来了。在构建itinerary时用于选择leg的基本规则。它传达了这样一个知识:路线选择的基础是航段的一个特定属性,这个属性最后可归结为一个数字。这样就可以在领域语言中,用一句简单的话来定义Routing service的行为:routing service根据所选定的strategy来选择leg总规模最小的itinerary。

12.2. 模式:composite


由多个部分组成的重要对象,这些部分本身又由其他一部分组成,进而又由其他部分组成。

当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化。

定义一个把composite的所有成员都包含在内的抽象类型,在容器上实现一些用来查询信息的方法,这些方法可用来收集与容器内容有关的信息。“叶”节点基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。

13 通过重构得到更深层的理解

13.2 探索团队

不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的精化方案。

13.5 重构的时机

当发生了以下情况时,就应该重构

  • 设计没有表达出团队对领域的最新理解
  • 一些重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法)
  • 发现了一个能令某个重要的设计部分变得更灵活的机会

通过重构得到更深层理解是一个持续不断的过程。人们可能会发现一些隐含的概念,并把它们明确地表示出来。

第四部分 战略设计

无法通过分析对象来理解系统时,就需要掌握一些操作和理解大模型的技术。本书的这一部分将介绍一些原则,遵循这些原则,就可以对一些十分复杂的领域进行建模。

企业在概念和实现上把系统分解为较小的部分。问题是如何在不损害集成利益的前提下完成这种模块化的过程。从而使系统的不同部分能够进行相互操作。以便使各种业务相互协调。

这一部分探索了三个大的主题:上下文、精炼和大比例结构

上下文:最基本的主题,无论大小、成功的模型都必须在逻辑上保持整体的一致,不能有互相矛盾或重叠的定义。通过为每个模型显式地定义一个bounded context。然后在必要的情况下定义它与其他上下文的关系,建模人员就可以避免使用模型变得缠杂不清。

精炼:通过精炼可以减少混乱,并且把注意力集中到正确的地方。人们通常在领域的一些次要问题上花费了太多的精力。整体领域模型必须要突出系统中最有价值和最特殊的那些方面。

大比例结构:用来描述整个系统,在一个非常复杂的模型中,人们可能会”只见树木,不见森林“。如果不沿着一个主题来应用一些系统级的设计元素模式的话。关系仍然可能非常混乱。概要介绍几种大比例结构的方法,然后详细讨论其中的一种模式–responsibility layer(职责层),通过这个示例来探索大比例结构的含义。

14 保持模型的完整性

模型最基本需求是它应该保持内部的一致性、术语总具有相同的一意义且不包含互相矛盾的规则。尽管我们很少明确的考虑这些需求。模型的内部一致性由叫做“统一”,这样每个术语都不会有模棱两可的意义,也不会有规则冲突。

在大型项目中尝试把所有软件统一到一个模型中,可能会有下面的风险

  1. 一次尝试对遗留系统做过多的替换
  2. 大项目可能会陷入困境,因为协调的开销太大,超出了这些项目的能力范围
  3. 具有一些特殊需求的应用程序坑不得不使用无法充分满足需求的模型,而只能将这些无法满足的行为放到其他地方。
  4. 试图用一个模型来满足所有人的需求可能会导致模型中包含过于复杂的选择

权力上的划分和管理级别的不同也要求把模型分开。

通过预先决定什么应该统一,并实际认识到什么不能统一,就能够创建一个清晰的、共同的视图。

需要用一种方式来标记处不同模型之间的边界和关系,需要有意识的选择一种策略,并一致地遵守它。

本章将介绍一些用于识别、沟通和选择模型边界以及关系的技术。限界上下文定义了每个模型的应用范围,而上下文图则给出了项目上下文以及它们之间关系的总体视图。

区分出那些具有共享内核的紧密关联的上下文,以及那些具有独立方式的松散耦合的模型。

14.1 模式:bounded context(限界上下文)

一个模型只在一个上下文中使用

  • 明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物流表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性。而不要收到边界之外问题的干扰和混淆。
  • Continuous integration:持续的集成。
  • 识别bounded context中的不一致:将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。重复的概念:两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时候,必须要更新两个地方。
  • 假同源:指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事,实际上并不是这样。当两个定义都与同一个领域方面相关,而只是在概念上稍微有区别时。这种冲突更难发现。

说来半天也没教怎么去划分限界上下文。

14.2 模式:continuous integragtion

定义完一个bounded context后,必须让它保持合理化

极限编程(xp)在这样的环境中真正显示了其特性。很多xp实践都是针对在很多人频繁更改设计的情况下如何维护设计的一致性这个特定问题而出现的。xp是一种非常适合在bounded context中维护模型完整性的形式,但是无论是否使用xp,都很有必要采取一些continuous integration过程。

持续集成:指把一个上下文中的所有工作足够频繁地合并到一起,并使它们经常保持一致。以便当

模型发生分裂时,可以迅速发现并纠正问题。持续集成也有两个级别的操作

  • 模型概念的集成
  • 实现的集成

大部分有效的方法都具有下面的特征

  • 分步集成,采用可以重复使用的合并/构建技术
  • 自动测试套件
  • 有一些规则,用来为那些尚未集成的改动设置一个合理的、稍高的生命期上限(没明白是啥意思)
  • 在讨论模型和应用程序时要坚持使用通用语言

建立一个经常把所有代码和其他实现工件合并到一起的过程,并通过自动测试来快速查明模型的分裂问题。严格坚持使用通用语言,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。

14.3 模式:context map(上下文图)

描述模型之间的接触点,明确每次交流所需的转换,并突出任何共享的内容。画出现有的范围。为稍后的转换做好准备。

  • 信任,但要确认

两个context:预订context和运输网络context

把协调这些bounded context之间的交互的职责交给routing service来完成。

上下文图,像是两个上下文之间的桥梁

public Itinerary route(RouteSpecification spec) {Booking_TransportNetwork_Translator translator = new Booking_TransportNetwork_Translator();List constraintLocation =  //做了一次转换translator.convertConstraints(spec);//通过地点找路径List pathNodes = traversalService.findPath(constraintLocation);//转换Itinerary result = translator.convert(pathNodes);return result;
}

两个上下文之间的接口非常小。Routing Service的接口把预订上下文的剩余部分与路线查找时间隔离开。这个接口完全是side-effect-free function构成,很容易测试。

14.9 模式 separate way

独立自主

集成总是代价高昂,有时却获益却很小

声明一个与其他上下文毫无关联的bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案(而不是一定要强行集成)

14.10 模式:open host service

separate way开发的模型是很难合并的。如果最终仍然需要集成,那么转换层将是必要的,而且坑很复杂。

定义一个协议,把你的子系统作为一组service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。

其他子系统就变成了与open host的模型相连接,而其他团队则必须学习host团队所使用的专用术语。在某些情况下,使用一个众所周知的published language(公开发布的语言)作为交换模型可以减少耦合并简化理解。

14.11 模式:published language

一个良好文档化的,能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。

举例:xml。cml

14.12 “大象”的统一

14.13.6 正在设计的系统

需要为正在设计中的整个设计使用一个bounded context。你可能希望采用shared kernel模式,并把几组相对独立的功能划分到bounded context中。在这些bounded context中,如果有两个上下文之间的所有依赖性都是单向的,就可以建成为customer/supplier development team。

14.13.7 满足不同模型的特殊需要

集成和不集成

14.3.8 部署

bounded context策略的选择,将对部署产生影响。在分布式系统中,一个好的做法是把context之间的转换层保持在单个进程中,这样就不会出现多个版本共存的情况。

14.3.10 当项目正在进行时

围绕当前组织结构来加强团队的工作。在context中改进continuous integration。把所有分散的转换代码重构到 anticorrruption layer中。

14.14 转换

像建模和设计的其他方面一样,有关bounded context的决策也是可以改变的。分割context是很容易的,但是合并它们或者改变它们的关系却很难。下面将介绍几种有代表性的修改。

14.14.1 合并context:separate way -> shared kernel

  1. 评估初始状况。在开始统一两个context之前,确信他们确实需要统一
  2. 建立合并过程,需要决定代码的共享方式以及模块应该采用哪种命名约定。
  3. 选择某个小的子领域作为开始。它是在两个context中重复出现的子领域,但不是code domain 的一部分。最好选择一些简单且相对普通或不重要的部分。

《领域驱动设计》学习笔记相关推荐

  1. 第二行代码学习笔记——第六章:数据储存全方案——详解持久化技术

    本章要点 任何一个应用程序,总是不停的和数据打交道. 瞬时数据:指储存在内存当中,有可能因为程序关闭或其他原因导致内存被回收而丢失的数据. 数据持久化技术,为了解决关键性数据的丢失. 6.1 持久化技 ...

  2. 第一行代码学习笔记第二章——探究活动

    知识点目录 2.1 活动是什么 2.2 活动的基本用法 2.2.1 手动创建活动 2.2.2 创建和加载布局 2.2.3 在AndroidManifest文件中注册 2.2.4 在活动中使用Toast ...

  3. 第一行代码学习笔记第八章——运用手机多媒体

    知识点目录 8.1 将程序运行到手机上 8.2 使用通知 * 8.2.1 通知的基本使用 * 8.2.2 通知的进阶技巧 * 8.2.3 通知的高级功能 8.3 调用摄像头和相册 * 8.3.1 调用 ...

  4. 第一行代码学习笔记第六章——详解持久化技术

    知识点目录 6.1 持久化技术简介 6.2 文件存储 * 6.2.1 将数据存储到文件中 * 6.2.2 从文件中读取数据 6.3 SharedPreferences存储 * 6.3.1 将数据存储到 ...

  5. 第一行代码学习笔记第三章——UI开发的点点滴滴

    知识点目录 3.1 如何编写程序界面 3.2 常用控件的使用方法 * 3.2.1 TextView * 3.2.2 Button * 3.2.3 EditText * 3.2.4 ImageView ...

  6. 第一行代码学习笔记第十章——探究服务

    知识点目录 10.1 服务是什么 10.2 Android多线程编程 * 10.2.1 线程的基本用法 * 10.2.2 在子线程中更新UI * 10.2.3 解析异步消息处理机制 * 10.2.4 ...

  7. 第一行代码学习笔记第七章——探究内容提供器

    知识点目录 7.1 内容提供器简介 7.2 运行权限 * 7.2.1 Android权限机制详解 * 7.2.2 在程序运行时申请权限 7.3 访问其他程序中的数据 * 7.3.1 ContentRe ...

  8. 第一行代码学习笔记第五章——详解广播机制

    知识点目录 5.1 广播机制 5.2 接收系统广播 * 5.2.1 动态注册监听网络变化 * 5.2.2 静态注册实现开机广播 5.3 发送自定义广播 * 5.3.1 发送标准广播 * 5.3.2 发 ...

  9. 第一行代码学习笔记第九章——使用网络技术

    知识点目录 9.1 WebView的用法 9.2 使用HTTP协议访问网络 * 9.2.1 使用HttpURLConnection * 9.2.2 使用OkHttp 9.3 解析XML格式数据 * 9 ...

  10. 安卓教程----第一行代码学习笔记

    安卓概述 系统架构 Linux内核层,还包括各种底层驱动,如相机驱动.电源驱动等 系统运行库层,包含一些c/c++的库,如浏览器内核webkit.SQLlite.3D绘图openGL.用于java运行 ...

最新文章

  1. BAT“上山下乡”,用AI“打入政府”
  2. R语言为dataframe添加新的数据列(add new columns):使用R原生方法、data.table、dplyr等方案
  3. 利用libswscale转换yuyv422到yuv422p或rgb之间的转换, 视频翻转
  4. 移动web开发都会遇到的坑(会持续更新)
  5. Angular(build打包)报错:supplied parameters do not match any signature of call target
  6. .NET 6 中 gRPC 的新功能
  7. 利用多线程句柄设置鼠标忙碌状态
  8. mysql 连接 内存溢出_mysql - MySQL中止连接未知错误 - 堆栈内存溢出
  9. PostgreSQL 12系统表(3)pg_tablespace
  10. 2020软件测试报告模板
  11. 吴裕雄--天生自然 高等数学学习:函数展开成幂级数
  12. pika详解(四) channel 通道
  13. JavaWeb——RequestResponse笔记
  14. moment.js 时间处理类库--时间戳和时间格式相互转换
  15. (OS 10038)在一个非套接字上尝试了一个操作 的解决办法
  16. js处理请求最多的服务器,vue.js 请求服务器
  17. phase test1
  18. 欧拉回路python
  19. 数据分析案例-对某宝用户评论做情感分析
  20. 方舟php服务器控制,《方舟:生存进化》私人服务器设置教程

热门文章

  1. aso优化师是什么_什么是ASO优化?
  2. 网络流——基础,Dinic和Sap(Gap优化)算法
  3. Java计算同比环比
  4. 老人信息管理系统c语言,基于STM32的老人吃药提醒器——智能电子药盒设计(原理图、PCB源文件、源码、APP源码等)...
  5. html按钮点击后无效,关于html中按钮的单击事件,第一次单击可以运行,再次单击不能运行的解决方法...
  6. uni-app实现微信小程序一键登录
  7. 深入了解Spring IoC
  8. 软件测试周刊(第24期):最不重要的素质就是智商
  9. 苹果蓝牙耳机使用说明_苹果蓝牙耳机怎么用,其功能及使用方法介绍
  10. HTML5+CSS3基础响应式页面布局