领域驱动设计 读书笔记 (2)

与所有探索活动一样,建模本质上是非结构化的。

对象分析的传统方法是先在需求文档中确定名词和动词,并将其作为系统的初始对象和方法。这种方法太过简单,只适合教导初学者如何建模。实际上,初始模型都是基于对领域的浅显认知而构建的。不够成熟、深入。

比如一个运输系统,不应该是货轮和集装箱的模型。而是船只航次(货轮、火车等调度好的航程)。

深层模型能穿过领域表象,清楚地表达领域专家的主要关注点和最相关的知识。

突破

通过不断地重构能够给系统最需要修改的地方增加灵活性。并找到简单快捷的方式来实现普通的功能。

重构的原则是始终小步前进。始终保持系统正常运转
当突破带来更深层的模型时,通常会令人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。
建模和设计工作的最重要的进展都来自突破
不要试图制造突破。通常,只有在实现了许多适度的重构之后才有可能出现突破。大部分时间里,我都在进行微小的改进,而在这种连续的改进中,深层模型含义也会逐渐显现。

要为突破做好准备,应专注于知识消化过程。寻找那些重要的领域概念,并在模型中清晰地表达出来。
不要犹豫着不做小的改进,这些改进即使脱离不开常规的概念框架,也会逐渐加深我们对模型的理解。

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

深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。深层建模的第一步是要设法在模型中表达出领域的基本概念。然后不断的消化知识和重构中,实现模型的精化。

若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码做许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式德表达出来。

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

约束条件一般是无法用单独的方法来轻松表达的。
下面是一些警告信号,表明约束的存在正扰乱其宿主对象(Host Object)的设计:

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

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

首先,我们不希望将过程变成模型的主要部分。对象是用来封装过程的,这样我们只需考虑对象的业务目的或意图就可以了。
我们现在讨论的是存在于领域中的过程,我们必须在模型中把这些过程表示出来。否则当这些过程显露出来时,往往使对象设计变得笨拙。

如果过程的执行有多种方式,那么我们也可以用另一种方式处理它,那就是将算法本身或者其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象。每个对象都表示一种不同的STRATEGY

规格,specification,提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它显式表示出来。

业务规则通常不适合作为ENTITY或者VALUE OBJECT的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。但是将规则移出领域层的结果更糟糕,因为这样一来,领域代码就不再表达模型了。
逻辑编程提供了一种概念,即“谓词”这种可分离可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念过于通用,在表达设计意图方面,它的针对性不如专门的设计那么好。
幸运的是,我们并不需要完全实现逻辑编程即可受益,大部分规则可以归类为几种特点的情况。我们可以借用谓词概念来创建可计算出布尔值的特殊对象。那些难于控制的测试方法可以巧妙地扩展出自己的对象。他们都是些小的真值测试,可以提取到单独的VALUE OBJECT中。而这个新对象则可以用来计算另一个对象,看看谓词对那个对象的计算是否为真。
这个新对象就是规格。规格中的声明是限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。规格有多种用途,其中一种体现了最基本的概念。这种用途是,规格可以测试任何对象以检验他们是否满足制定的标准。

简单的规格可以组合。

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

  • 验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备
  • 从集合中选择一个对象(比如过期发票)
  • 指定在创建新对象时必须满足某种需求

柔性设计

柔性设计是对深层建模的补充。
过多的抽象层和间接设计常成为项目的绊脚石。
当复杂性阻碍了项目的前进时,就需要仔细修改最复杂最关键的地方,使之变成一个柔性设计,突破复杂性带给我们的限制,不会陷入遗留代码维护的麻烦中。

INTENTION-REVEALING INTERFACES

如果开发人员为了使用一个组件而必须去研究它的实现,那就失去了封装的价值。当某个人开发的对象或者操作被别人使用时,如果使用这个组件的新的开发者不得不根据实现来推测其用途,那么他推测出来的可能并不是那个操作或者类的主要用途。如果这不是那个组件的用途,虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的想法背道而驰。
命名类或操作时要描述他们的效果和目的,而不要表露他们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部实现细节。这些名称应该与UBIQUITOUS LANGUAGE保持一致,以便团队成员可以迅速推断出他们的意义。在创建一个行为之前,先为它编写一个测试,这样可以促使你站在一个客户开发人员的角度来思考它。

所有复杂的机制都应该封装到抽象接口的后面,接口只表明意图,而不表明方式

在领域的公共接口中,

  • 可以把关系和规则表述出来,但是不要说明规则是如何实施的
  • 可以把事件和动作描述出来,但是不要描述他们是如何执行的
  • 可以给出方程式,但是不要给出解方程式的数学方法
  • 可以提出问题,但是不要给出获取答案的方法

SIDE-EFFECT-FREE FUNCTION

操作可以分为命令和查询。
任何对未来操作产生影响的系统状态改变都可以称为副作用。

多个规则的相互作用或计算的组合所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现和它所调用的其他方法的实现。如果开发人员不得不揭开接口的面纱,那么接口的抽象作用就受到了限制。如果没有了可以预测到结果的抽象,开发人员就必须限制组合爆炸,这就限制了系统行为的丰富性。

减少命令

  • 把命令和查询严格地放在不同的操着中。确保导致状态改变的方法不返回领域数据,并尽可能保持简单。在不引起可观察到的副作用的方法中执行所有的查询和计算
  • 总有一些替代的模型或设计,他们不要求对现有对象做任何修改。相反,他们创建并返回一个VALUE OBJECT,用于表示一个计算结果。VALUE OBJECT可以在一次查询的响应中被创建和丢弃,不像ENTITY,实体的生命周期是受到严格管理的

如果一个操作把逻辑或计算与状态改变混杂在一起,我们就应该把这个操作重构成两个独立的操作。但是从定义上看,这种把副作用隔离到简单的命令方法中的做法仅适用于ENTITY。在完成了命令和查询的分离之后,可以考虑再进行一次重构,把复杂计算的职责转移到VALUE OBJECT中。

ASSERTION

把复杂计算封装到SIDE-EFFECT-FREE FUNCTION可以简化问题,但实体仍然会留有一些有副作用的命令,使用这些ENTITY的人必须了解使用这些命令的后果。这种情况下,使用断言可以把副作用明确地表示出来,使他们更容易处理。

如果操作的副作用仅仅是由他们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径跟踪程序的执行。封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。

INTENTION-REVEALING INTERFACE只能非正式地给出操作的用途,但是这常常是不够的。契约式设计,向前推进了一步,通过给出类和方法的断言,使开发人员知道肯定会发生的结果。

后置条件描述了操作的副作用,也就是调用一个方法后必然会导致的结果。
前置条件就像合同条款,即为了满足后置条件必须要满足的前置条件。类的规定规则规定了操作结束时对象的状态。

所有这些断言都描述了状态,而不是过程。因此他们更易于分析。
断言应该把调用其他操作的效果都考虑在内了。

把固定规则、后置条件和前置条件都清楚地表述出来,这样开发人员就能够理解使用一个操作或对象的后果。

INTENTION-REVEALING INTERFACE清楚地表明了用途,SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们准确预测结果。因此封装和抽象更加安全。

CONCEPTUAL CONTOUR

如果把模型或设计的所有元素都放在一个整体的大结构中,他们的功能会发生重复。外部接口可能无法给出客户关心的全部信息。由于不同的概念被混杂在一起,他们的意义就会变得难理解。
而另一方面,把类和方法分解开也可能是毫无意义的。这会使客户更复杂,迫使客户对象去理解各个细微部分是如何组合在一起的。更糟糕的是,有的概念会完全丢失。铀原子的一半并不是铀。而且,粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。

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

我们的目标是得到一组可以在逻辑上组合起来的简单接口,使我们可以用UBIQUITOUS LANGUAGE进行合理的表述。并且使那些无关的选项不会分散我们的注意力。也不增加维护负担。

STANDALONE CLASS

互相依赖使得模型和设计变得难以理解、测试和维护。
MODULE和AGGREGATE的目的是为了限制互相依赖的关系网。当我们识别出一个高度内聚的子领域并把他们提取到一个MODULE的时候,一组对象也随之与系统的其他部分也解除了联系。这样就把互相联系的概念的数量限制在一个有限的范围内。
即使在MODULE内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担更大。

我们应该对每个依赖关系提出质疑,直到正式它确实表示对象的基本概念为止。
低耦合是设计对象的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了。这就使得我们可以单独研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE而带来的负担。

我们的目标是消除所有不重要的依赖
尽力把最复杂的计算提取到STANDALONE CLASS中,实现此目的的一种方法是从存在大量依赖的类中将VALUE OBJECT建模出来。

独立的类是低耦合的极致。

CLOSURE OF OPERATION

闭合(比如实数的乘法是闭合的)的性质简化了对操作的理解,而且闭合操作的链接或组合也容易理解。
在适当情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引起对其他概念的任何依赖。
这种模式更常用于VALUE OBJECT的操作。
在尝试和寻找减少互相依赖并提高内聚的过程中,有时候会碰到“半个闭合操作”这种情况。参数类型和实现者的类型一致,但返回类型不同。或返回类型与接收者的类型相同但参数类型不同。这些操作都不是闭合操作,但他们确实具有闭合操作的某些优点。当没有形成闭合操作的那个多出来的类型是基本类型或基本库类时,它几乎与闭合操作一样减轻了我们的思考负担。

声明式设计

声明式设计,通常是指一种编程方式-把程序或程序的一部分写成一种可执行的规格。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。
声明式设计有多种实现方式,比如反射,比如根据声明自动生成传统代码。

从模型属性的声明来生成可运行的程序是MODEL-DRIVEN DESIGN的理想目标。但是在实践中这种方法也有自己的缺陷。

  • 声明式设计并不足以表达一切所需的东西,它把软件束缚在一个由自动部分构成的框架之内。使软件很难扩展到这个框架之外
  • 代码生成技术破坏了迭代循环。它把生成的代码合并到手写的代码中,使得代码重新生成具有巨大的破坏作用

基于规则的编程(带有推理引擎和规则库)是另一种有望实现的声明式设计方法。

尽管基于规则的程序原则上是声明式的,但是大多数系统都有一些用于性能优化的控制谓词(control predicate)。这种控制代码引入了副作用,这样行为就不再由声明完全控制了。添加、删除规则或者重新排序可能会引起预想不到的后果。

声明式设计发挥的最大价值是用一个范围非常窄的框架自动处理设计中某个特别单调又容易出错的地方,比如持久化和对象关系映射。

声明式设计风格

柔性设计使得客户代码可以使用声明式的设计风格。

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

在不考虑实际设计的情况下单纯分析是有缺陷的。

将设计模式应用于模型

有些设计模式可以用作领域模式,不过,这样使用的时候,需要变换一下重点。
设计模式把相关设计元素归为一类,这些元素能够解决在各种上下文中经常遇到的问题。这些模式的动机以及模式本身都是从纯技术角度描述的。但这些元素中的一部分在更广泛的领域和设计上下文中也适用,因为这些元素所对应的基本概念在很多领域中都会出现。

近年来还出现了许多技术设计模式,有些模式反映了在一些领域中出现的深层概念。
从代码的角度看,他们是技术设计模式。
从模型的角度看他们就是概念模式。

STRATEGY(也称为POLICY)

它定义了一组算法,并将每个算法封装起来,并使他们可以互换。STRATEGY允许算法独立于他们的客户而变化。

领域模型包含一些并非用于解决技术问题的过程,将他们包含进来是因为他们对处理领域问题具有实际的价值。当必须从多个过程中进行选择时,选择的复杂性再加上多个过程本身的复杂性会使局面失去控制。

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

通常,作为设计模式的策略侧重于替换不同算法的能力。而当它作为领域模式时,侧重点则是表示概念(过程或策略规则)的能力。
使用策略,可能会增加系统中对象的数目。如果这是个问题,可以把STRATEGY实现为无状态对象,以便在上下文中共享,从而减小开销。

COMPOSITE

将对象组织为树表示部分-整体的层次结构。利用COMPOSITE,客户可以对单独的对象和对象组合进行同样的处理。
在一些领域中,各层嵌套在概念上有区别的。但在另一些领域中,各个部分与他们组成的整体是完全相同的事物,只是规模较小一些而已。

当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化。(例如,容器通常不能包含同一层中的其他容器,而且嵌套的层数也是固定的)客户必须通过不同的接口来处理来处理层次结构中的不同层,尽管这些层在概念上没有区别。通过层次结构来递归地收集信息也变得非常复杂。

定义一个把COMPOSITE的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而叶节点则基于他们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分叶和容器。

COMPOSITE模式在每个结构层上都提供了相同的行为。而且无论较小的部分还是较大的部分,都可以对这些部分提出一些有意义的问题,这些问题能够透明地反映出他们的构成情况。

比如船运的航段。

为什么没有介绍FLYWEIGHT

FLYWEIGHT不适用于领域模型。

当一个VALUE OBJECT集合(其中的值对象数目有限)被反复使用的时候(比如房屋规划中的电源插座),把他们实现为FLYWEIGHT可能是有意义的。这是一个适用于VALUE OBJECT而不适用于ENTITY的实现选择。COMPOSITE模式与他的不同之处在于,组合模式的概念对象是由其他概念对象组成的。这使得组合模式既适用于模型,也适用于实现,这是领域模型的一个基本特征。

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

要关注:

  • 以领域为本
  • 用一种不同的方式来看待事物
  • 始终坚持与领域专家对话

何时重构:

  • 设计没有表达出团队对领域的最新理解
  • 重要的概念被隐藏在设计中了
  • 发现了一个能令重要的设计部分变得更灵活的机会
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值