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


本书中的软件风格主要遵循”职责驱动设计“的原则。

第4章 分离领域

模式:LAYERED ARCHITECTURE(分层架构)

在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。

如果与领域相关的代码分散在大量的其他代码中,那么查看、分析以及修改领域代码就会变得异常困难。

分离关注点:

使设计中的各个部分都得到单独的关注

分层的价值在于每一层都只代表程序中的某一个特定的方面,使每个方面的设计更具有内聚性,更容易理解,并且只依赖于他的下层,层与层之间是松散链接的,并且只能是单向的。
请添加图片描述

职责
用户界面层负责展示信息和接受用户动作
应用层定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。
这一层对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。
应用层要尽量简单,不包含业务规则,只是为下一层中的领域对象协调任务、分配工作,使他们相互协作。
他没有反映业务情况的状态,但是却可以具有另一种状态,为用户或程序显示某个任务的进度。
领域层负责表达业务模型概念,业务状态信息以及业务规则。是业务软件的核心。
将领域实现独立出来是领域驱动设计的前提
基础设施层为上面各层提供通用的技术能力:为应用层传递消息、为领域层提供持久化机制、为用户界面层绘制屏幕组件等

模式:THE SMART UI(智能用户界面)反模式

这种模式是不被推荐的

智能用户界面的实现是,在用户界面中实现所有的业务逻辑。将应用程序分成晓得功能模块,分别将他们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据库存储。使用自动化程度最高的用户界面创建工具和可用的可视化编辑工具。

优点:

  • 效率高
  • 能力要求低
  • 程序之间彼此独立
  • 可以很顺利利用关系数据库,能够提供数据级的整合

缺点:

  • 不通过数据库很难集成应用程序
  • 没有对行为的重用,没有业务问题的抽象
  • 抽象的缺乏限制了重构的选择
  • 复杂的功能很快让你无所适从

第5章 软件中所表示的模型

区分标识模型的3种模型元素模式:ENITITY(实体)、VALUE OBJECT(值对象)、SERVICE(服务)。

实体和值对象的根本区别

一个对象是用来表示具有连续性和标识的事务呢,还是用于描述某种状态的属性呢?这是实体和值对象的根本区别

领域中还有一些方面适合用动作或操作表示,这比用对象表示更加清楚。服务是对客户端请求来完成某事,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项服务。

关联

模型中每个可遍历的关联,软件中都要有同样属性的机制。越多的关联使实现和维护变得很复杂,使得关系的本质变得模糊混乱。

3种控制关联的方法:

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

例:

美国有很多总统,国家和总统之间是一个双向的、一对多的关系(左图)。

从实用角度讲,可以将这种关系简化为从国家到总统的单项设计(中图)。

通过限定,一个国家在一段时间只有以为总统,变成了一对一的关系(右图)。

请添加图片描述

最终得到了一个单向的、一对一的关联。

模式:ENTITY(实体)

很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。当一个对象由标识区分时,那么在模型中应该主要通过标识来确定该对象的定义,并集中关注生命周期的连续性和标识。

每个实体都必须有一种建立标识的操作方式,以便区分和其他的对象,即使其他对象包含了相同的属性。

当对象属性没办法形成唯一键值时,另一种经常用的的解决方案是为每个实例附加一个全局唯一的ID,并且一般是不可变的。

模式:VALUE OBJECT(值对象)

值对象指的是用于描述领域的某个方面而本身没有概念标识的对象为值对象,这些元素我们只关心他是什么,而不关心他是谁。

当我们只关心一个模型元素的属性时,应该把它归类为值对象。我们应该使这个模型元素能够表达出其属性的意义,并为他提供相关功能。

值对象应该是不可变的,不要为他分配任何标识,而且不要设计的特别复杂。

值对象为性能优化提供了更多选择,复制和共享哪个更划算取决于实现环境。复制可能会导致系统包含大量的对象,但还共享可能会减慢分布式系统的速度。

以下几种情况最好使用共享:

  • 节省数据库空间或减少对象数量是一个关键要求时
  • 通信开销很低时
  • 共享的对象被严格的向定位不可变时

只要值对象是不可变的,变更管理就会很简单,以为除了整体替换之外没有其他的更改,不变的值对象可以自有的共享。

何时允许可变性?

有些情况下处于性能考虑,仍需要让值对象可变

  • 如果值频繁改变
  • 如果创建和删除对象的开销很大
  • 如果替换将打乱集群
  • 如果值共享的不多,或者共享不会提高性能

可变的值对象不能共享

值对象对数据库的影响:

  • 如果通过复制的方式,存储值对象,那么将会浪费空间,但是读取速度会比较快(用空间换时间)。
  • 如果通过共享的方式,那么就需要关联其他的表才能够取到所有数据,节省空间。(用时间换空间)。

模式:SERVICE(服务)

有时候对象不是一个事物,而是一些活动或动作。他们没有自己的状态,而且除了所承载的操作之外在领域中也没有其他意义。这些领域概念不适合创建为对象。

SERVICE是为接口提供的一种操作,他在模型中是独立的,不想实体和值对象那样具有封装的状态。定义了能够为客户做什么。

好的SERVICE有以下3个特征:

  • 与领域概念相关的操作不是实体或者值对象的一个自然组成部分
  • 接口是根据领域模型的其他元素定义的
  • 操作是无状态的

很多领域或应用层SERVICE是在ENITITY和VALUE OBJECT的基础上建立起来的,他们的行为类似于将领域的一些潜在的功能组织起来以执行某种任务的脚本。ENITY和VALUE OBJECT往往由于粒度过细而无法提供对领域层功能的便捷访问。

SERVICE应该在各个层中。

SERVICE的粒度,在大型系统中,中等粒度的,无状态的SERVICE更容易被复用,因为他们在简单的接口背后封装了重要的功能,细粒度的对象可能导致分布式消息传递的效率低。

应用层负责对领域对象的行为进行调用,因此细粒度的领域对象可能会把领域知识泄漏到应用层中。这产生的结果就是应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码中,而领域层会失去这些知识。**明智的引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。

模式:MODULE/PACKAGE(包)

MODULE为人们提供了两种观察模式的方式

  • 在MODULE中查看细节,而不会被整个模型淹没
  • 观察MODULE之间的关系,而不考虑内部细节

MODULE之间应该是低耦合的,MODULE内部应该是高内聚的。MODULE并不仅仅是代码的划分,而且也是概念的划分。

选择能够描述系统的MODULE,并使之包含一个内距的概念集合,这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之的耦合,或者找到一个可以作为MODULE的基础的概念,基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立的理解和分析这些概念。对模型进行静华,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。

除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念的所有对象的所有代码都放到同一个模型中。并且利用打包将领域层从其他代码中分离出来,否则,就尽可能让领域开发人员自有的决定领域对象的打包方式,以便支持他们的模型和设计选择。

第6章 领域对象的声明周期

请添加图片描述

图:领域对象的声明周期

每个对象都有生命周期。对象创建后,可能会经历各种不同的状态,直至最终消亡。

这个过程中主要的挑战有两个:

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

3种解决这些问题的方法:

  • AGGREGATE(聚合):定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。
  • FACTORY(工厂):使用工厂来创建和重建对象独享和AGGREGATE(聚合),从而封装他们的内部结构。
  • REPOSITORY(存储库):在生命周期的中间或和末尾使用存储库来提供查找和检索持久化对象并封装庞大的基础设施的手段

使用AGGREGATE进行建模,并且在设计中将和使用FACTORY和REPOSITORY,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统的操作他们。AGGREGATE可以划分出一个范围,这个范围内的模型元素的生命周期各个阶段都应该维护其固定规则。FACTORY和REPOSITORY在AGGREGATE的基础上进行操作,将特定的生命周期转换的复杂性封装起来。

模式:AGGREGATE(组合)

我们该如何直到一个有其他对象组成的对象从哪里开始,又到何处结束呢?

在具有复杂关系的模型中,要想保证对象更改的一致性是很困难的。不仅互不关关联的对象要遵守一些固定规则,而且紧密关联的各族对象也要遵守一些固定规则。

在任何具有持久化数据存储的系统中,对数据修改的事务必须要有范围,而且要有保持一致的机制。尽管这看上去像是数据库事务方面的问题,但它的根源在于模型,归根到底是由于模型中缺乏明确定义的边界

AGGREGATE就是一组相关对象的集合,我们把它看做是数据修改的单元。每个AGGREGATE都有一个根(root)和一个边界(boundary)。边界定义了AGGREGATE的内部都有什么。对外部而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他ENITITY都有本地标识,但这些标识只在AGGREGATE内部才需要加以区别,因为外部对象除了根以外看不到其他对象。

固定规则

指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求每时每刻保持最新状态。通过事件处理、批处理、其他更新机制,这些以来会在一定时间内得以解决。但在每个事务完成时,AGGREGATE内部所应用的固定规则必须得到满足。

为了实现概念上的AGGREGATE,需要对所有事务应用一组规则:

  • 根ENTITY具有全局标识,并最终负责检查固定规则
  • 根ENTITY具有全局标识,边界内的ENTITY具有本地标识,这些标识不必是全局唯一标识
  • AGGREGETE外部对象不可引用出根ENTITY之外的任何内部对象。根ENTITY可以把内部ENTITY对象引用传递给他们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个值对象传递给另一个对象,而不必关心他们发生什么
  • 只有AGGREGATE的根才能直接通过数据库查询获取,所有其他对象必须通过比遍历关联关系来发现
  • AGGREGATE内部的对象可以保持对其他AGGREGATE根的引用
  • 删除操作必须一次性删除AGGREGATE边界内的所有对象
  • 当提交修改时,整个AGGREGATE的所有固定规则都必须被满足

我们应该将ENTITY和值对象分门别类的聚集到AGGREGATE中,并定义每个AGGREGATE的边界。在每个AGGREGATE中,选择一个AGGREGATE作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象对根的引用。对内部对象的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于渠道包AGGREGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个整体满足固定规则。

示例:发动机上面都刻有序列号,可以独立于汽车被跟踪

请添加图片描述

红色线:AGGREGATE边界外部的对象不能够引用Tire,因为他是内部的

模式:FACTORY

当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用FACTORY进行封装。

对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在他的生命周期中要承载大量职责。

如果再让复杂对象负责自身的创作,那么职责过载将会导致问题。

对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作,将这些职责混在一起,可能产生难以理解的拙劣设计,让客户直接负责创建对象又会使客户陷入混乱,并且破坏被装配对象或组合的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

应该将创建复杂对象的实例和AGGREGATE的职责转一个单独的对象,这个对象本身可能没有承担模型中的职责,但是他是领域设计的一部分。提供了一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。

FACTORY提供了一种更加抽象且不与其他对象发生耦合的构造机制。

请添加图片描述

工厂的两个基本要求:

  • 每个创建方法都是原子的,而且要保证被创建对象或AGGREGATE的所有固定规则
  • FACTORY应该被抽象为所需的具体类型,而不是要创建的具体类

FACTORY以及应用位置:

  • 在一个已经存在的AGGREGATE添加元素,可以在AGGREGATE的根上创建一个工厂方法。这样可以将AGGREGATE内部实现细节隐藏起来,同时使根负责确保AGGREGATE在添加元素时的完整性
  • 在一个对象上使用工厂方法,这个对象不拥有生成的对象。当一个对象的创建主要使用另一个对象的数据时,则可以在后者对象上创建一个工厂方法。
  • 创建一个专用的工厂对象,工厂负责把对根的引用传递出去,并确保创建出的AGGREGATE满足固定规则

有些情况下只需使用构造函数:

  • 类是一种类型。他不是任何相关层次的一部分,而且一般没有通过接口实现多态性
  • 客户关心的是实现,可能将其作为选择策略的一种方式
  • 客户可以访问对象的所有属性
  • 构造并不复杂

模式:REPOSITORY(存储库)

数据库搜索是全局可访问的,它使我们可以直接访问任何对象。当客户代码直接使用数据库时,开发人员会试图绕过模型的封装,而直接获取和操作他们所需的数据。这将导致越来越多的领域规则被嵌入到查询代码中,或者干脆丢失了。

客户需要一种有效的方式来获取对已经存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关系,这会使模型变得非常混乱。另一方面,开发人员可能使用产村从数据库中提取他们所需的数据,或者直接提取具体的对象,而不是通过AGGREGATE根来得到这些对象。这样会导致领域逻辑进入查询和客户代码中,而实体和值对象则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快会使客户代码混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些AGGREGATE根的时候,就需要使用这种访问方式。它们通常是ENITITY,有时具有复杂内部结构的VALUE OBJECT,还可能是枚举VALUE。而其他对象则不宜适用这种方式,因为这会混乱他们之间的重要区别。这些对象除了通过根来遍历查找这种方式外,应禁止使用其他方法对AGGREGATE内部的任何对象进行访问。随意的数据库查询会破坏对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。

REPOSITORY将某种类型的所有对象表示为一个概念集合。他的行为类似于集合,只是具有更复杂的查询功能。再添加和删除相应类型的对象时,REPOSITORY的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密关联的职责集中在一起,这些职责提供了对AGGREGATE根的整个生命周期的全局访问。

为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的替身。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入和删除的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合,从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。

优点:

  • 为客户提供了一个简单的模型,用来获取持久化对象并管理他们的生命周期
  • 他们使应用程序设计和持久化技术解耦
  • 他们体现了有关对象访问的设计决策
  • 他们很容易将他们替换为哑实现,以便在测试中使用

请添加图片描述

示例:

请添加图片描述

注意事项:

  • 对类型进行抽象,并不意味着每个类都有一个REPOSITORY。类型可以是一个接口
  • 充分利用与客户解耦的优点。我们可以很容易的修改REPOSITORY的实现,也可以利用解耦来优化性能,因为这样可以使用不同的查询技术,或在内存中缓存对象,可以随时切换持久化策略。
  • 将事务的控制权留给客户。尽管REPOSITORY会执行数据库的插入和删除操作,但它通常不会提交事务。

REPOSITORY与FACTORY的关系

REPOSITORY可以委托FACTORY来重建一个已经存在的对象

请添加图片描述

FACTORY的工作是用数据库来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道在创建完成之后,应该把他添加到REPOSITORY中,由REPOSITORY来封装对象在数据库中的存储。

请添加图片描述

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

货物运输系统简介

最初需求包含三个基本功能

  1. 跟踪客户货物的主要处理
  2. 事先预约货物
  3. 当货物到达其处理流程中的某个位置时,自动向客户寄送发票
    请添加图片描述

对象的定义:

  • Handling Event:处理事件,对Cargo采取的操作,如装船、卸货、入关等
  • Deliver Specification:运送规格,定义了运送目标,至少包含目的地和到达时间
  • Customer:客户,分为不同的角色,入托运人、收货人、付款人等
  • Carrier Movement:表示一辆卡车或者船,从一个地点到另一个地点
  • Delivery History:运送历史,反映了Cargo实际上发生了什么事情

隔离领域:引入应用层

为了防止领域层的职责与系统的其他部分混杂在一起,通过分析,识别出用户级别的应用程序功能:

  1. TrackingQuery(跟踪查询),访问某个Cargo过去和现在的处理情况
  2. Booking Application(预定应用),允许注册一个新的Cargo,并使系统处理他
  3. Incident Logging Application(事件日志应用),记录对Cargo的名词词处理

设计运输领域中的关联

双向关联在设计中容易产生问题。遍历方向常常反映了对领域的洞悉,是模型得以深化

AGGREGATE边界

Customer、Location、Carrier Movement都有自己的标识,且被许多Cargo共享,因此他们都在各自的AGGREGATE中必须是根。

选择REPOSITORY

在AGGREGATE内部的对象一般不需要添加REPOSITORY,因为这部分对象往往都是通过AGGREGATE的根进行比那里的,不能直访问。

Customer:需要

Location:需要

Handling Event:可以有

Delivery History:不需要,在AGGREGATE内部,和Cargo关联

Carrier Movement:需要

停一下,Cargo Aggregate的另一种设计

由于添加Handling Event时需要更新Delivery History,而更新Delivery History会在实务中牵涉Cargo AGGREGATE。因此,如果同一时间其他用户正在修改Cargo,那么Handlind Event事务将会失败或延迟。

我们在Delivery History中可以不使用Handling Event集合,而是用一个查询来替代它,这样在添加Handling Event时就不会再其自己的AGGREGATE之外引起任何完整性问题。

为了使用查询,我们可以为Handling Event增加一个REPOSITORY。

引入新需求:配额检查

为了避免预定量不足或超订、或者利润较低,Sales Management System规定各种类型的货物的预定量。

请添加图片描述

如何连接两个系统:

销售管理系统是一个外部系统,不能直接对接。

我们需要创建一个类,让它充当我们的模型和销售关系系统之间的翻译。并且将配额的判断逻辑放到领域层中,不在Booking Application中判断。

请添加图片描述

并且Allocation Checker的内部实现可能为解决性能问题提供了机会,比如不是每次都去请求Sales Management System接口,而是缓存结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值