领域驱动视频总结

3.领域模型的元素

3.6 实体

即使DDD应用程序是由行为驱动的,我们仍然需要对象。DDD有两种类型的对象表现,这些对象分别是由身份定义的,以及由它们的值定义的对象。我们将首先关注由它们的标识定义的对象。这些对象称为实体。一个实体是我们需要能够跟踪、定位、检索和存储的东西,我们用一个标识键来实现这的。
它的属性可能会改变,因此我们不能使用它的属性来标识对象。如果你在软件中做过任何类型的数据持久性,你可能对实体和它们的id会很熟悉。当我们在建模一个问题时,我们可以讨论实体,而不必考虑它们是如何在结果的软件中实现的。但是,当开始编码时,需要遵循一些模式,以确保这些对象具有领域驱动设计的实体的专业属性。
正如您可以从IDDD书中的那份导航图的Entity那一部分看到的,实体对于我们的软件来说是相当重要的。因此,在我们了解这些其他元素之前,比如领域事件、存储库、工厂等等,您应该首先对实体有很好的理解。我们模型中最重要的实体是Appointment(预约)。这就是我们将在安排Appointment(预约)时创建、编辑和检索的内容。Appointment继承了我们创建的名为Entity的基类,它提供了一个ID属性,但在图中不能看到它。我们一会儿再看这个。注意,这里显示的所有类都是继承自Entity标识基类的(PS:即所有实体其实都继承自Entity)。

这里写图片描述

然而,尽管其他的类是实体,在我们与Michelle的讨论之后,我们得出结论,我们希望有一个单独的实用程序来管理client和patient的信息,以及另一个单独的应用程序来管理关于staff(员工)的信息,以及staff(员工)人员调度。因此,在预约调度的限界上下文内,我们不需要这些协作实体相关的非常多的信息或行为。此外,我们确定管理client、patient和staff的信息,这是这个模型的外部系统的责任,而且非常适合简单的CRUD。我们并没有从新建个修改这些数据中得到任何的复杂的规则和行为.
因此,doctors, rooms, clients, 和patients的概念都是在预定调度限界上下文之外管理的。比较一下其他有界上下文中的patient和client的CRUD类。它们非常简单,它们不会继承我们的Enitity基类,最有趣的是它们的ID属性都是Integer。当我们创建这些类时,我们将让数据库分配id。所以这些类不是用领域驱动的设计来设计的。

这里写图片描述

现在让我们回到预约调度上下文。这里的Client, Patient, Doctor, 和Room类都完全不同于我们刚才看到的CRUD类。然而,它们确实是刚才那些CRUD类的相同字段的子集(即该上下文中的类所具有的属性,只是限界上下文之外的CRUD类的属性的一部分)。当我们调度的时候,我们需要知道的是它们的id,它们的name,也许还有其他一些细节。但这里它们只是简单的数据,它们是只读的。
我们决定将Michelle作为调度应用程序的规则。我们将与现有的Patients, Clients, 和Doctors一起工作,但可以在需要的时候将其链接到需要的维护功能上。假设诊所里的人正在和Ross通电话,准备预约Indiana Jones。但另一边电话响了,她把罗斯搁置了,打进来电话的是我。在最好的情况下,我打电话给她,让她知道我的姓拼错了,这经常发生,人们只是想把H放在那里。因此,即使她处于正在调度的中间,但是调度有自己的后台,它有自己的限界上下文,并且完全独立于客户管理区域,非常快速地修改我的名字后,她仍然可以将应用程序转到客户端管理区域。注意,Ross仍然是主动调度的客户对象,它出现在页面的左上角。(其实这个小插曲只是告诉我们,由于进行了限界上文的划分,我们在做客户修改的同时也可以做预约调度的工作,互不冲突)。
现在回顾一下日程安排。我注意到更新正显示出来了,我的名字被就纠正了 ,现在她可以继续和Ross安排非常可爱的Indiana Jones做一些健康检查。所以对于用户来说,预约安排和客户操作没有区别。这是两个之间的平滑流。
但为了设计我们的应用程序,一切都是在他们的个人上下文中绑定的,当我们需要做类似的事情时,我们不必担心快速切换上下文。记住,我们现在讨论的是什么原因使得它们是实体。一个Appointment(预约)对象需要定位和跟踪,我们需要能够很容易地编辑它们。使用一个惟一的标识允许我们持久化和检索Appointment(预约),即使它的一些值发生了变化。在我们的系统中,Appointment(预约)绝对是一个实体。在这个特殊的背景下,我们需要更多地思考当前上下文中的 Client, Patient, Doctor, 和 Room类。我们的讨论强调了这样一个事实:在创建预约时,我们只需要访问客户、病人、医生和房间的一些高级信息(即只是查询),但这些对象不会被编辑(即修改)。
所以我们想要这些被剥离的、只读的类型给我们每个人最少且够用的细节即可。我们仍然需要能够独特地识别他们,我的意思是,他们确实有一些标识符。如果client的名称发生了更改,我们将在client管理系统中进行更改,那么在查看该客户的预约调度时需要反映出我们刚刚新改动的名称。在我们的后端系统中,应该只有一个记录来表示特定的client(即只有一份完整数据,所有数据都要是该数据的子集)。因此,在此上下文中属于引用类型的client类和其他类型仍然是实体。我们与另一种领域专家沃恩·弗农(Vaughn Vernon),一位DDD专家,对我们的决定进行了三重检查,我们很高兴地得到了他对这个特别决定的赞许。
因此,所有这些类型都继承了我们的基本Entity类,但是请注意,这些引用类型使用int作为它们的基本实体的ID,而不是像Appointment(预约)中使用的GUID那样。这是因为所有其他类型的管理都是使用CRUD完成的,而且使用CRUD很容易使用数据库生成的ints。而Appointment 是使用DDD原则建立的,您将看到在构建DDD实体及其相关逻辑时使用GUIDS要相对容易得多,而不是依赖数据库提供主键标识值。
它不仅更简单,而且更清楚地遵循DDD原则,因为我们将在不涉及数据库的情况下构建所有预约的领域逻辑。在我们的模型中,如果我们总是需要一个数据库来分配他们的id,那么我们就很难在我们的模型中进行预约,以及当我们开发应用程序时进行单元测试。这并不是说如果要使用DDD类型的应用程序,就不能使用整数id,只是它会让DDD工作进展变得更困难一些。你说呢,朱莉?

朱莉:是的,我在Entity Framework中确实遇到了一些与之有关的东西。我显式的继续使用数据库生成的ints主键的模式,因为我不想给人们留下他们必须纠正这个错误的印象。就像我一样,25年的依赖了,对吗?突然间,我不得不戒掉坏的习惯 ,然后转到GUIDs。当然,我的意思是,在你选择使用ID的时候会有一些权衡,但是在我们的客户端代码中生成的ID,这在我们的代码中有很多的价值。哦,每一次我们都在做不同的单元测试,作为测试的一部分,我们都需要来实例化一个int类型的东西来作为主键。
我们就像,呃,现在我们必须找到另一种方法来实现它,因为这是一个问题,我们要保护它。Julie,Michelle,我还谈到了如何命名在这个特定的有界环境中简单引用类型的类型。起初,我们担心我们可能会因为对Client, Patient, Doctor, 和Room的不同定义而感到困惑。我们想称它们为ClientDetails,或ClientView,或者类似的东西。
但是由于使用了领域专用语言(ubiquitous language),我们在调度上下文中的事实使我们能够理解client在这个特定空间中的含义。在调度客户端仍然是一个客户端,所以我们使用相同的名字,尽管这是一个不同的定义的类型比与我们合作的客户在客户端病人管理应用程序,由于名称空间在我们的代码中,我们能够让它清楚哪些代码。

这里写图片描述

我们在调度上下文中的事实使我们理解client在这个特定空间中的含义。在调度上下文中的client仍然是一个client,所以我们使用相同的类名字,尽管它定义的类型不同于我们在client-patient管理应用程序中使用的client.好的,感谢我们代码中的命名空间,我们能够清楚地知道哪些代码在代码中。

3.7 Eric Evans谈实体的单一职责

这是Eric Evans对实体的一些额外见解。史蒂夫问他,实体是如何与单一责任原则保持一致的。如果您不熟悉这个面向对象的编程原理,您可以在Steve的SOLID课程中了解更多关于它的内容,这里是Pluralsight。Eric,我听到的一个问题是,一个实体的单一责任是什么,或者换一种方式问,有一个实体在它里面有很多商业逻辑,那么它违反了面向对象编程中的单一责任原则了吗?或者你会如何协调?
我想说的是,我不认为一个实体应该有很多不同的业务逻辑。现在,我确实认为这些实体是非常核心的,所以它们得到大量的功能是很自然的。但如果你这样做,你就会有几次打击。其中之一就是当你建立系统时这些中心实体的需求越来越矛盾。因此,它们最终变得巨大。发现有600或1000种方法以及成员等等的的实体并不少见。所以,我不认为这很好。我认为专注于设计实体上的职责其实很简单,我想你们也可以说,即非常紧密相关的责任---那就是身份标识和生命周期。因此,注意,这里所说的身份标识可不仅仅是代表一个ID字段。
在某些情况下,你只需在对象上放置一个ID,它就会很好地处理它。这种领域的结构是允许的。例如,如果UPS运送一个包裹,发生的第一件事其实就是跟踪ID被分配到那个待运送的包上,它会一直保持到最后。所以在包的身份中除了ID没有其他的信息了,但事实并非总是如此,特别是在涉及到人的领域中。
你经常会发现,仅仅使用一个标识的ID是不足以标示一个客户的。例如,你可能需要打电话给那个人,你必须弄清楚那个人实际上是否是号码3275628的顾客吗?他们不知道这个号码来告诉你,或者如果他们知道,它们却想留着自己验证一下。所以,你问它询问他们的名字---当然,这并不能确定某人的身份,但这是证据。而你所向他们询的电话号码,也许是关于他们的一些秘密信息。
这是一个身份变得更复杂的例子。实际上,当它变得非常复杂的时候,我不想把责任推到实体上。所以,有时候,身份匹配变得如此复杂,我将会有另一个对象来对此负责,或一组函数,来负责这个。
让我们回到更典型的情况。你有一个实体。也许大多数时候你可以用那个ID字段来管理。这个ID经历了一些生命周期。一个UPS包裹经过了一些生命周期,比如收货,我想,然后是发货,还有在运输过程中,它从一个位置移动到另一个位置,最后它被送到客户那里,可能还有收据,你知道的,有人要签一下收据。所以这是一个包裹所经历的基本的生命周期。但如果还有其他的逻辑,我打赌,你会想开始使用其他的逻辑源。
您可能需要一些服务,可以回答关于应该如何处理快递包的问题。但你可能想要,即使是核心实体本身在做的逻辑,它可以把它委托给value对象,这样所有的实体都在说一些关于它的状态的简单问题,或者是关于下一步应该做什么,或者类似的事情。并且值对象交互来回答这个问题,或者调用一些服务来回答这个问题。我想说的是,我认为单一责任是一种适用于实体的好原则,这种模式可以将你指向一个实体应该保留的责任。任何不属于这个类别的东西,我们都应该放到别的地方,委托其他地方,或者服务,或者类似的东西。

3.8 Eric Evans在实体比较上

我开始相信一个实体甚至不应该有一个平等的比较。一个实体是否与另一个实体相同的问题是一个重大的问题。equals是一个简单的概念我认为它完全适用于一个值对象。一个值对象应该始终有一个定义良好的equals,几乎总是这样。但是我认为这个概念并不适合实体,通常你所说的相似指的是同一个实体,或者是我得到了同一个对象的相同的两个副本。我的意思是你真的不得不说,好,你到底是什么意思?而对于一个值对象,你知道的,如果它说是32英里,假设它是一个值对象。如果我再找一个32英里值的物体,那么它就是它,没有别的东西可以知道了。但是如果我在一个系统中找到两个对象代表某个客户,而现在我想知道他们是否都代表着同一个客户,为什么我要问这个问题呢?因为这是更复杂的。
我这么问是因为我们最终可能需要进入他们两次,拿到连个客户的ID,然后来确定他们是否为同一个人?或者说我们现在有两个客户对象,他们具有相同的ID但是他们属性却并不一致---这种情况您肯定遇到过,即在分布式数据中,数据还没有来得及更新,在这种情况下,我试图调和两个我认为具有相同身份的对象。所以我们分别的考虑这些情况的话,会发现:实体的equals并不能告诉我所想要知道的。

3.8 如何在代码中实现实体

现在让我们来看看我们的兽医预约应用程序中的一个实体。我们来看看Appointment实体 ,它定义了所有的预约所需要的信息:需要安排一个特定的动物,特定的医生,在一个特定的房间里。现在,Appointment类继承自Entity<T>,它是一个泛型基类。在这种情况下,它是Entity<Guid>,如你所见。Guid定义了我们的标识ID属性的类型,我们的ID,我们之前讲过这个模型中不同实体的结构。

这里写图片描述

这里写图片描述

我们想在Appointment中使用Guid来作为主键的,原因是我们可能会动态的创建Appointment(预约)。 让我们来看看这个Entity基类,你能看到一些关于下图所示的的东西。

这里写图片描述

首先,它是一个抽象基类,所以我们不能只创建Entity对象,我们必须创建一个其实现类,比如一个Appointment(预约)。使用泛型,我们说实体将会使用我们要求的任何类型,这类型是用来定义ID的,所以对于Appointment(预约),我们说实体将会使用Guid类型作为它的标识。我之前提到过,为什么我需要在这个上下文中为Appointment(预约)使用Guid,因为我需要能够在这个上下文中创建新的预约,而我不打算等待数据库为我生成这个ID。因为使用这个Guid来作为主键,让我可以在之前就创建好主键ID,因为它是创建Appointment(预约)所不可或缺的。
如果您查看Entity基类的构造函数,其实构造函数只是检查,以确保您传递的值是是否与类型的默认类型相违背。这是假设默认的类型是Guid,或者默认的是int的话,那么你的ID就得是整数。这里还有一个构造函数,它是一个受保护的构造函数(空参构造器,为持久化使用)。现在你所听过我们谈论的都是领域相关的知识,而不是我们的持久层。但是,因为我知道我们将使用实体映射框架,这是我们使用的ORM,我知道Entity Framework对于构造函数有一个特定的规则.我现在只是想提前做点事情,而不是以后再担心。
再加上一个持久化层,我将会做很多其他的事情,但我不会把它们放到实际的领域类中。但这里之所以出现这种情况是因为它需要访问参数列表构造函数,因为它使用反射来物化查询结果中的对象。这也是它存在的唯一原因。我只是在为我的坚持做准备,这只是我对我的持久层做出的一个务实的让步,尽管我们关注的是领域。好的,下面我们有很多方法来进行相等的 比较。

这里写图片描述

我们刚才听到Eric Evans说他真的不喜欢在实体本身中进行相等的比较,但是在这里我们使用的是非常简单的实体,所以在这个例子中,我们发现在Entity基类中,相等比较是有帮助的。好的,让我们回顾一下Appointment(预约)的其他部分。既然Appointment(预约)有更多的行为,而不仅仅是状态,我们不希望它只是一个我们的应用程序可以得到和设置的属性包。因为那将是一个贫血的领域模型。是的,因为那将会引导我们走向一个更贫血的领域模型。我们想要一个富领域模型。特别是,我们也在限制我们如何创造这个任命。
来看Appointment的构造器,我们总是需要直接指定一个Guid类型的ID,或者只是让它通过我们刚才看到的基构造函数将ID传递进来。现在有些时候我们想要更新一个Appointment(预约),记住现在它并不是值对象所以它们是可变的,因此我们可以改变它们,我们要通过方法来做。例如,如果我们决定更新一个Appointment(预约)的room,那么我们将通过一个方法来完成它,而不仅仅是一个setter方法,因为我们想做的是额外的行为。

这里写图片描述

在这种情况下,我们可能想要产生一个我们可以处理的预约更新的事件,作为这个事件的结果,我们可能会发送一个通知,或者是触发一些其他的行为。这同样为我们在未来改变逻辑带来了巨大的灵活性。而如果我们只是在应用程序中通过设置一个值来满足刚才的这个需求的话,那么要实现这种灵活性是很难做到的。
朱莉:right

现在,我们不想在构造函数中做很多工作,因为这使得我们的代码更难测试。 因此,由于我们在创建Appointment(预约)时确实有一些验证以及想要做的其他检查,我们创建了一个静态的工厂方法,它只是做一些检查,以确保所需要的各种数据片段的正确性。在这种情况下,大多数需要的数据都只是我们领域中其他相关元素的id。我们只是检查一下,确保它们在ID的有效范围内,因为有的人可能会有指定某个医生的特殊要求,但是在输入医生的ID时候却键入了0.所以我们进一步限制他们也不这么做。因为如果没有这些值,就不能创建Appointment(预约)。这帮助我们避免构建不完整的对象,即有的属性已经设置了,而有的属性没有设置,这样的话他们处于不一致的状态。一旦我们创建了一个Appointment(预约),我们就需要把它记录下来作为诊所时间表的一部分,这涉及到一些额外的丰富的行为。因此我们向下滚动到底部,我们有这个方法叫做Schedule。而这也正是我们要的参与到一个Appointment(预约)的额外工作,不只是在我们的应用中创建和实例化,还包括存储在我们的持久化层中,与已经存在于系统中的其他Appointment(预约)一样。 

这里写图片描述

这里写图片描述

现在,这里有一些不同的事情,但我们不会在这一点进入代码。因为在下一个模块中,我们将会稍微调整一下我们的设计,因为我们已经了解了更多关于域的知识,所以我们将更多地讨论下一个模块的调度是如何工作的。因为在接下来的模块,实际上我们要调整我们的设计一点的,因为我们已经了解了更多关于领域的知识,所以在下一个模块我么你会讨论如何让调度真正的工作起来。

这里写图片描述

好吧,让我们来看看另一个简单的类型,它不是用DDD构建的,它只是我们的引用类型之一:Doctor类。你可以看到这里我们也有从Entity中继承的Doctor。在这种情况下,它使用int作为它的标识符ID,它唯一的作为属性的就是医生的名字。如果你记得在Eric之前看到所有这些类的类描述,我们已经明确地决定了这些引用实体我们实际上在其他地方进行维护,因此它们没有复杂性。对,它们只是只读的,所以我们不需要创建它们。我们使用的ints是在我们在应用程序的不同区域创建的CRUD上下文中创建的,但它们仍然是实体,但它们是integer类型的实体。

3.9 Associations(也叫Relationship)

包括我自己在内的许多开发人员倾向于在两个方向上定义类之间的关系。例如,一个订单(order)有一个行项目(line item),一个行项目对应一个订单。宠物主人对应宠物,宠物对应着主人。我们中的许多人在默认情况下倾向于选择双向关系。由于领域驱动设计旨在简化模型,我们开始更快地认识到双向关系常常使事情变得过于复杂。例如,在我使用Entity Framework来讲实体加入到持久化存储中时候,我就经常会发现这个问题的严重性。这带来了它自己的行为和关于关系如何管理的假设。
有时,我的模型包括一些可能不完全必要的导航属性,这可能是一些悲伤的原因,这让我花了一些时间考虑是否真的需要导航。领域驱动的设计引导以单向来构建实体之间的关系。这并不是说你不应该有双向关系,而是因为涉及到额外的复杂性,你应该花些时间考虑一下复杂性是否合理。
关系(Relationship,),也称为关联(Associations),应该是类型定义的一部分。如果引入双向关系,就意味着两个对象不能在没有另外一个对象存在的情况下单独定义。如果不是这样,那么具体的关系方向,也称为遍历方向,以保持您的模型设计简单。埃里克·埃文斯(Eric Evans)这样说,一个双向关联意味着两个对象只能被理解在一起。当应用程序需求不需要在两个方向上进行遍历时,添加遍历方向会减少相互依赖,简化设计。所以,用DDD的角度来看我们的模型,然后问,我们可以在不标识他们的宠物(即patient,毕竟病的是小动物)的情况下定义client(客户)吗?以及我们可以定义一个宠物但是不指定它的主人吗??
这听起来是一个简单的问题,但是它会导致一大堆的辩论。例如,如果一个人没有宠物,为什么要安排Appointment(预约)呢?因此,在安排预约的情况下,如果没有一个或更多的宠物(即patient),client的存在就不是很适合。或者从另一个角度看,猫不能支付账单或打电话预约。那么,我们如何定义一个没有客户的宠物呢?
这些都是很合理的论点,但都不能得到最终的结果。我们再从默认的单向关系开始。client(客户)需要一个patient(生病的宠物)来安排Appointment(预约)。client(客户)不需要patient(生病的宠物)来支付账单。好的,如果我们从关系的的末端开始,patient(生病的宠物)不会安排一个Appointment(预约)所以这就变成了一个静音点。patient(生病的宠物)也不付账,你知道,因为我的狗没有信用卡。同样他也不能很好地使用电话。
所以你什么时候开始给病人看病需要去了解负责该patient的client信息呢??这听起来是一个有趣的问题。所以,在安排预约的情况下,我们可以认为我们应该从client(客户)到patient(病人)的遍历,而我们从patient(病人)返回到client(客户)的遍历过程中什么也没有得到。

这里写图片描述

你可能会回避这个概念,但要记住,我们现在所关心的是安排一个Appointment(预约),而不是所有其他可能的场景,在这个场景中,从patient(病人)到client(客户)遍历,可能有意义。当然,这是YAGNI的另一个例子,你不需要它。事实上,在这个背景下,我们最初是将所有者(就是主人)作为patient的一个属性,但我们意识到这不是必要的,所以我们把它移走了。但是我们保留了ID,因为我们有一些有用的场景。因此最终我们选择了从Appointment(预约)到doctor(医生),patient(病人)和client(病人)的关系。和定义从client(客户)到patient(病人),或他们的宠物的遍历方向,而不是另一种遍历方向。

3.10 Value Object(值对象)

在介绍实体时,Steve和我讨论了由连续性和标识符定义的对象,而不是由它们的值定义的。那么哪些对象是由它们的值定义的呢?这些被称为值对象,它们在领域模型中与实体对象一样,都扮演着同样重要的角色。值对象具有非常具体的特征。它是用来测量、量化或描述你的领域中的某样东西的对象。与其拥有一个身份标识符ID,它的标识基于其所有属性的值的组合,即值对象是由其属性值来唯一确定的。
因为属性值定义了一个值对象,它应该是不可变的。换句话说,一旦创建了这些值对象,就不能更改任何属性。相反,您只需创建另一个具有新值的实例来替换之前的值对象即可。如果您需要比较两个值对象来确定它们是否相等,那么应该通过比较其所有的值来进行比较。值对象可能有方法和行为,但它们不应该有副作用(副作用是函数编程的概念,即方法的执行并不会访问外部数据以及IO操作,我这里只是片面的解释,更多的含义要自己参考)。任何关于值对象的方法都应该只计算东西,它们不应该改变值对象的状态,因为值对象是不可变的,或者是修改系统。如果需要新的值,则应该返回一个新的值对象。
您可能一直使用的一个值对象是一个字符串。在.NET和许多其他语言(作为一名java开发我必须说话了,java的字符串也是不可变的),字符串类型是不可变的,您现在知道不变性是一个值对象的关键属性之一。字符串是字符的集合,所有这些字符的组合赋予了字符串的含义。例如,c-a-r,英文,表示一辆车。
如果一个字符串是可变的,我们可以把R变成T,现在这个字符串是c-a-T,这是一只猫,它的意思和汽车完全不同啦。或者我们可以加个字母,比方说把S放在car前面,那么就变成了scar。这就完全的改变了car的意思啦。但它不仅仅是一个字符串的数组,它们的顺序对意思的表达也很重要。让我们来想想“dog,d-o-g”这个词,把它的字母移动一下,给我们一个不同的意思。
在.net中修改字符串是一件很容易的事,比如你可以改变它的长度或对所有元素做一个全部大写。例如,在一个字符串上调用ToUpper,它没有改变字符串对象,而只是为您提供了一个新的字符串实例,它现在具有所有大写字母。

这里写图片描述

我听过很多开发人员说,货币值和金融系统是他们系统中值对象的完美候选者。Ward Cunningham为我们提供了一个非常有用的例子,一个公司的市值。如果一个公司价值5000万美元,这意味着什么,50 million dollars。这是一个非常具体的度量。50 million 本身并不是一种度量,因为它没有没有单位,它就没有意义,而在这种情况下单位是美元。事实上,$符号并没有起到什么作用?因为有可能是美元,加元,澳元等等(注意:我上面 的截图比较晚,其实刚开始的图,$USD并没有字幕后缀,那时后来随着讲解又加上的)。但是,你如果仅仅拿出一个“WOrth Unit:US Dollor”也不足以表示价值。只有当你把这两个组合在一起的时候,它才表示5000万美元的意义(其实这个例子的作用,只是告诉你,值对象应该是一个完整的意义,任何一个5000 million或者是US Dollor都是不足以表示出一个完整的度量意义的)。
因为金融系统的运作方式和货币价值的流动性,实际上还有一个因素需要考虑,就是这个5000万美元的时间点。我们仍然可以在这个公司类中有两个属性,但是通过创建一个值对象,你也可以保护和约束度量。例如,我们可能有一个类叫Company。它可能有一个属性,它是一个小数,它代表的是Worth(我叫它市值),另一个属性叫做Worth Unit(市值单位),它可能是一个表示美元的字符串,就像上面的“US Dollar”。

这里写图片描述

这种方法的问题在于它并没有将这两个属性和绑定在一起。比如说没有人应该能够在不设置Worth的情况下设置Worth Unit,它们应该一起改变。更重要的是,没有人能随便的只改变Worth Unit(毕竟这可是钱啊,单位一变,钱就差的很多了,1美元和1日元能使一回事吗??),5000万卢比完全不同于5000万美元。值应该由对象作为一个整体来表示。它是不可变的。如果公司的市值改变,那么我们可以重新初始化一个值对象,其市值是新的值,然后替换掉旧的即可。值对象并不总是用于度量,但它可以是另一种类型的描述。

这里写图片描述

我经常使用这个,DataTimeRange,它非常适合于兽医预约应用程序。我们通常设定一个开始和结束的时间在一起,而不能创建其中的一个却不创建另一个。因此,我们在方法中通常需要传递两个值,开始和结束时间。我们将它们封装在一个名为DateTimeRange的值对象中。该值对象的属性setter方法是私有的,因此它是不可变的。我们没有显示类的完整的逻辑,但是,当我们查看应用程序中的值对象时,您将看到更多关于如何在我们的软件中实现一个值对象的方法,以确保它满足所有的属性的要求,而不仅仅是不变性,而是如何处理相等的比较和其他逻辑。
在IDDD这本书中,他建议我们应该尝试使用值对象而不是所有可能的实体。他说,你可能会惊讶地发现,我们应该努力使用价值对象而不是实体来建模。即使一个领域的概念必须被建模为一个实体,实体的设计应该偏向于作为一个值容器,而不是一个实体容器。这这意味着你会发现你的设计会有一些实体,它们的逻辑非常小,或者很少有一些基本元素作为它们的属性,但是他们却有很多的值对象类型的属性在其中。
所以他并不是说所有的东西都应该是值对象,但从实体开始思考这可能是我们的自然本能,然后可能在一段时间后,哦,也许这应该是一个价值对象。沃恩建议的是每次都要思考,这应该是一个值对象?你会惊奇地发现,你原本可能认为作为一个实体,其实作为一个值对象会更有意义。时,当你看到一个实体时,可能会有一些属性似乎总是在一起。着可能就是一个信号,告诉您可能可以将这些属性打包成一个值对象。

3.11 Eric Evans在值对象上的方法

我认为值对象是一个很好的放置方法和逻辑的地方。应该说没有比值对象更好的地方了,为什么这么说呢??因为我们可以在没有副作用和标识符的情况下进行推理,而所有这些副作用等,都会让逻辑变得棘手。我们可以把函数放在那些值对象上,做纯推理。所以,你知道的,人们对数据的关注程度如此之高,他们对于实体也同样,他们也会说,基本上,一个实体就是一个ID,还有什么呢?嗯,我认为有更多的原因,因为它只是区区一个ID而已,你知道,它是什么意思,你如何解释它?
但是,有了一个值对象,就有了非常密切相关的逻辑。好吧,一个很好的例子可能是日期。日期是一个典型的值对象,有各种各样的逻辑。现在我们通常在某个实体的上下文中做这个。假设一个人有一个出生日期,我们想知道他们有多大。所以,如果你想做的话,那么你就必须知道现在是什么日期。从概念上讲,很容易理解,那么我就把今天的日期减去某一天的日期,我就会得到年份(毕竟这里求的是年龄,不需要那么精确)。

这里写图片描述

也许我会根据四舍五入取最近的年龄,这就是我们通常所说的某个人的年龄。但这一逻辑并不简单。joda-time是一个很好的值对象,它实际做了一些事情。你会发现这些对象不仅仅是数据,即它不仅仅是一个数据结构,这里有两个日期。而且它确实是一个带有逻辑的对象。但逻辑没有副作用?是的,这将是我将应用于值对象的规则之一。
我们可以如此自由地使用它们的原因之一,是因为当我说时间间隔给我你的持续时间时候,我没有修改它的时间间隔,或者日期,我只是返回那个新的方向对象。如果我说,哦,我不想要这个完整的间隔,我要创建一个新的区间。没有办法修改任何joda-time对象,也没有任何方法可以修改任何东西。

3.12 我们代码中的值对象

这里写图片描述

下面仔细查看我们创建的DateTimeRange值对象。一个值对象本身没有意义,所以您可以看到它被用作Appointment(预约)类型的属性。让我们看一下如何实现值对象的一些重要概念的代码。DateTimeRange类继承自ValueObject。这两种类型都位于我们的SharedKernel中(共享内核,说白了,就是你们的common包),因为我们可以在应用程序中的多个不同的限界啥下文 中使用它们。
看一下DateTimeRange,首先要注意的是它继承自ValueObject<T>,如果我们看这个类型。这是Jimmy Bogard写的一个类,你可以在这里的URL找到它。使用这个基类的值对象的益处是该基类实现了IEquatable <T>,还为您提供了重写了的Equals和GetHashCode方法,将自动通过反射,看一下您的值对象所具有的不同属性,并与它们进行比较,以确定两个值对象是否相等。然而,有一点需要指出,它并没有考虑到您的值对象具有集合属性的可能性。

这里写图片描述

现在我们故意使用这个,因为到目前为止,我们没有任何具有集合属性的值对象。回顾一下DateTimeRange,要注意的一点是,我们使它变得不可变。不幸的是,c#没有对不可变类型的内置语言支持,但是我们可以通过确保我们的属性没有任何public的setter方法,或者任何其他方式来让用户在创建这个对象时设置这个对象的状态。我们在构造函数中提供了这种类型所需要的任何状态属性。

这里写图片描述

注意,我们有两个不同的公共构造函数,第一个是很明显的,我们传递一个开始和结束时间的值。但当我们在建造它的时候,史蒂夫有一个很好的想法,考虑到我们可能有一个开始时间和一个持续时间的场景。所以如果我们要创建一个Appointment(预约),我们知道它从上午10点开始,这是一个半小时的时间。这让我们传入这些信息,然后构造函数本身根据这两个值来设置结束日期的值。

这里写图片描述

您刚刚听到Eric Evans讨论了在值对象中可以提供该值对象责任之内的额外的逻辑方法。作为一个例子,我们得到了这个DurationInMinutes方法,它让我们很容易地发现一个特定的DateTimeRange所代表的时间。当我们在发展我们的应用程序时,也许我们想要增加一个几天的持续时间,或者在增加一个数小时持续时间,但现在的按照分钟计算的持续时间可以满足我们所有的需求。我想指出的是我们在这里所说的其他方法类似于我们之前讨论的一些字符串方法。记住,字符串方法基于现有字符串返回字符串的新实例,再加上您要求执行的方法的任何逻辑(外国人说话真费劲,就是应用了所执行的方法逻辑的字符串新对象)。

这里写图片描述

我们在这里做同样的事情,例如,newEnd我们可能需要改变DateTimeRange的结束时间,例如,一个约会。既然这是不可变的,我们不会改变结束时间但我们可以说,嘿,DateTimeRange,我们需要一个新的看起来就像你一样,除了结束时间改变。所以我们要做的就是调用这个方法,并传递一个end Time,它为我们返回一个全新的DateTimeRange实例,并且具有正确的开始和结束时间。所以我们有其他方法可以用不同的方法来做。

这里写图片描述

然后现在我们所指出的另一个值对象是AnimalType。这只是为了让您知道,您的值对象可以非常简单。在这种情况下,动物类型只是一个物种的组合,以及特殊宠物的品种,或者是我们在兽医诊所所处理的病人的品种。这里并没有很多其他的行为,但它确实为我们提供了一个容器,将这两个相关属性作为一个值对象封装起来。

3.13 Eric Evans谈在值对象中的实体逻辑

我觉得这很好。如果这里有经典的软件逻辑。那么我就喜欢将其放在值对象中。你要记住一件事,即在值对象上进行测试,要远比在实体上测试来的简单的多。你可以更自由地使用它们。那么,你的实体通常在那些值对象之间,就变成了这种关键的胶水,一个协调器的角色而存在,但并没有做很多,不能自己完成很多的操作。
但这并不意味着我们仍然没有一些逻辑,但它会非常简洁。事实上,我认为一个很好的方法是,如果我们能将领域专用语言(ubiquitous language)运用到你所看到的实体的方法中,你会看到更高层次的东西。这几乎是一种用例层次的交流,而不是细节的细节。

3.14 Domain Service(领域服务)

PS:与application Service(应用服务)区分开,并不是一回事!!领域服务属于领域层,应用服务属于应用层!!
当一个操作对模型很重要,但不确定应该属于哪一个实体或值对象时,将其划分到Service通常是合适的。但是,不要急于放弃在现有实体或值对象上为该操作找存放地方。否则你可能会得到一个非常程序化的贫血模型。通常,Domain Service(领域服务)需要为多个不同的协作实体或值对象的操作充当协调器。

这里写图片描述

Evans指出,①良好的领域服务必须是独立的,而不能属于现有实体或值对象的自然部分。②同样,我们不想将我们所有的丰富的行为从实体和值对象转移到我们的服务中去。服务也应该有一个由领域模型元素组成的定义了的接口。③最后,领域服务应该是无状态的,尽管它们可能有副作用。这意味着我们应该始终能够简单地创建一个服务的新实例来执行操作,而不必依赖于某个特定服务实例中可能发生的任何以前的历史。但当然,调用服务的方法可能会导致系统本身的状态发生变化的结果。

这里写图片描述

这些规则特别适用于领域服务,领域服务属于我们的应用程序的核心。您的软件还可能使用服务来执行与基础设施相关的工作,或者作为应用程序前端的一部分。下面是我们可以在DDD应用程序的不同层中找到的一些服务的例子。

UI层代表系统的前端,这里应该尽可能少的业务逻辑。它经常与应用层相结合,应用层关注于应用程序所必需的行为,而不是客户的问题领域。例如,应用程序可能需要使用文件格式或解析一些XML,它可能有用于这些目的的服务,但这些都与领域无关。
我们在应用程序的核心,存储了核心模型和领域对象,我们将为任何不属于其他地方的操作定义领域服务。这些服务经常涉及多个领域元素的操作,或者可能负责编排某种工作流。例如,处理订单可能涉及一系列步骤和多个领域元素,比方说:系统检查库存,验证客户信息,还可能调用信用卡支付,然后发送消息来发送订单,通知客户,并减少库存。
最后,我们有基础设施级的服务,这些服务通常会实现在领域的核心中定义的接口,例如ISendEmail。但是由于它们需要访问文件系统、数据库或网络资源等外部依赖关系,所以它们就生活在系统的基础结构层中。关于我们的领域,你可能会发现基础设施不是很有趣。尽管,那些创建这些服务的内部工作的人可能会发现它们很吸引人。稍后的课程中,我们将在应用程序中查看服务的实现。
SO:其实服务是一个很大的概念,领域服务知识服务的一个子集而已,因为它与领域逻辑相关,所以相对比较重要。我们这里所说的服务,主要分为:领域服务(即与实体或者值对象进行协调),基础设施服务(比较通用的服务以及与其他外部系统交互的服务,像数据库,NOSQL,文件服务器等的交互,都是基础设施层的服务范围),还有最后的UI服务,其实我也认为就是应用服务,主要处理消息的解析,文件解析等等,在应用服务中行不会出现领域相关的逻辑。

3.15 词汇术语表

我们已经在这个模块中涵盖了很多领域,你已经学了很多新术语,所以我们只是想在进入下一个模块之前,先复习一下其中的一些。

这里写图片描述

第一个是一对术语经常是相互作用的,贫血的领域模型和富领域模型,而从DDD的角度来看,贫血性领域模型对CRUD来说是完美的。这些模型看起来更像是一个数据库模式,而不是一个有很多方法和丰富的行为的类。另一方面,这是一个富领域模型,这是我们在领域驱动设计中所追求的,这是一个真正专注于行为的模型,而不仅仅是改变属性的值。
然后我们讨论实体,实体往往是我们领域模型的核心部分。在我们的模型中区分一个实体和其他类型的关键是它有某种身份,我们可以使用它来跟踪它,以及从持久化机制中检出和存储该对象。
我们还讨论了Immutable(不变性),这是值对象的一个非常关键的属性。不变性只是意味着一旦对象被实例化,你就不能改变它的任何属性的值。

这里写图片描述

另一个重要的术语是value object,即值对象。值对象是一个不可变的类,它由它所拥有的不同属性的总体来定义。我们不需要一个特定于值对象的标识,实际上,一个值对象在它所拥有的单独属性之外没有标识符。为了让我们可以比较value objects,我们只需要简单的比较它的所有属性,如果它们都匹配,那么我们可以考虑这两个值对象是相等的。
我们还学习了域服务,这些很有趣,因为领域服务让你在不知道将逻辑和行为放在哪一个实体或者值对象时候,找到了一个合适的地方。
我们要复习的最后一项是副作用。副作用是在您的应用程序中发生的变化,或者任何与外部世界的交互。从技术上讲,应用程序状态的任何变化都可以看作是副作用,但一般来说,当我们谈论它们的时候,我们谈论的是那些改变了除了你正在执行的操作的主要意图之外的东西。例如,将查询信息与更改状态的操作分离开来通常是一个好主意。如果你遵循这个练习,那么对状态进行更改的任何查询,都会被认为有副作用。

3.16 Key Takeaways

哇,所以我们在这个特殊的模块里涵盖了很多地方。让我们记住我们从哪里开始。我们已经讨论了很多不同的细节,但是大的问题是我们想要关注的领域。还记得我们之前讲过的:DDD的第一个D代表的是领域。现在,当我向您展示我们的实体基类中的保护性构造函数时,您确实看到了我们为持久化做了一些的让步,但在其他大多数情况下,我们并没有过分的将我们的注意力关注在持久层上,而还是专注于领域。在某种程度上,我们需要将这些数据持久化到某种数据存储中,但我并不担心。我恰好擅长ORM,我们将为我们的持久层使用ORM。我相信,无论我们的领域向我们抛出了什么,我们都能从数据存储中得到它,而不会遇到任何问题。

3.17 资料

这里有一些你会发现有用的参考资料。再一次,我们不得不像你推荐Eric的DDD这本的书,还有沃恩·弗农的IDDD书,所以他们又来了。对于这个特殊的模型,我们讨论了域服务。Jimmy Bogard有一个很棒的博客和一篇关于服务的好文章。是的,你知道我有很多次都在寻找一些关于DDD的东西,或者是编程的其他方面,所以我经常来找Jimmy的博客。他只是以一种方式写作,当他解释这些的时候,对我来说是很有意义的。
再次,我们要感谢埃里克·埃文斯抽出时间帮助我们完成这个模块你可以在DomainLanguage.com网站上找到他的更多信息。和你的课程。哦,对了,我们和埃里克谈过单一责任原则。如果你不熟悉这个原则,你可以了解更多,这是我在我的扎实课程中所涵盖的一个坚实的原则。
PS:我也将Jimmy Bogard的博文做了翻译,欢迎查看~~
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值