领域的驱动设计 读后日志

领域驱动设计-软件核心复杂性应对之道

前言

  1. 目的:交付能够满足组织后续需求,可以不断演进的复杂软件。
  2. 真正决定软件复杂性的是设计方法。
  3. 很多应用程序最主要的复杂性并不在技术上,二十来自领域本身、用户的活动或业务。
  4. 本书的两个前提:
      • 在大多数软件项目中,主要的焦点应该是领域和领域逻辑;
      • 复杂的领域设计应该基于模型。
  5. 两个开发实践:
      • 迭代开发。
      • 开发人员与领域专家具有密切的关系。

第一部分 运用领域模型

①模型是一种简化,它是对现实的解释——把解决问题密切相关的方面抽象出来,而忽略无关的细节。

②模型在领域驱动设计中的作用:

  1. 模型和设计的核心相互影响
  2. 模型是团队所有成员使用的通用语言中枢
  3. 模型是浓缩的知识

③软件的核心:

软件的核心是为其用户解决领域相关的问题的能力。

第一章 消化知识

    1. 有效建模的要素:
      1. 模型和实现的绑定。
      2. 建立了一种基于模型的语言。
      3. 开发一个蕴含丰富知识的模型。
      4. 提炼模型。
      5. 头脑风暴和实验。
    2. 知识消化:
      1. 知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。
      2. 模型永远都不会是完美的,因为它是一个不断演化完善的过程。模型对理解领域必须是切实可用的。它们必须非常精确,以便使应用程序易于实现和理解。
    3. 持续学习
    4. 知识丰富的设计:
      1. 业务活动和规则如同所涉及的实体一样,都是领域的核心。
      2. 不建议精细设计应用到领域的每个细节。
    5. 深层模型:
      1. 知识消化是一种探索,它永无止境。

第二章 交流与语言的使用

  1. 通用语言(Ubiquitous Language):
    1. 通用语言的词汇包括类和主要操作的名称。
    2. 开发人员应该使用基于模型的语言来描述系统中的工件、任务和功能。
    3. 将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
    4. 要认识到。通用语言的更改就是对,模型的更改。
  2. “大声地”建模
    1. 讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。
  3. 一个团队、一种语言
    1. 有了通用语言之后,开发人员之间的对话、领域专家之间的讨论以及代码本身所有表达的内容都基于同一种语言,都来自于一个共享的领域模型。
  4. 文档和图:
    1. UML图无法传达模型两个重要的方面:①模型所表达的概念意义②对象应该做哪些事情
    2. 我们应该避免使用包罗万象的对象模型图,甚至不能使用包含所有细节的UML数据存储库。
    3. 设计的重要细节应该在代码中体现出来。
    4. 务必记住模型不是图。
  5. 书面设计文档
    1. 文档应作为代码和口头交流的补充。
      • 文档不应再重复表示代码已经明确表单出的内容。
    2. 文档应当鲜活并保持最新
      • 文档必须深入到各种项目活动中去。
      • 如果文档不再担负重要的作用,那么纯粹靠意志和纪律保持其更新就是浪费精力。
      • 通过将文档减值最少,并且主要用它来补充代码和口头交流,就可以避免文档与项目的脱节。
      • 根据通用语言及其演变来选择那些保持更新并与项目活动紧密交互的文档。
    3. 完全依赖可执行代码的情况:
      • 尽管代码可能会产生误导,但它仍然比其它文档更基础。要想利用当前的标准技术使代码所传达的消息与它的行为和意图保持一致,需要纪律和思考设计的特定方式。要有效地交流,代码必须在编写需求时所使用的同一种语言,也就是开发人员之间、开发人员与领域专家之间的讨论时所使用的语言。
    4. 解释性模型
      • 本书的核心思想是在实现、设计和团队交流中使用同一个模型作为基础。

第三章 绑定模型和实现

领域驱动设计要求模型不仅能够指导早期的分析工作。

  1. 模式:Model-Driven Design(模型驱动设计)
    1. 严格按照基础模型来编写代码,能够使代码更好地表达设计含义,并且使用模型与实际的系统相契合。
    2. 模型驱动设计不再将分析模型和程序设计分离,而是寻求一种能够满足这两方面需求的单一模型。
    3. 软件系统各个部分的设计应该忠诚的反应领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的通用语言。
    4. 单一模型能够减少出错的概率,因为程序设计直接来源于经过仔细考虑而创建的模型。程序设计,甚至是代码本身,都与模型密不可分。
    5. 软件开发是一个不断精化模型、设计和代码的统一的迭代过程。
  2. 建模范式和工具支持
    1. 面向对象编程之所以功能强大,是因为它基于建模范式,并且为模型构造提供了实现方式。
    2. 我们需要反复研究领域知识,不断重构模型,才能够将领域中重要的概念提炼成简单而清晰的模型。
  3. 揭示主题:为什么模型对用户至关重要
    1. 让用户了解模型,将使他们有更多机会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。
  4. 模式:Hands-On Modeler(亲身实践的建模者)
    1. 任何参与建模的技术人员,不管在项目中主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度的参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过通用语言与接触代码的人及时交换关于模型的想法。
  • 模型驱动设计的构造快

将领域设计与软件系统中的其他关注点分离会使设计与模型之间的关系非常清晰。根据不同的特征来定义模型元素则会使元素的意义更加鲜明。

  • 分离领域
  1. Layered Architecture(分层架构)

用户界面层

(表示层)

负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。

应用层

定于软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层来进行交互的必要渠道。

应用层要尽量简单,不包括业务规则或者知识,而只为下一层中的领域对象协调任务、分配工作,使他们相互协作。他没有反映业务的状况,但是却可以具有另外一种状态为用户或程序显示某个任务的进度。

领域层

(模型层)

负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态得技术细节是由基础设施层实现的,但是反应业务情况的状态是由本层控制并使用的。领域层是业务软件的核心

基础设施层

为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。

 

  1. 给复杂的应用程序进行划分层次。
  2. 在每一层分别进行设计,使其具有内聚性并且只依赖于它的下层。
  3. 领域对象应该将重点放在如何表达领域模型上。
  1. 将各层关联起来
    1. 各层之间是松散连接的,层与层的依赖关系只能是单向的。
  2. 架构框架
    1. 框架使用目的:建立一种可以表达领域模型的实现并且用它来解决重要问题。
  3. 领域层是模型的精髓
  4. 模式:The Smart UI“反模式”
    1. 领域驱动设计只有应用在大型项目上才能产生最大收益。
  • 软件中所表示的模型
  1. 关联
    1. 模型中每个可遍历的关联,软件中都要有同样属性的机制。
    2. 设计必须指定一种具体的遍历机制,这种遍历的行为应该与模型的关联一致。
    3. 使关联更易控制的方法:
      • 规定一个遍历方向
      • 添加一个限定符,以便有效的减少多重关联
      • 消除不必要的关联
  2. 模式:Entity(又称为Reference Object)
    1. 很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。
    2. 在对象的很多个实现、存储形式和真实世界的参与者(如打电话的人)之间,概念性标识必须是匹配的。
    3. 一些对象主要不是由他们的属性定义的。他们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经理多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属相的另一个对象区分开。错误的标识可能会破坏数据。
    4. Entity的两个条件:
      • 它在整个生命周期中具有连续性
      • 它的区别并不是由那些对用户非常重要的属性决定的。
    5. 标识是Entity的一个微妙的、有意义的属性,我们是不能把他交给语言的自动特性来处理的。
    6. 当一个对象尤其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定对象的定义。
    7. 模型必须定义出“符合什么条件才算是相同的事物”。
  3. Entity建模
    1. 抓住Entity对象的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必须的属性。
  4. 设计标识操作
    1. 不管系统是如何定义的,都必须确保标识属性在系统中是唯一的,即使是在分布式系统中,或者对象已被归档,必须确保标识的唯一性。
    2. 当对象属性没办法形成真正唯一键时,另一种经常用到的解决方案是为每个实例附加一个在类中唯一的符号(如一个数字或字符串)。
  5. 模式:Value Object
    1. 很多对象没有概念上的标识符,他们描述了一个事务的某种特征。
    2. 用于描述领域的某个方面的人本身没有概念标识的对象称为Value Object(值对象)。
    3. Value Object应该是不可变的。
    4. Value Object所包含的属性应该形成一个概念整体。
  6. 设计Value Object
    1. 最好使用共享的情况:
      • 节省数据库空间或减少对象数量是一个关键要求时
      • 通信开销很低时
      • 共享对象被严格要求为不可变时
    2. 如果一个Value的实现是可变的,那么就不应该共享它。
    3. 存储相同数据的多个副本的技术称为非规范化(denormalization)。
  7. 模式:Service
    1. Service强调的是与其他对象的关系,它只是定义了能够为客户做什么。
    2. Service往往是以一个活动命名。
    3. 好的service有3个特征:
      • 与领域概念相关的操作不是entity或value object的一个自然组成部分。
      • 接口是根据领域模型的其他元素定义的。
      • 操作是无状态的。
  8. 粒度
    1. 在大型系统中,中等粒度的、无状态的service更容易被复用,因为他们在简单的接口背后封装了重要的功能。
  9. 模式:Module(也称package)
    1. Module提供了两种观察模型的方式:
      • 可以在module中查看细节,而不会被整个模型淹没。
      • 观察module之间的关系,而不考虑内部细节。
    2. Module是一种表达机制
    3. Module的选择应该取决于被划分到模块中的对象的意义。
    4. Module的名称应该是Ubiquitous Language中的术语。Module及其名称反映出领域的深层知识。
  10. 敏捷的Module
    1. 更改module可能需要大范围的更新代码。这些更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具(如源代码控制系统)的使用。因此,module结构和名称往往反映了模型的较早形式,而类则不是这样
    2. 如果一个类确实依赖于一个包中的某个类,而且本地module对该module并没有概念上的依赖关系,那么或许应该移动一个类,或者考虑重新组织module。
  11. 通过基础设施打包时存在的隐患
    1. 框架的设计解决两个合理的问题:
      • 关注点的逻辑划分
      • 层的分布
    2. 精巧的打包方案会产生如下两个代价:
      • 如果框架的分层惯例把实现概念对象的元素分得很零散,那么代码将无法再清楚的标识模型。
      • 人的大脑把划分后的东西还原成原样的能力是有限的,如果框架把人的这种能力都耗尽了,那么领域开发人员就无法再把模型还原成有意义的部分了。
    3. 除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码都放在同一个模块中(如果不能放在同一个对象中的话)。
    4. 利用打包把领域层从其他代码中分离出来。否则,就尽可能让领域开发人员自由的决定领域对象的打包方式,以便支持他们的模型和设计选择。
    5. 不要再领域对象中添加任何与领域对象所表示概念没有紧密关系的元素。领域对象的职责是标识模型。
  12. 建模范式
    1. 主流的范式是面向对象设计。
  13. 在混合范式中坚持使用Model-Driven-Design
    1. 当将非对象元素混合到以面向对象为主的系统中时,需要遵循的4条经验规则:
      • 不要和实现范式对抗。
      • 把通用语言作为依靠的基础。
      • 不要一味依赖UML。
      • 保持怀疑态度。
  • 领域对象的声明周期

主要挑战有两类:

  • 在整个生命周期中维持完整性
  • 防止模型陷入管理生命周期复杂性造成的困境当中。

 

  1. 模式:Aggregate(聚合)
    1. 每个aggregate都有一个根(root)和一个边界(boundary)。
    2. 对aggregate而言,外部对象只可以引用根,而边界内部的对象之间可以互相引用。
    3. 固定规则(invariant)是指在数据变化时必须保持一致性规则,其涉及aggregate成员之间的内部关系。
    4. 为实现概念上的aggregate,需要你对所有事务应用一组规则:
      • 根entity具有全局标识,他最终负责检查固定规则。
      • 根entity具有全局标识。边界内的entity具有本地标识,这些标识只在aggregate内部才是唯一的。
      • Aggregate外部的对象不能引用除根entity之外的任何内部对象。根entity可以把对内部entity的引用传递给它们,但是这些对象只能临时使用这些引用,而不能保持引用。根可以把一个value object的副本传递给另一个对象,而不必关心它发生什么变化,因为,它是一个value,不再与aggregate有任何关联。
      • 作为上一条的推论,只有aggregate的根才能直接通过数据库查询获取。所有其他对象必须通过便利关联来发现。
      • Aggregate内部对象可以保持对其他aggregate根的引用。
      • 删除操作必须一次删除aggregate边界之内的所有对象。
      • 当提交对aggregate边界内部的任何对象的修改时,整个aggregate的所有固定规则都必须被满足。
    5. 只允许外部对象保持根的引用。对内部成员对象的临时引用可以被传递出去,但仅在一次操作中有效。
    6. Aggregate划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。
  2. 模式:Factory
    1. 对象的功能主要体现在其内部配置以及关联方面。
    2. 复杂的对象创建时领域层的职责,然而这项任务并不属于那些用于表示模型的对象。
    3. 任何好的工厂需要满足的基本需求:
      • 每个创建方法都是原子的,而且要保证被创建对象或aggregate的所有固定规则。
      • Factory应该被抽象为所需的类型,而不是所要创建的具体类。
  3. 选择factory及其应用位置
    1. 当有些细节需要隐藏(无论要隐藏的是具体实现还是构造的复杂性)而又找不到合适的地方来隐藏它们时,必须创建一个专用的factory对象或service。
  4. 有些情况下只需要使用构造函数
    1. Factory实际上会使那些不具有多态性的简单对象复杂化。
    2. 以下情况最好使用简单的、公共的构造函数:
      • 类(class)是一种类型(type)。他不是任何相关层次结构的一部分,而且也没有通过接口实现多态性。
      • 客户关心的是实现。可能是将其作为选择strategy的一种方式。
      • 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有潜逃的对象创建。
      • 构造并不复杂。
      • 公共构造函数必须遵守与factory相同的规则:他必须是原子操作,而且要满足被创建对象的所有固定规则。
    3. 不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。
  5. 接口设计
    1. Factory设计:
      • 每个操作都必须是原子的。
      • Factory将与其参数发生耦合。
    2. 最安全的参数是那些来自较低设计层的参数。好的参数选择是模型中被构建对象密切相关的对象,这样不会增加新的依赖。
  6. 固定规则的相关罗技应该放置在哪里
    1. Factory可以将规定规则的检查工作委派给被创建对象,而且这通常是最佳选择。
    2. 在某些情况下,把固定规则的相关逻辑放到factory中是有好处的,这样可以让被创建对象的职责更明晰。对于aggregate规则来说尤其如此。但固定规则的相关逻辑却特别不适合放到那些与其他领域对象关联的factory method中。
  7. Entity factory与value object factory
    1. 由于value object是不可变的,因此factory所生成的对象就是final的。因此factory操作必须得到被创建对象的完整性描述。
    2. Entity factory则只需要具有构造有效aggregate所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
  8. 重建已存储的对象
    1. 用于重建的factory与用于创建的factory的不同:
      • 用于重建对象的entity factory不分配新的跟踪id。
      • 当固定规则未被满足时,重建对象的factory采用不同方式进行处理。
  9. 模式:Repository(存储库)
    1. 获得对象引用的方式:
      • 创建对象
      • 遍历关联
    2. 是提供遍历还是依靠搜索,这成为一个设计决策,需要在搜索的解耦与关联的内聚之间做出权衡。
    3. 把使用以存储的数据创建实例的过程称为重建。
    4. Repository的优点:
      • 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期。
      • 它们使应用程序和领域涉及与持久化技术(多种数据库策略甚至是多个数据源)解耦。
      • 它们体现了有关对象访问的设计决策。
      • 可以很容易将它们替换为“哑实现”(dummy implementation),以便在测试中使用(通常使用内存中的集合)。
  10. Repository查询
    1. 基于specification(规格)的查询是将repository通用化的好办法。
    2. 即使一个repository的设计采用了灵活的查询方式,也应该允许添加专门的硬编码查询。
  11. 客户代码可以忽略repository的实现,但开发人员不能忽略
  12. Repository的实现
    1. 注意事项:
      • 对类型进行抽象
      • 充分利用与客户解耦的优点
      • 将事务的控制权留给用户
  13. Repository与factory的关系
    1. Factory负责处理对象生命周期的开始,而repository帮抓管理生命周期的中间和结束。
    2. Factory负责制造新对象,而repository负责查找已有对象。
    3. 通常,在领域中将新对象和已有对象区分开来很重要的,而将它们组合在一起的框架实际上只会使局面变得混乱。
  14. 为关系数据库设计对象
    1. 三种常见情况:
      • 数据库是对象的主要存储
      • 数据库是为另一个系统设计的
      • 数据库是为这个系统设计的,但它的任务不是用于存储对象
    2. 分析:
      • 当数据库被视作对象存储时,数据模型与对象模型的差别不应太大。可以牺牲一些对象关系的丰富性,以保证它与关系模型的紧密关联。
      • 对象系统外部的过程不应该访问这样的对象存储。它们可能会破坏对象必须满足的固定规则。此外,它们的访问将会锁定数据模型,这样使得在重构对象时很难修改模型。
    3. 大多数情况下关系数据库是面向对象领域中的持久化存储形式,因此简单的对应关系才是最好的。
  • 使用语言:一个扩展的示例
  1. 一般情况下,模型的精化、设计和实现应该在迭代开发过程中同步进行。
  2. 当为了更好的支持设计而对模型进行精化时,也应该让模型反映出对领域的新理解。
  • 通过重构来加深理解
  1. 要想成功开发出实用的模型,需要注意以下3点:
    1. 复杂巧妙的领域模型是可以实现的,也是值得我们去花费力气实现的。
    2. 这样的模型离开不断的重构是很难开发出来的,重构需要领域专家和热爱学习领域知识的开发人员密切参与进来。
    3. 要实现并有效的运用模型,需要精通设计技巧。
  2. 重构层次
    1. 重构就是在不改变软件功能的前提下重新设计它。
  3. 深层模型
    1. 深层模型能够穿过领域对象,清楚地表达出领域专家们的关注点以及最相关的知识。
  4. 深层模型/柔性设计
    1. 柔性设计除了便于修改,还有助于改进模型本身。
  5. 发现过程
    1. 要想创建出确实能够解决当前问题的设计,首先必须拥有可捕捉到领域核心概念的模型。
  • 突破
  1. 华而不实的模型
  2. 突破
  3. 更深层次模型
  4. 冷静决策
  5. 成果
  6. 机遇
  7. 关注根本
  8. 后记:越来越多的新理解
  • 将隐式的概念转变为显示的概念

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

  1. 概念挖掘
  2. 倾听语言
    1. 倾听领域专家使用的语言。
    2. Ubiquitous language(通用语言)是由普遍于对话、文档、模型图甚至代码中的词汇构成的。
  3. 检查不足之处
    1. 要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于解释的地方。
  4. 思考矛盾之处
  5. 查阅书籍
    1. 很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。
  6. 如何为那些不太明显的概念建模
    1. 显式的约束
      • 将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这条约束。
      • 表示约束扰乱“宿主对象(Host Object)”的设计:
        1. 计算约束所需的数据是从定义上看并不属于这个对象
        2. 相关规则在多个对象中出现,造成了代码重复或导致不属于同一族的对象之间产生了继承关系。
        3. 很多设计和需求讨论时围绕约束进行的,而代码实现中,他们却隐藏在过程代码中。
    2. 将过程建模为领域对象
      • “规格(specification)”提供了用于表达特定类型的规则的精确方式,它把这些规则从条件中提取出来,并在模型中把他们显式地表示出来。
    3. 模式:specification
      • 谓词是指计算结果为“真”或“假”的函数,并且可以使用操作符(如AND和OR)把他们连接起来以表达更复杂的规则。
      • Specification(规格)中声明的是限制另一个度喜爱那个状态的约束,被约束对象可以存在,也可以不存在。
      • 为特殊目的的创建谓词形式的显示的value object。Specification就是一个谓词,可用来确定对象是否满足某些标准。
    4. Specification的应用和实现
      • 出于以下目的的一个或多个,我们可能需要指定对象的状态。
        1. 验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备
        2. 从集合中选择一个对象
        3. 指定在创建新对象时必须满足某些需求
      • 使用描述性的specification来定义生成器的接口,这个接口就是显式地约束生成器产生的结构。
        1. 生成器的实现与接口分离
        2. 接口把规则显式地表示出来
        3. 接口更为灵活,或者说我们可以增强其灵活性
        4. 这种接口更加便于测试
  • 柔性设计

当具有发杂行为的软件缺乏良好的设计时,重构或元素的组合会变得很困难。

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

柔性设计是对深层建模的补充。

过多的抽象层和间接设计常常成为项目的绊脚石。

柔性设计能够揭示深层次的底层模型,并把它潜在的部分明确地展现出来。

  1. 模式:Intention-Revealing Interface 释义接口
    1. 要避免“知识过载”问题。
    2. 如果开发人员为了使用一个组件而必须去研究它的实现,那么就失去了封装的价值。
    3. 类型名称、方法名称和参数名称组合在一起,共同形成了一个intention-revealing interface(释义接口)。
    4. 在命名类和操作时要描述他们的效果和目的,而不是要表露他们是通过何种方式达到目的的。
    5. 所有复杂的机制都应该封装到抽象接口的后面,接口只是表明意图,而不是表明方式。
  2. 模式:side-effect-free function(无副作用函数)
    1. 任何对未来操作产生影响的系统状态改变都可以称为副作用。
    2. 返回结果而不产生副作用的操作叫做函数。
    3. 减少命令产生问题的方法:
      • 可以把命令和查询严格的放在不同的操作中。确保导致状态改变的方法不返回领域数据,并尽可能保持简单。
      • 总是有一些替代的模型和设计,他们不要求对现有对象做任何修改。相反,他们创建并返回一个value object,用于表示计算结果。
    4. 如果一个操作把逻辑或计算与状态改变混合在一起,那么我们就应该把这个操作重构为两个独立的操作。
    5. 尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。
  3. 模式:assertion(断言)
    1. 当人们必须了解使用命令的后果时,可以使用assertion(断言)可以把副作用明确地藐视出来,使他们更易于处理。
    2. 把操作的后置条件和类及aggregate的固定规则表述清楚。如果在你的编程语言中不能直接编写assertion,那么就把他们编写成自动单元测试。还可以把他们写到文档或图中(如果符合项目开发风格的话)。
    3. 寻找在概念上内聚的模型,以便便于开发人员更容易推断出预期的assertion,从而加快学习过程并避免代码风格。
  4. 模式:conceptual contour(概念轮廓)
    1. 通过反复重构最终会实现柔性设计。
    2. 寻找概念上有意义的功能单元,这样可以使得设计既灵活又易懂。
    3. 把设计元素(操作、接口、类和aggregate)分解为内聚的单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构在过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层conceptual contour。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
    4. Intention-revealing interface使客户能够把对象表示为有意的单元,而不仅仅是一些机制。Side-effect-free function和assertion使我们可以安全的使用这些单元,并对他们进行复杂组合。Conceptual contour的出现使模型的各个部分变得更稳定,也使得这些单元更为直观,更易于使用和组合。
  5. 模式:standalone class(独立的类)
    1. 互相依赖使模型和设计变得难以理解、测试和维护。
    2. 隐式概念,无论是否已被识别出来,都与显式引用一样会加重思考负担。
    3. 我们应该对每个依赖关系提出质疑,知道证实它确实表示对象的基本概念为止。
    4. 低耦合是对象涉及的一个基本要素。
    5. 尽力把复杂的计算提取到Standalone Class(独立的类)中,实现此目的的一种方法是从存在大量依赖的类中将value object建模出来。
    6. 低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。
  6. 模式:closure of operation(闭合操作)
    1. 大部分引起我们兴趣的对象所产生的行为仅用基本类型是无法描述的。
    2. 闭合的性质极大地简化了对操作的理解,而且闭合操作的链接和组合也很容易理解。
    3. 在适当的情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实际集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。
  7. 声明式设计
    1. 声明式设计通常指一种编程方式——把程序或程序的一部分写成一种可执行的规格(specification)。
    2. 声明式设计发挥最大价值是用一个范围非常窄的框架来自动处理设计中的某个特别单调且易出错的方面。
    3. 领域特定语言是一种有趣的方法,他有时也是一种声明式语言。采用这种编码风格时,客户代码使用一种专门为特定领域的特定模型定制的语言编写的。
    4. 声明式设计风格
      • 用声明式的风格来扩展specification。
      • 包容
        1. 包容就是逻辑蕴含(logical impication)
        2. 在大多数情况下,最好避免出现这样的复杂性:要么选择放弃一些运算符,要么不使用包容。
  8. 切入问题的角度
    1. 分割子领域
      • 重点突出某个部分,使设计的一个部分真正的变得灵活起来,这比分散精力泛泛的处理整个系统要有用的多。
    2. 尽可能利用已有的形式
      • 例子最终;
        1. 复杂的逻辑通过side-effect-free function被封装到了专门的value object。
        2. 修改状态的操作很简单,而且是用assertion来描述的。
        3. 模型概念解除了耦合,操作只涉及最少的其他类型。
        4. 熟悉的形式使我们更容易掌握协议(protocol)。
  • 应用分析模式
  1. 分析模式一种概念集合,用来表示业务建模中的常见结构。他可能与一个领域有关,也可能跨越多个领域(可复用的对象模型)。
  2. 在不考虑实际设计的情况下进行单纯的分析是有缺陷的。
  3. 脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险。
  4. 一定注意,不管其表面形式的变化有多大,都不要改变它所表示的基本概念。原因:
    1. 模式中蕴含的基本概念将帮助我们避免问题
    2. 使用广泛理解或至少被明确解释的术语可以增强ubiquitous language。
  • 将设计模式应用于模式

从代码角度来看他们(设计模式)是技术设计模式,从模型角度来看他们(设计模式)就是概念模式。

  1. 模式:strategy(也称为policy)策略模式
    1. 定义一组算法,将每个算法封装起来,并使他们可以互换。Strategy允许算法独立于使用它的客户端变化。
    2. 我们需要把过程中的易变部分提取到一个单独的“策略”对象中。将规则与他所控制的行为区分开来。按照strategy设计模式来实现规则或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
  2. 模式:composite(组合模式)
    1. 将对象组织为树来表示部分整体的此次结构。利用composite,客户可以单独的对象和对象组合进行同样的处理。
    2. 定义一个把composite的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内所汇总的信息。而“叶子”节点则基于他们自己的值来实现这些方法。客户只需要使用抽象的类型,而无需区分“叶子”和容器。
  3. 为什么没有介绍flyweight(享元模式)
    1. 享元模式不适用于领域模型。
    2. 把设计模式用作领域模式的唯一要求是这些模式能够描述关于概念领域的一些事情,而不仅仅是作为解决技术问题的技术解决方案。
  • 通过重构得到更深层次的理解

三个关注点:

  1. 以领域为本
  2. 用一种不同的方式来看待事物
  3. 始终坚持与领域专家的对话
  1. 开始重构
    1. 与传统重构观点不同的是,即使在代码看上去很整洁的时候也有可能需要重构,原因是模型的语言没有与领域专家保持一致,或者新需求不能被自然地添加到模型中。
    2. 重构的原因也有可能来自于学习:当开发人员通过学习获得了更深刻的理解,从而发现了一个得到更清晰或弓呢更有用的模型的机会。
  2. 探索团队
    1. 想要保证过程的效率:
      • 自主决定
      • 注意范围和休息
      • 练习使用ubiquitous language
  3. 借鉴先前的经验
  4. 针对开发人员的设计
    1. 柔性设计主要通过减少以来和副作用来减轻人们的思考负担。
    2. 在模型中,只有那些对用户最重要的部分才具有较细的粒度。
  5. 重构的时机
    1. 应该进行持续重构
    2. 需要进行重构的情况:
      • 设计没有表达出团队对领域的最新理解
      • 重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法)
      • 发现了一个能令某个重要的设计部分变得更灵活的机会
  6. 危机就是机遇
    1. 通过重构得到更深层理解是一个持续不断的过程。
  • 战略设计
  1. 战略设计原则必须指导设计决策,以便减少各个部分之间的互相依赖,在使设计意图更为清晰的同时而又不失去关键的互操作性和协同性。
  2. 战略设计原则必须把模型的重点放在捕获系统的概念核心。
  3. 三个部分:
    1. 上下文:成功的模型必须在逻辑上一致,不能有互相矛盾或重叠的定义。
    2. 精炼:通过精炼可以减少混乱,并且把注意力集中到正确的地方。
    3. 大型结构:大型结构是用来描述整个系统的。
  • 保持模型的完整性

模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含相互矛盾的规则:虽然我们很少明确地考虑这些要求。模型的内部一致性又叫做统一(unification),这种情况下,每个术语都不会有模棱两可的意义,也不会有规则冲突。

单一模型:这个模型是统一的,没有任何相互矛盾或互相重叠的术语定义。

大型系统领域模型的完全统一既不可行,也不划算。

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

  1. 模式:bounded context(限界上下文)
    1. 一个模型只在一个上下文中使用。(这个上下文可以是代码的一个特定部分,也可以是某个特定团队的工作)
    2. 模型上下文是为了保证该模型中的术语具有特定意义而必须要应用的一组条件。
    3. 为了解决多个模型的问题,我们需要明确地定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能使其保持统一。
    4. 明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。
    5. 跨边界的集成必然需要进行一些转换。
    6. 识别bounded context中的不一致:
      • 将不同模型的元素组合到一起可能引发的两个问题:
        1. 重复的概念:重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念。
        2. 假同源:假同源是指使用相同的术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。
  2. 模式:continuous integration(持续集成)
    1. Continuous integration是指把一个上下文中的所有工作足够频繁地合并在一起,并使他们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。
    2. Continuous integration的两个级别的操作:
      • 模型概念的集成
      • 实现的集成
    3. 集成过程的有效特征:
      • 分步集成,采用可重现的结合/构建技术
      • 自动测试套件
      • 有一些规则,用来为那些尚未集成的改动设置一个相当小的生命周期上限
      • 在讨论模型和应用程序时要坚持使用ubiquitous language。
    4. 建立一个把所有代码和其他实现工作频繁结合到以前的过程,并通过自动化测试来快速查明模型的分裂问题。严格的使用ubiquitous language,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。
  3. 模式:context map(上下文图)
    1. Bounded context之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转化去实现。
    2. Context map位于项目管理和软件设计的重叠部分。
    3. ①识别在项目中起作用的每个模块,并定义其bounded context。这包括非面向对象子系统的隐含模型。为每个bounded context命名,并把名称添加到ubiquitous language。②描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。③先将当前的情况米哦啊输出来。以后再做改变。
    4. 测试context的边界
      • 这些测试有助于解决转换时所存在的一些席位问题以及弥补边界沟通上存在的不足。
    5. Context map的组织和文档化
      • Bounded context应该有名称,以便可以讨论他们。这些名称应该被团队添加到团队的ubiquitous language中。
      • 每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的context,或任何情况的context。
  4. Bounded context之间的关系
    1. 以下模式的目的:
      • 为成功地组织开发工作设定目标
      • 为描述现有组织提供术语
    2. 区别:区别包括你对另一个模型的控制程度、两个团队之间合作水平和合作类型,以及特征和数据的集成程度。
  5. 模式:shared kernel(共享内核)
    1. 从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集之外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没于另一个团队商量的情况下不应该擅自更改它。
    2. 功能系统要经常进行集成,但集成频率应该比团队中continuous integration的频率低一些。在进行这些集成的时候,两个团队都要运行测试。
    3. 使用shared kernel的目的是减少重复(并不是消除重复,因为只有在一个bounded context中才能消除重复),并使两个子系统之间的集成变得相对容易一些。
  6. 模式:customer/supplier development team(客户/供应商开发团队)
    1. 在两个团队之间建立一种明确地客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
    2. 两个团队共同开发自动化验收测试,用来验证预期接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。
    3. 在迭代期间,下游团队成员应该像传统的客户一样随时回答上游团队的提问,并帮助解决问题。
    4. 这种模式的两个关键要素:
      • 关系必须是客户与供应商的关系,其中客户的需求至关重要。
      • 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不总是密切关注上游团队的行动。
  7. 模式:conformist 跟随者
    1. 通过严格遵从上游团队的模型,可以消除在bounded context之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择conformist模式可以极大地简化集成。此外,这样还可以与供应商团队共享ubiquitous language。供应商处于统治地位,因此最好使沟通变得容易。他们从利他主义的角度出发,会与你分享信息。
    2. 这个决策会加深你对上游团队的依赖,同时你的应用也会受限于上游模型的功能,充其量也只能做一些简单的增强而已。
    3. Shared kernel是两个高度协调的团队之间的合作模式,而conformist模式则是应对与一个对合作不感兴趣的团队进行集成。
  8. 模式:anticorruption layer(防御层)
    1. 创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统做出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。
    2. Anticorruption layer并不是向另一个系统发送消息的机制,相反,它是在不同模型和协议之间转换概念对象和操作的机制。
    3. 设计anticorruption layer的接口:anticorruption layer的公共接口通常是以一组service的形式出现,但偶尔也会采用entity的形式。
    4. 实现anticorruption layer:对anticorruption layer设计进行组织的一种方法是把它实现为Facade、adapter或者转换器的组合,外加两个系统之间进行对话所需的通信和传输机制。
    5. anticorruption layer是连接两个bounded context的一种方式。
  9. 模式:separate way(各行其道)
    1. 我们必须严格划定需求的范围。如果两组功能之间的关系并非必不可少,那么二者完全可以彼此独立。
    2. 声明一个与其他上下文无关联的bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案。
  10. 模式:open host service(开放主机服务)
    1. 定义一个协议,把你的子系统作为一组service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
  11. 模式:published language(公共发布的语言)
    1. 把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
  12. “大象”的统一
    1. 承认多个互相冲突的领域模型实际上正是面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚的看到要在两个模型之间创建的任何特殊接口的含义。
  13. 选择你的模型上下文策略
    1. 团队策略或更高层策略
      • 在决定是否扩展和分割bounded context时,应该权衡团队独立工作的价值以及能产生直接且丰富集成的价值,以这两种价值的成本-效益作为决策的依据。
      • 在实践中,团队之间的行政关系往往决定了系统的集成方式。
    2. 置身上下文中
      • 要知道自己处于的上下文,并且在超出该context map的应用边界时能够意识到已越界。
    3. 转换边界
      • 首选较大的bounded context
        1. 当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅。
        2. 一个内聚模型比两个不同模型再加他们之间的映射更容易理解。
        3. 两个模型之间的转换可能会很难(有时甚至是不可能)。
        4. 共享语言可以使团队沟通起来更清楚。
      • 首选较小的bounded context
        1. 开发人员之间的沟通开销减少了
        2. 由于团队和代码规模较小,continuous integration更容易。
        3. 较大上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺。
        4. 在不同的模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和ubiquitouslanguage的专门术语包括进来。
      • 在一个模型中,只有那些能够严格按照另一个模型来表述的部分才能够进行集成。
      • 当两个系统之间有一个很小的接口时,集成是有意义的。
    4. 接受那些我们无法更改的事物:描述外部系统
      • 定义bounded context的目的是把模型统一在特定边界之内。
    5. 与外部系统关系
      • 可以使用:
        1. Separate way模式
        2. Conformist模式
        3. Anticorruption layer模式
    6. 设计中的系统
      • 一般来说,每个bounded context对应一个团队。
    7. 用不同模型满足特殊需求
      • 有时会出现一个深层次的模型,他把这些不同语言统一起来,并能够满足双方的要求。
    8. 部署
      • 绘制context边界时应该反映出部署计划的可行性。
      • 当两个context通过一个转换层连接时,想要更新其中的一个context,新的转换层需要为另一个context提供相同的接口。
    9. 权衡
      • 无缝功能集成的益处和额外的协调沟通工作之间的权衡。
      • 更独立的操作与更顺畅的沟通之间的权衡。
    10. 当项目正在进行时
      • 根据当前的状况来定义bounded context
      • 围绕当前组织结构来加强团队的工作
      • 考虑修改边界和他们的关系
  14. 转换
    1. 合并context:separate → shared kernel
      • 评估初始状况
      • 建立合并过程
      • 选择某个小的子领域作为开始,它应该是两个context中重复出现的子领域,但不是core domain的一部分。
      • 从两个团队中选出2 - 4位开发人员组成小组,由他们来为子领域开发一个共享模型。
      • 来自两个团队的开发人员一起负责实现模型(或修改要共享的现有代码)、确定各种细节并使模型开始工作。
      • 每个团队的开发人员都承担与心得sharedkernel集成的任务。
      • 清除那些不再需要的翻译。
    2. 合并context:shared kernel → continuous integration
      • 确保每个团队都已经建立了continuous integration所需的所有过程(共享代码所有权、频繁集成等)。
      • 团队成员在团队之间流动。
      • 澄清每个模型的精髓。
      • 把核心领域合并到shared kernel中。
      • 随着shared kernel的增长,把集成频率提到到每天一次,最后实现continuous integration。
      • 当shared kernel逐渐把先前两个bounded context的所有内容都包括进来的时候,你会发现要么形成了一个大的团队,要么形成了两个较小的团队,这两个较小的团队共享一个continuous integration的代码库,而且团队成员可以经常在两个团队之间来回流动。
    3. 逐步淘汰遗留系统
      • 在任何一次迭代中:
        1. 确定遗留系统的哪个功能可以再一次迭代中被添加到某个新系统中。
        2. 确定需要在anticorruption layer中添加的功能
        3. 实现
        4. 部署
        5. 找出anticorruption layer中那些不必要的部分,并去掉它们
        6. 考虑删除遗留系统中目前未被使用的模块,虽然这种做法未必实际。
    4. Open host service → published language
      • 如果有一种行业标准语言可用,则尽可能评估并使用它
      • 如果没有标准语言或预先公开发布的语言,则完善作为host的系统的core domain
      • 使用core domain作为交换语言的基础,尽可能使用像xml这样的标准交互范式
      • (至少)向所有参与协作的各方发布新语言
      • 如果涉及新的架构,那么也要发布它
      • 为每个协作系统构建转换层
      • 切换
  • 精炼

领域模型的战略精炼包括以下部分:

  1. 帮助所有团队成员掌握系统的总体设计以及各个部分如何协调工作
  2. 找到一个具有适度规模的核心模型并把它添加到通用语言中,从而促进沟通
  3. 指导重构
  4. 专注于模型中最具有价值的那部分
  5. 指导外包、现成组件的使用以及任务委派
  1. 模式:core domain(核心领域)
    1. 在制定项目规划的时候,必须把资源分配给模型和设计中最关键的部分。
    2. 选择核心
      • 我们需要关注的是那些能够表示业务领域并解决业务问题的模型部分。
      • 对core domain的选择取决于看问题的角度。
    3. 工作分配
      • 一个良好的框架可能会提供满足你的专门使用需求的高水平抽象,他可以节省开发那些更通用部分的时间,并使你能够专注于core。
  2. 模式:generic subdomain(通用子领域)
    1. 识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的module中。任何专有的东西都不应放到这些模块中。
    2. 当开发这样软件包的集中选择的优劣比较。

现成的解决方案

优点:

  1. 可以减少代码的开发
  2. 维护负担转移到了外部
  3. 代码已经在很多地方使用过,可能较为成熟,因此比自己开发的代码更可靠和完备

缺点:

  1. 在使用之前,仍然需要花时间来评估和理解它
  2. 就业内目前的质量控制水平而言,无法保证它的正确性和稳定性。
  3. 他可能设计的过于细致了(远远超过你的目的),集成的工作量可能比开发一个最小化的内部实现更大
  4. 外部元素的集成常常不顺利。他可能有一个与你的项目完全不同的bounded context。即使不是这样,他也很难顺利地引用你的其他软件包中的entity。
  5. 他可能会引入对平台、编译器版本的依赖等。

公共发布的设计或模型

优点:

  1. 比自己开发的模型更为成熟,并且反映了很多人的深层知识。
  2. 提供了随时可用的高质量文档

缺点:

1.可能不是很符合你的需求,或者设计的过于细致了(远远超出了你的需求)。

把实现外包出去

优点:

  1. 使核心团队可以脱身去处理core domain,那才是最需要知识和经验积累的部分。
  2. 开发工作的增加不会使团队规模无限扩大下去,同时又不会导致core domain知识的分散。
  3. 强制团队采用面向接口的设计,并且有助于保持子领域的通用性,因为规格已被传递到外部。

缺点:

  1. 仍需要核心团队花费一些时间,因为他们需要与外包人员商量接口、编码标准和其他重要方面。
  2. 当把代码移交回团队时,团队仍需要耗费大量精力来理解这些代码(但是这个开销比理解专用领域要小一些,因为通用子领域不需要理解专门的背景知识)。
  3. 代码质量或高或低,这取决于两个团队能力的高低。

内部实现

优点:

  1. 易于集成
  2. 只开发自己需要的,不做多余的工作
  3. 可以临时把工作分包出去

缺点:

  1. 需要承诺后续的维和和培训负担
  2. 很容易低估开发这些软件包所需的时间和成本

 

    1. 通用不等于可重用
      • 我们应该尽可能把大部分精力投入到core domain工作中,而只在必要的时候才在支持性的generic subdomain中投入工作。
      • 尽管我们很少需要考虑设计的可重用性,但是通用子领域的设计必须严格的限定在通用概念的范围之内。
    2. 项目风险管理
      • 除非团队拥有精湛的技术并且对领域非常熟悉,否则第一个雏形系统应该以core domain的某个部分作为基础,不管他有多么简单。
      • Core domain就是高风险的,因为它的难度往往会超出我们的预料,而且如果没有它,项目就不可能获得成功。
  1. 模式:domain vision statement(领域愿景说明)
    1. 最好的愿景说明会展示出应用程序为组织带来的价值。
    2. 领域愿景说明关注的重点是领域模型的本质,以及如何为企业带来价值。在项目开发的所有阶段,管理层和技术人员都可以直接用领域愿景说明来指导资源分配、建模选择和团队成员培训。
    3. 写一份core domain的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
    4. Domain vision statement可以用作一个指南,它帮助开发团队在精炼模型和代码的过程中保持统一方向。
  2. 模式:highlighted core(突出核心)
    1. 对代码所做的重大结构性改动是识别core domain的理想方式。
    2. 精炼文档
      • 精炼文档并不是完备的设计文档。他只是一个最简单的切入点,描述并解释了核心,并给出了更进一步研究这些核心部分的理由。精炼文档为读者提供了一个总体视图,指出了各个部分是如何组合到一起的,并且指导读者到相应的代码部分寻找更多细节。
      • 编写一个非常简短的文档(3 - 7页,每页内容不必太多),用于描述core domain以及core元素之间的主要交互过程。
    3. 标明core
      • 把模型的主要存储中的core domain标记出来,不用特意去阐明其角色。使开发人员很容易就知道什么在核心内,什么在核心外。
    4. 把精炼文档作为过程工具
      • 把精炼文档作为一个指南。
  3. 模式:cohesive mechanism(内聚机制)
    1. 把概念上的cohesive mechanism分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个intention-revealing interface来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
    2. Core domain或generic subdomain的模型描述的是事实、规则或问题。而cohesive mechanism则用来满足规则或者用来完成模型指定的计算。
    3. Generic subdomain与cohesive mechanism的比较
      • Generic subdomain是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与core domain没什么区别,只是重要性和专门程度较低而已。Cohesive mechanism并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。
      • 模型提出问题,cohesive mechanism解决问题。
    4. Mechanism是core domain的一部分。
  4. 通过精炼得到声明式风格
    1. 精炼的价值在于使你能够看到自己正在做什么,不让无关细节分散你的注意力,并通过不断削弱得到核心。
    2. Cohesive mechanism用途最大的地方是它通过一个intention-revealing interface来提供访问,并且具有概念上一致的assertion和side-effect-free function。
  5. 模式:segregated core(分离核心)
    1. 对模型进行重构,把核心概念从支持性元素(包括定义的不清楚的那些元素)中分离出来,并增强core的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中——即使这会把一些紧密耦合的元素分开。
    2. 通过重构得到segregated core的一般步骤:
      • 识别出一个core子领域(可能是从精炼文档中得到的)。
      • 把相关的类移到新的module中,并根据与这些类有关的概念为模块命名。
      • 对代码进行重构,把那些不直接表示概念的数据和功能分离出来。
      • 对新的segregate core module进行重构,使其中的关系和交互变得简单、表达的更清楚,并且最大限度地减少并澄清它与其他module的关系(这将是一个持续进行的重构目标)。
      • 对另一个core子领域重复这个过程,直到完成segregate core的工作。
    3. 创建segregate core的代价
      • 企业软件的最大价值来自于模型中企业的那些特有方面。
    4. 不断演变的团队决策
      • 困难之处在于既要约束每个人使其都使用相同的core定义,又不能一成不变地去执行这个决策。
      • 决策必须具有足够的敏捷性,可反复的纠正。团队必须进行有效的沟通,以便使每个人都共享同一个core视图。
  6. 模式:abstract core(抽象核心)
    1. 把模型中最基础的概念识别出来,并分散到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的module中,而专用的、详细的实现类则留在由子领域定义的module中。
    2. Abstract core提供了主要概念及其交互的简化图。
    3. 对abstract core进行建模需要深入理解关键概念以及它们在系统的主要交互中扮演的角色。换言之,它是通过重构得到更深层次的理解的。
  7. 深层模型精炼
    1. 精炼的目标是把模型设计的更明显,使我们可以用模型简单地把领域表示出来。
    2. 深层模型把领域中最本质的方面精炼成一些简单的元素,使我们可以把这些元素组合起来解决应用程序中的重要问题。
    3. 尽管任何带来深层模型的突破都有价值,但只是core domain中的突破才能改变整个项目的轨道。
  8. 选择重构目标
    1. 如果采用“哪痛治哪”这种重构策略,要观察一下根源问题是否涉及core domain或core与支持元素的关系。如果确实涉及,那么就要接受挑战,首先修复核心。
    2. 当可以自由选择重构的部分时,应首先集中精力把core domain更好地提取出来,完善对core的分离,并且把支持性的子领域提炼成通用子领域。
  • 大型结构
  1. 严格划分bounded context可能会防止出现破坏和混淆,但其本身对于整体上审视系统并无任何益处。
  2. “大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则(或两者兼有)来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,他还能够协调不同人员工作,因为它提供了共享的整体视图,让人们指导各个部分在整体中的角色。
  3. 好的结构可以帮助人们深入理解模型,还能够对精炼起到补充作用。
  1. 模式:evolving order(演进顺序)
    1. 让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依次过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。
    2. 在选择大型结构时,应该侧重于整体模型的管理,而不是优化个别部分的结构。
    3. 根据实际经验和领域知识来选择结构,并避免采用限制过多的结构,如此可以降低折中(“结构统一”与“用最自然的方式表示个别组件”)的难度。
    4. 真正适合领域和需求的结构能偶使细节的建模和设计变得更容易,因为它快速排除了很多选项。
    5. 应该为那些在不同context中工作的开发团队保留一定的自由,允许他们为了满足局部需要而修改模型。
    6. 当发现一种大型结构可以明显使系统变得更清晰,而有没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不是使用它,因为最好不要为了追求设计的完整性而勉强去使用一种结构,而应该找到尽可能精简的方式解决所出现的问题。要记住宁缺毋滥的原则。
  2. 模式:system metaphor(系统隐喻)
    1. System metaphor是一种松散的、易于理解的大型结构,它与对象范式是协调的。
    2. 系统隐喻是对领域的一种类比。
    3. 当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。围绕这个隐喻来组织设计,并把它吸收到ubiquitous language中。System metaphor应该既能促进系统的交流,又能指导系统的开发,它可以增加系统不同部分之间的一致性,甚至可以跨越不同的bounded context。但所有隐喻都不是完全精确的,因此应不断检查隐喻是否过度或不恰当,当发现它起到妨碍作用时,要随时准备放弃它。
  3. 模式:responsibility layer(职责层)
    1. 所谓的层,就是对系统进行划分,每个层的元素都知道或能够使用它“下面”的那些层的服务,但却不知道它“上面”的层,而且与它上面的层保持独立。
    2. 分层:
      • “作业”职责:公司的活动,无论是过去、现在还是计划的活动,都被组织到“作业”层中。
      • “能力职责”:这个层反映了公司在执行作业时所能利用的资源。
      • “决策支持”职责层:软件的这个层为用户提供了用于制定计划和决策的工具,它具有自动制定一些决策的潜能。
    3. 选择适当的层:
      • 指导方针:
        1. 场景描述
        2. 概念依赖性
        3. Conceptual contour(概念轮廓)
      • 通用分层:
        1. 潜能层:关心能够做什么
        2. 作业层:关心正在做什么
        3. 决策支持层:用来作出分析和制定决策
        4. 策略层:规则和目标
        5. 承诺层:具有策略层的性质,它表述了一些知道未来运营的目标;但也有作业层的性质,因为承诺是作为后续业务活动的一部分而出现和变化的
  4. 模式:knowledge level(知识级别)
    1. “knowledge level”是一组描述了另一组对象应该有哪些行为的对象。
    2. Reflection模式能够使软件具有“自我感知”的特性,并使所选中的结构和行为可以接受调整和修改,从而满足变化需求。
      • 划分为:
        1. 基础级别(base level):承担应用程序的操作职责
        2. 元级别(meta level):表示有关软件结构和行为方面的知识。
    3. Knowledge level具有两个特征:
      • 它关注的是应用领域
      • 它并不追求完全的通用性
    4. 创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。
  5. 模式:pluggable component framework(可插入式组件框架)
    1. 从接口和交互中提炼出一个abstract core,并创建一个框架,这个框架要允许这些接口的各种不同实现被自由替换。同样,无论是什么应用程序,只要它严格地通过abstract core接口进行操作,那么就可以允许它使用这些组件。
    2. pluggable component framework缺点:
      • 需要高精度的接口设计和一个非常深入的模型
      • 只为应用程序提供了有限的选择
    3. 要想设计一个有用的框架,必须要有成熟的理解。
    4. pluggable component framework不适合作为项目的第一个大比例结构,也不适合作为第二个。最成功的例子都是在完全开发出多个专门应用之后才采用这种结构。
  6. 结构应该有一种什么样的约束
    1. 不要滥用框架和死板的实现大比例结构。大比例结构的最重要的贡献在于它具有概念上的一致性,并帮助我们更深入的理解领域
    2. 每条结构规则都应该使开发变得更容易实现。
  7. 通过重构得到更适当的结构
    1. 最小化
      • 控制成本的一个关键是保持一种简单、轻量级的结构。不要试图面面俱到。只需解决最主要的问题即可,其他问题可以留到后面一个一个解决。
    2. 沟通和自律
      • 整个团队在新的开发和重构中必须遵守结构。
    3. 同构重构得到柔性设计
    4. 通过精炼可以减轻负担
  • 领域驱动设计的综合运用
  1. 将大型结构与精炼结合起来使用
    1. 大型结构和精炼的概念互为补充。
    2. 大型结构可以帮助解释core domain内部的关系以及generic subdomain之间的关系。
    3. 大型结构本身可能是core domain的一个重要部分。
  2. 首先评估
    1. 画出context map
    2. 注意项目上的语言使用
    3. 理解重点所在
    4. 项目所采用的技术是否遵循model-driven design
    5. 团队开发人员是否具备必要技能
    6. 开发人员是否了解领域知识、是否对领域感兴趣
  3. 由谁制定策略
    1. 从应用程序开发自动得出的结构
      • 适用情况:团队数目相对较少,各个团队之间能够一致地保持彼此协调,他们的设计能力大致相同,而且他们的结构需求基本一致,可以通过同一种大型结构来满足
    2. 以客户为中心的架构团队
  4. 制定战略设计决策的6个要点
    1. 决策必须传达到整个团队
    2. 决策过程必须收集反馈意见
    3. 计划必须允许演变
    4. 架构团队不必把所有最好、最聪明的人员都吸收进来
    5. 战略设计需要遵守简约和谦逊的原则
    6. 对象的职责要专一,而开发人员应该是多面手
  5. 技术框架同样如此
    1. 不要编写“傻瓜式”框架
      • 人员在设计方面不够聪明,就不应该让他们来开发软件
    2. 注意总体规划

 

结束语

  1. 检验软件成功与否的是最有效的方法是让他运行一段时间。
  2. 如果人们赖以工作的一个系统封闭起来,那么它将会变为一项永久的、谁也不敢碰的遗留资产。
  3. 理解目标领域并将学到的知识融合到软件中。
  4. 创建好的软件是一项需要徐诶和思考的活动。

你可能感兴趣的:(领域的驱动设计 读后日志)