为啥想学习这本书,之前就有同事分享过,但是因为完全听不懂,就没有去学。但是因为在准备晋升的ppt时,看到其他同事写的ppt,就发现区别太大了,他是站在更高的视野更高的角度来思考项目、思考业务、思考软件实现的。而我在写我的部分时,也越来越发现,如何定义自己做的项目,如何把三维世界的对象、问题、事件转化成代码,如何更好地描述问题,我一直都没有答案,期望能有一些对于真实问题建模的理论支撑.
一直思考什么样的代码算是好的代码,好的设计,到现在,入行马上两年,一些个人的想法,好的设计,1.首先要在功能上能满足业务需求,2是性能,能覆盖足够的异常场景,合理的时延等,3是可扩展性。可扩展性就意味着在设计时,需要面向未来,全面地了解业务,了解问题。而对本书的期待,就是能获取第三个点的一些答案。
该书分为四个部分
1. 让领域模型发挥作用:领域驱动开发的基本目标&术语定义
2. 模型驱动设计的构造块:消除模型和实际运行的软件之间的鸿沟
3. 通过重构来加深理解:有价值的模型,是在迭代重构中产生的
4. 战略设计:作为一个整体应用于系统的三个原则:上下文、提炼和大规模结构
模型:对现实的解释,一种知识形式,对知识进行有选择的简化和有目的的结构化,是经过严格组织并精心选择的抽象知识。
通过头脑风暴的方式和领域专家进行领域知识的学习,将现有信息转化成类图,在讲述自己已知的点和领域专家指出的问题的过程中修改、重构类图,使其能更正确地描述问题。
建模不仅仅需要对实体进行建模,还需要对规则、行为进行建模,因为他们也是领域的中心。
举例:策略模式
将业务规则没有定义地写在主流程中,其他非开发人员无法理解,也没有站在更高的层面去理解这段代码。
抽象出策略类,包含重要的业务规则。
领域模型:软件项目的公共语言的核心,是人们头脑中形成的与项目有关的概念的集合,用术语和关系反映了领域的深层含义,为领域量身剪裁的,十分精确。
通用语言:项目需要使用通用的,共享的团队语言。如果不使用通用语言,领域专家使用自己的语言,而技术团队使用自己的语言来从设计角度讨论领域。翻译成本高,即使有深刻的理解,也无法记录到文档中。
通用语言应该包含类名称和主要操作,有些术语用来讨论模型中已经明确的规则,还有一些术语则来自于施加于模型上的高级组织原则。团队一致应用于领域模型的名使该语言更丰富。
团队在交流活动和代码中坚持使用这种语言,并在讨论中使用模型的元素,以及模型中各元素的交互来大声的描述场景,将各种概念结合到一起,找到更简单的表达方式来讲出要将的话,应用于图和代码中。
需要让你设计的模型被领域专家理解,并判断是否合理。
讨论时,图可以帮助表达和解释模型,文档可以解释模型的概念,帮助在代码的细节中指引方向。
模型驱动设计:模型需要在分析和程序设计阶段都能发挥良好的作用。如果一个模型不能忠实地描述领域的关键概念,或者对于程序猿来说不太实用时,必须要重新设计。程序代码就是模型的表达,在修改代码时候需要有清晰的认知,修改代码就是在修改模型。
为什么Net是个抽象类。
因为Net分为普通的Net和总线,普通Net会有自己的布线规则,比如说从哪个组件的哪个引脚,线的宽度(最小最大)是啥。
总线:总线连接更多的组件和引脚,和普通net相比可能会需要有更宽的线,起始组件以及承载的功能也会不一样。
但是他们都有自己的规则,规则都为rule类来描述。
普通的net/总线都可以通过assignRule来添加规则,通过assignedRules()来获取自己的规则集合
服务定义了net的行为,工具类提供有用的接口。
建模人员参与程序开发:不参与开发的问题
1.传递模型过程中会丢失某些意图
2.模型与程序实现和技术互相影响,不参与无法获取反馈
建模人员参与开发的两个基本要素:模型要支持有效的实现并抽象出关键的领域知识(重复表达)
介绍一些标准的模式
分层架构:分层原则是层中任何元素都仅依赖于本层以及其下层元素。
日常开发使用三层架构,表现层、业务逻辑层、持久层,业务逻辑一般是在service里写的。领域驱动设计的关键在于,领域层负责表达业务的概念(model)、业务的状态信息(status)、以及业务规则(service)。领域层=service+dto
不同层的设计原则,具有内聚性且只依赖于其下层,与其上层保持松散的连接, 上层可以保持对下层的引用,而下层想和上层通信则需要通过其他的方式,比如回调。需要保证在连接时,只关注本层的关注点。
smart ui, “anti-parttern”
需要在用户界面实现所有的业务逻辑,简单的输入以及展示,可以使用smart ui
主要讨论了三种模型:entity,value Object和service。
还讨论了module
对象之间的关联会使建模和实现之间的交互更为复杂
三种方法使关联更易于控制
图5.1 领域更关心一个国家有哪些总统,而不关系一个总统是哪个国家的,从双向关联变成领域关心的单向关联
图5.2 一个国家会有多个总统,但是加上限定条件,阶段,由一对多变成一对一。
5.1:将关联限定于领域中所偏向的方向
简化的目的:清除那些对当前工作或对象模型不重要的关联
如果发现了关联的约束,就应该将这些约束添加到模型和实现中,使得模型更精确和易于维护
定义:不通过其属性定义的,而是通过一连串的连续时间和标识来定义的对象。
实体的基本概念是一种贯穿整个生命周期的抽象的连续型
建模时不要太关注属性,而要关注entity对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为必须要的属性。应该将行为和属性转移到与核心实体关联的其他对象中。除了标识问题之外,实体往往通过协调其对象的操作来完成自己的职责。
选取entity的属性时,关注哪些属性是用来匹配和区分实体的。不同的领域需要的不一样。
设计实体的标识
属性组合
为每个实体附加一个在类中唯一的符号,且该符号不可改变
很多对象没有概念上的标识,它们描述了一个事务的某种特征。
定义:用于描述领域的某个方面本身,而没有概念标识的对象称为 value object(值对象)。
value object被实例化后,用来表示一些设计元素。我们只关心这些元素是什么,而不关心他们是谁。
举例,地址
在邮购公司中,需要用地址核实信用卡并投递包裹。地址是订单实体的一个属性。关心的领域是订单,是购物,投递是由快递公司来处理的。此时地址是一个value object
而在快递公司,不同的地址,决定了包裹该去往何处,此时地址是一个entity
我们只关心一个模型元素的属性时,应该把它归类为value object。
value object 通常作为参数来在对象之间传递消息。比如日常开发中经常用的,查询参数,各种vo
共享和复制
复制:可能导致系统被大量的对象阻塞,但是在分布式系统中,却可以减少相应时间。以空间换时间
共享:一个实例,多处调用,减少空间,但是分布式系统响应时间更长,时间换空间
使用共享的场景
value object的应用
数据库中通过增加副本来降低查询开销,降低响应时间
value object没有标识,应该尽量完全清除value object直接的双向关联
不是事物的对象,由一些操作组成
一个银行应用软件,其中转账相关的,是领域业务知识,它应该被设计到领域层,因为转账是包含复杂的业务规则,而且是能够表征这个领域的。而将交易转换并导出到一个excel中,是应用层的service。“文件格式”在银行领域中是没有意义的,不涉及到业务规则。
将“转账“操作强加在account对象上是很别扭的,因为这个操作涉及两个账户和一些全局规则。
思考:
不同层次的service在命名和结构上会有什么区别?
module之间是低耦合的,但是在module的内部是高内聚的。
module是一种表达机制,module的选择应该取决于被划分到模块中的对象的意义。如果说模型讲述了一个故事,那么module就是这个故事的各个章节。模块的名称表达了其意义。
选择能够描述系统的module,并且使之包含一个内聚的概念集合。
找到一个可以作为module基础的概念,基于这个概念组织的module可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立的理解和分析这个概念。
module及其名称反映出领域的深层知识。
尽力避免重构module,module的更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具的使用。
一些重构原则:一个类确实依赖于另一个包中的某个类,而且本地module对该module并没有概念上的依赖关系,那么或许应该移除一个类,或者考虑重新组织module。
对象的一个最基本的概念是用数据的操作逻辑来封装数据。数据的属性无法直接访问,但是可以通过方法来访问,为什么要这么做呢?这样暴露给外界的都是方法,可以在方法里做一些逻辑。还有呢?
分层以及分层打包,会使得模型变得分散,开发人员难以还原模型原来的样子。
不要在领域模型中添加任何与领域模型所表示的概念没有紧密关系的元素。模型中的设计元素的任务是表示模型。
在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则。而且紧密关联的各组对象也要遵守一些固定规则,然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地相互干扰,从而使系统不可用。
我们需要用一个抽象来封装模型中的引用。aggregate是一组相关对象的集合,作为数据修改的单元。每个aggregate都有一个根(root)和一个边界(boundary)。边界定义了aggregate的内部都有什么。根则是aggregate中所包含的一个特定的entity。在aggregate中,根是唯一允许外部对象保持对他的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他entity都有本地标识。但是这些标识只有在aggregate内部才需要加以区别,因为外部对象除了entity之外看不到其他对象。
思考:为什么要保持这种聚集呢?这样也是在设计的时候,就尽可能的降低和其他模型对象不必要的耦合,定义清楚聚集关系和边界。避免在修改时带来很大的困难。
举例:领域:汽车修配厂的软件,使用的汽车模型
汽车是具有全局标识的entity:在修理时需要知道这辆车的唯一编号
如果我们想知道每个轮胎的里程数和磨损度,我们可能不会关心这些轮胎在这量汽车上线文之外的标志,如果更换了轮胎并将旧轮胎送到回收场,那么软件不需要跟踪它们,没有人会关心他们的转动历史。
汽车修配厂软件的语境,人们只会在数据库中查找汽车,然后临时查看下这部汽车的轮胎情况
汽车是agggregate的根entity,但是轮胎知识处于这个aggregate的边界之内。
思考:我理解就是这些配件,只需要关注他们的型号,而相同型号之间没有区别,不需要感知。但是汽车不一样,汽车有所属人,具体是哪个4s店卖出的。所以必须要关注到即使是不同型号的不同个体之间的差异,这些通过entity标识来区分。
固定规则:在数据变化时候必须保持不变的一致性规则
可以在模型中加入以下业务知识
其中第三点表示,对price的修改有可能会因为业务规则限制,导致price修改失败。
part只表示价格,而项目item中会有quantiy和price,数量和价格。
思考:还是没有明白为啥要这样设计
在创建一个对象或者创建整个aggregate时,如果创建工作很复杂,或者暴露了过多的内部结构,可以使用factory进行封装。
复杂对象的创建过程不应该交给客户端,这样客户必须要要知道对象内部结构的一些知识。
对象的创建本身可以是一个主要操作,但是被创建的对象并不适合承担复杂的装备操作。让客户端负责创建会使客户端的设计陷入混乱,而且破坏被装配对象或者aggregate的封装,导致客户与被创建对象的实现之间产生过于紧密的耦合。
增加模型,和以往的模型不同,不对应于模型中的任何事物,但是确承担领域层的部分职责。
factory:负责创建其他对象的程序元素。
应该将创建复杂对象的实例和聚合的指责转移给一个单独的对象,它是领域设计中的一部分,提供一个封装所有复杂装配操作的接口,而且这个接口应该不需要客户引用被实例化的对象的具体类,在创建aggregate时要把它作为一个整体,并确保它满足固定规则。
工厂设计方式:factory method、abstract fatcory、builder
好的工厂需要满足以下两个基本需求
这一节主要讨论,factory用在哪些需要隐藏细节的地方。这些决策通常与aggregate有关。
前者使用后者的对象,在后者的对象上创建一个factory method
通过这个账户创建了一个交易订单
brokerage account 佣金账号
factory可以避免客户与具体类之间的耦合。
trade order不在aggregate中,但是创建它需要用到brokerage account中的一些信息,所以由brokerage account创建
规则:把访问限制在aggregate内部,并确保从aggregate外部只能对aggregate临时引用
思考:临时引用是啥意思?
原因:factory可能会使一些不具有多态性的简单对象复杂化
以下情况最好使用简单的、公共的构造函数
前面介绍的模型,示例中一次只应用一种模式,但在实际的项目中,必须将他们结合起来使用。
假设我们需要为一家货运公司开发一个新的软件,最初的需求包括3项基本功能
1.跟踪客户货物的主要处理部署(什么意思?)
2.事先预约货物
3.当货物到达其处理过程中的某个位置时,自动向客户寄送发票
如果是我来设计,最开始我应该只会设计出:Cargo、Customer、Handing Event和Delivery History。下面这些Delivery Specification、Location、Carrier Movement都不会考虑到。为什么作者会设计这些类呢?下面会讲解设计Delivery Specifaction的原因。
使用Delivery Sepecification抽象出来的优点如下:
customer在运输中所承担的部分,是按照角色来划分的,比如 shipper、receiver、payer等等。customer和cargo是多对一的关系。这个地方理解有点障碍,一个cargo(货物)应该会有多个customer才对。的确是这样,看上面的定义。
Carrier movement表示由某个Carrier执行的,从一个location到另一个location的旅程。Cargo被装上Carrier后,通过Carrier的一个或者多个Carrier Movement,就可以在不同的地点之间转移。
Delivery History 反映了Cargo实际上发生的事情。
一般情况下,模型的精化、设计和实现,应该在迭代开发过程中一起进行,本章中,是从一个相对成熟的模型开始,视线中采用构造块模式,一切修改完去由需求来驱动。
目的:为了防止领域的职责与系统的其它部分的职责混杂在一起,应用layered architecture把领域层划分出来
比较直接可以看到的三个用户层的应用程序功能
这句话有点不太理解是啥含义:应用层是协调者,只是负责提问,不负责回答,回答是领域层的工作。为啥只是提问?不是回答?意思是不是说,应用层只是告知,需要做什么,实际的执行是领域层的事情?
对每个对象进行考虑,考虑这个对象是必须被跟踪的实体还是仅表示一个基本值。
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。
双向关联在设计中是最容易产生问题的。只有深刻地理解领域后,才能确定遍历方向,理解遍历方向能够使模型更深入。
在Delivery History中提供一个List对象,并把Handling Event都放到这个List对象中。
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的根。
在我们的设计中,有5个entity是Aggregate的根。因此在选择存储库时只需要考虑这个五个实体。其他对象都不能有Repository。
为什么handling event需要在一个“低争用”的事务中创建?
考虑实际场景
预订订单时
需要经常走查场景,以确保能够有效地解决应用问题&复核这些决策
会有这样的场景,customer打电话说需要更改货物的目的地。Delivery Specifaction是一个value object,创建一个新的,再使用Cargo上的setter方法把旧值替换成新值。
相同的customer的重复预订,往往是类似的。因此他们想要将旧的Cargo作为新的Cargo的原型。应用程序可以允许用户在存储库中查找一个Cargo(看上去像是历史订单的功能),然后选择一条命令来基于选中的Cargo来创建一个新的Cargo。
Cargo是一个entity,而且是Aggregate的根,因此在复制它的时候要非常小心,其边界里的每个对象或者是属性的处理都需要仔细考虑。(此时就能体现出Aggregate的好处了,只要根变动,其他aggregate内的实体和值对象都需要变更!)
复制了Cargo Aggregate边界内部的所有对象,并对副本进行了一些修改,但是并没有对边界之外的对象产生任何影响。啊 聚合&边界的好处~~~
可以在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时就创建DeliveryHistory
customerRoles = new HashMap();
}
得到一个新的Cargo,带有一个指向它自己的新的Delivery History。
货物在真实世界中每次的处理,都会有人使用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); //设置CarrierMovement
return result;
}
可以把反向指针的创建封装到Factory中,并将其放在领域层中。但是可以看另一种设计,完全消除了这种别扭的设计。
好奇怪,应该是通过Cargo找到其对应的Delivery history,然后把handling event塞到Delivery history里面。
由于在添加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是啥的。
前面的讲不太好的模型,没看太懂是啥意思,只学习下这个更好的领域模型吧。
先解释下uml图:其中空心菱形标识聚合,比如contact和customer就表示,一个customer has a contact。一个用户有多个合同。
实线&箭头,表示关联,一个类知道另一类的方法和属性。其中customer agreement 关联route specification。用户协议是和specification关联的。
通过现有的模块划分,可以很好的描述现实世界
公司给customer shipping(customer模块和shipping模块的是有关联关系的)。因此向他们寄出bill(billing和customer也有关联关系)。公司的销售和营销人员,与customer协商并签署协议,因此协议是两者之间的桥梁。操作人员负责将货物shipping到指定的目的地,后勤办公人员负责billing(处理账单),并根据Customer协议开具发票。这里理解,后勤办公人员和操作人员都是customer的一个对象。
现在完成了初始的需求和模型,要添加第一批重要的新功能。
公司需要为管理效益,制定销售计划。需要根据货物类型、出发地和目的地,或者任何可以作为分类名输入的其他因素来制定不同类型货物的运输配额。这些配额构成了各类货物的运输量目标。这样利润较低的货物就不会占满配额而导致无法运输利润较高的货物。同时避免预订量不足或者过量预订。
配额检查需要检查cargo repository以及去销售管理系统里拿到销售信息。
销售管理系统不是根据这里所使用的模型编写的。如果booking application和它直接交互,我们的应用程序就需要兼容另一个系统的设计。我们创建另一个类,让它充当我们的模型系统和销售管理系统之间的翻译,只对我们应用程序所需要的特性进行翻译,并根据我们的领域模型重新对这些特性进行抽象。这个类作为一个anticorrpution layer。会在14章进行讨论
我们需要定义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)
考虑通信开销,可以把enterprise segment 缓存到服务器中,但是被缓存的数据必须保持最新。
有可能会对其中的一个设计产生疑惑,那就是为什么不把enterprise segment的划分职责分给Cargo呢?如果enterprise segment的所有数据都来源于Cargo,那么这样的设计看上去是一个不错的选择,但是出于不同目的,可能需要对相同的entity进行不同的划分。当以税务会计的角度或者是当销售策略发生变化时,配额的enterprise segment的划分都可能发生变化。因此Cargo必须知道allocation checker。但是这完全不在其职责范围内。
正确的设计是让知道这些规则的对象来承担获取这个值的指责,而不是把这个职责施加给包含具体数据的对象。这些对象可以被分离到一个独立的strategy对象中。然后将这个对象传递给cargo。
通过一系列的修改可以得到更符合现实且更符合用户那些最重要的需求的模型。模型变得简单了,其功能性及说明性却增强了。
背景:作者在给一家投资银行开发一个大型应用程序的核心部分,该程序用于管理银团贷款。
解释银团:假设intel需要建造一座价值10亿的工厂,就需要申请巨额贷款,但任何一家借贷公司都无法独立承担,于是这些公司就组成银团。集中资源来支持这种巨额信贷。投资银行通常在银团里担当领导者的角色。负责协调各种交易和其他服务。作者的项目就是需要开发这样一个用于跟踪和支持以上整个过程的软件。
其中这个领域其实就主要有三个角色,银团作为中间商,接受各大银行的投资,并且放贷给需要的人。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以反映这个事实。
实际业务场景,loan和facility的股份可以在互不影响的前提下独立发生
解释:facility的总额是1亿美元,而借款者从中提取的第一笔loan金额是5000万美元,且3个放贷方按照各自原先承诺的facility股份来支付。
此时贷款者又贷了3000w,但是这次b公司不参与,由a额外承担剩余的股份
当借款者还钱时候,是按照实际loan的股份来进行偿还的。利息也是按照loan股份来进行分配的
但是借款者为享有facility权而支付费用时,比如信用卡的年费,这笔钱是按照facility来支付的。
这样带来的疑问是,不同的银行给了借款者不同的贷款额度,但是最后却不是按照这个额度来实际出资的,这样不会有影响吗?有点奇怪。
加深理解:投资和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的股份份额
与股份模型搭配的新贷款模型
讲他们老大为他们顶住压力,做正确的事情
作者讲第一个版本交付后,他的神经衰弱也有了好转,原来不止我一个人会因为工作神经衰弱
不要试图去制造突破,那样只会让项目陷入困境。通常只有在实现了适度重构后才有可能出现突破
开发过程中发现,提取货款、缴纳费用等业务是由一些重要的规则控制的。这些逻辑都分散在了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来变化。
深层建模的第一步,就是要设法在模型中表达出领域的基本概念,随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。
需要实现对领域的底层模型的挖掘,第一步首先是需要是被某种形式存在的隐含的概念,无论这些概念有多么原始。
要挖掘出大部分的隐含概念,需要开发人员去倾听团队语言,仔细检查设计中的不足之处,以及与专家观点矛盾的地方,研究领域相关的文献,并且进行大量的实验。
不同于原来“名词即对象”的概念,听到新单词只是个开头,我们还要进行对话,消化知识,这样才能挖掘出清晰实用的概念。
如果领域专家/软件设计者都在使用不在通用语言里的词汇,软件设计者需要警惕,那说明我们的模型还有需要改进的部分。思考、行动、改进。
示例:
团队已经开发出了可用来预定货物的有效应用程序,现在他们开始开发“作业支持”应用程序,此程序可帮助工作人员管理工作单,工作单用于安排起始地和目的地的货物装卸以及在不同货轮之间的运转。
现状:预订程序使用一个路线引擎来安排货物行程。运输过程的每段行程都作为一行数据存储在数据表中,指定了装载该货物的航次(某一货轮的某一航次)ID、装货地点以及卸货地点。
明确工单的作用:这些工单用于安排起始地和目的地的货物装卸以及在不同货轮之间的运转。
所以工作人员是需要,什么时间、在什么地点、有什么货物需要装上或者卸载的。
有点没太get到区别,所有的leg其实是都需要落库的哇,这样在工单应用程序里,员工才能知道有哪些航次,起始地和目的地是什么,如何安排人员装货和卸货。
把显式的Itinerary对象作为模型的一部分,带来的益处
哦~原始的模型里,根本没有航程和航段的模型哇,这样子怎么能work呢?
有些概念是需要挖掘的,这个时候,应该让领域专家参与到讨论中来。
探索利息计算的模型
金融公司,主要经营商业贷款和其他一些生息资产。公司开发了一个用于跟踪这些投资和收益的应用程序,通过一项一项地添加功能来使它不断的发展。每天晚上,公司会运行一个批处理脚本,用于计算当天所生成的利息和费用,并把它们相应地记录到公司的会计软件中。
accrual:累计,增加
ledger:分类账
夜间脚本会通知每个assert执行calculateAccuralsThroughDate(),其返回值是Accural的集合,而其中每笔金额都会过账到指定的分类账中。
新模型有几个优点
accrual schedule是什么东西
Ledger:总账
Daily compound interest: compound 混合物
思考矛盾的地方,如果能达成统一,那就会透过问题领域的表面获得更深层次的理解
查阅书籍理解一些专业领域的概念
讨论过程中,会尝试六七种不同的思路,找到一个看起来足够清晰且实用的概念,并在模型中尝试它
面向对象范式会引导我们去寻找和创造特定类型的概念,所有事物(即使像是“应计费用”这种非常抽象的概念)及其操作行为是大部分对象模型的主要部分。
约束是模型概念中非常重要的类别。它们通常是隐式出现的,将他们显式表现出来可以极大的提高设计质量。
将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这种约束。现在这个约束条件就是一个“有名有姓”的概念了。可以用这个名字来讨论它。
下面这些信号,表明约束的存在正在扰乱它的“宿主对象”的设计
如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出,但是在模型中却不明显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合。
思考:约束/策略,也可以成为一个类,来显式的处理
过程应该被显式地表达出来,还是应该被隐藏起来,区分方法很简单,它是经常被领域专家提起,还是仅仅被当作计算机程序机制的一部分?
约束和过程是模型概念中,应用范围很广的概念,当我们使用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
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 拖欠发票规格,来对一些发票进行评估,这个规格用过后,就丢弃掉了,这样只需要指定评估日期就可以了。
specification最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下三个目的中的至少一个目的,我们可能需要来指定对象的状态
这三种用法,我认为是三种场景(验证,选择和根据要求来创建)从概念层面上来讲是相通的,如果没有如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();
}
}
为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要要让人们乐于使用,而且易于作出修改,这就是 柔性设计(supple design)
目的展现接口?
客户开发人员想要有效的使用对象,必须知道对象的一些信息,如果接口没有开发人员这些信息,那么他就必须深入研究对象的内部机制,以便于理解。这样就失去了封装的大部分价值。
如果开发人员,为了使用一个组件,而必须要去研究它的实现,那么就失去了封装的价值。
当我们把概念显式地建模为类或者方法时候,为了真正从中获取价值,必须为这些程序元素赋予一个能够反映他们的概念的名字,类和方法的名称为开发人员之间的沟通创造了很好的机会,也能够改善系统的抽象。
设计中所有公共元素共同构成类接口,每个元素的名称都提供了一次揭示设计意图的机会。类型名称、方法名称和参数名称组合在一起,共同形成了一个intention-revealing interface(释义接口)
命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的,这样可以使客户开发人员不必去理解内部的细节,这些名称应该和通用语言一致。在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。
无副作用的功能
副作用这个词暗示着,“意外的结果”,但是在计算机科学中,任何对系统状态产生的影响都叫副作用。为了便于讨论,我们把它的含义缩小一下,任何对未来操作产生影响的系统状态的改变都可以称之为副作用
副作用这个词强调了这种交互的不可避免性。
多个规则或者计算组合的相互作用所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及所有派生操作的实现。
如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就收到了限制
如果没有了可以安全地预见到结果的抽象,开发人员就必须限制“组合爆炸”,这就限制了系统行为的丰富性。
这就要求开发人员既保持接口的抽象,又能安全地预见结果的抽象。
尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令隔离到不反悔领域信息的,非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到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);
}
}
函数的计算结果很容易理解,也很容易测试,因此可以安全的使用或者与其他操作进行组合,由于它的安全性很高,因此复杂的调色逻辑真正被封装起来了。
模型或者设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复,外部接口无法全部给出客户可能关心的信息。另一方面,把类和方法分解开也不行,这会使客户更复杂。迫使客户对象去理解各个小部分是如何组合在一起的。
粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。
通过反复重构最终会实现柔性设计,随着代码不断适合新理解的概念或需求,conceptual contour也就逐渐形成了。
高内聚低耦合这一对基本原则都起着重要的作用,这两条原则既适用于代码,也适用于概念。在做每个决定时,都要问自己:这是根据当前模型和代码中的一组特定关系作出的权宜之计呢?还是反映了底层领域的某种轮廓。
寻找在概念上有意义的功能单元,这样可以使得设计既灵活又易懂。
把设计元素(操作、接口、类和aggregate)分解为内聚得单元。在这个过程中,你对领域中的一切重要划分的直观认识也要考虑在内。在连续的重构过程中,发生变化和保证稳定的规律性。并寻找能够解释这些变化模式的底层conceptual contour。使模型与领域中的那些一致的方面相匹配。
Accrual schedule:应计时间表
增加新的需求:利息付款和手续费付款实际上使用相同的规则。新模型可以很自然的使用payment类。
payment类里包含:date(日期)、amount(金额)、legerName(贷方姓名)
如果使用原来的模型,两个payment history类之间必然会出现重复(这个难题可能使得开发人员意识到payment类应该被共享,这样就会从另一条途径得到类似的模型),新元素之所以能够很容易就被添加进来了,真正的原因是经过前面的重构,设计能够很好地与领域的基本概念产生吻合。
孤立的类
互相关联使模型和设计都变得难以理解、测试和维护。而且,互相依赖性,很容易越积越多。
每个关联都是一种依赖性,要想理解一个类,必须理解它与哪些对象有联系,与这个类有联系的其他对象还会与更多的对象发生联系。这些联系也必须要弄清楚,每个方法的每个参数的类型也是一个依赖性,每个返回值也都是一个依赖性。
如果有一个依赖关系,我们必须同时考虑两个类以及它们之间的关系的本质。如果某个类以来另外两个类,就必须考虑这三个类中的每一个、这个类与其他两个类之间的相互关系的本质,以及这三个类可能存在的其他相互关系
module和aggregate的目的都是为了限制互相依赖的关系网。当我们是别处一个高度内聚的子领域,并把它提取到一个module中的时候,一组对象也随之与系统的其他部分解除联系。这样就可以限制呼吸那个联系的概念的数量。但是即使把系统分成了各个module。如果不严格控制module内部的依赖,也一样会让我们耗费很多精力去考虑依赖关系。
我们应该对每个依赖关系提出质疑,直到证实它确实表示对象的基本概念为止,这个仔细检查依赖关系的过程从提取模型概念本身开始。然后需要注意每个独立的关联和操作,仔细选择模型和设计能够大幅度减少依赖关系—常常能减少到0。(这怎么可能呢??)
低耦合是对象设计的一个基本要素,尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变成完全孤立的了,每个这样的孤立的类都极大的减轻了因理解module而带来的负担。
目的是不是消除所有的依赖,而是消除所有不重要的依赖,当无法消除所有的依赖关系时,每清除一个依赖对开发人员而言都是一种解脱,使它们能够集中精力处理剩下的概念依赖关系。
尽力把最复杂的计算提取到standalone class(孤立的类)中,可能实现此目的一种方法是把具有最紧密联系的类中的所有value object建模出来。
闭合操作
在适当的情况下,在定义操作时让它的返回类型与其他参数类型相同,如果实现者的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。
开发时,尽量不引入其他类型,增加开发者的理解负担。
声明式设计:把程序或程序的一部分写成一种可执行的规格(spcification).
使用声明式设计时,软件实际上是由一 些非常精确的属性描述来控制的。声明式设计有多种实现方式。比如,可以通过反射机制来实现,或者在编译时通过代码生成来实现(根据声明来自动生成传统代码),
限制:
本章给出了一系列技术,用于澄清代码意图,使得使用代码的后果变得显而易见,并且解除模型元素的耦合。
介绍几种主要的方法,然后给出扩展的示例。显示如何把这些模型结合起来使用,并用于处理更大的设计。
如果模型的某个部分可以被看作是专门的数学,那么可以把这部分分离出来。如果应用程序实施了某些用来限制状态改变的复杂规则,那么可以把这部分提取到一个单独的模型中,或者提取到一个允许声明规则的简单框架中。重点突击某个部分。
在商业领域,会计的一些概念,是使用已久的,直接使用就行
股份/借款的例子,缺乏概念模型:share pie
最开始设计成entity,share pie的标识在loan内部
后来改成value object
变成操作闭合,不增加share pie,或者向它添加share,而只是把两个share pie加起来,结果是一个新的,更大的share pie,prorate()操作,按比例的。
分析模式:是一种概念集合,用来表示业务建模中的常用构造,可能只与一个领域有关,也可能跨越多个领域。
分析模式不是技术解决方案,只是用来指导人们设计特定领域中的模型
基本没get到是啥子意思
设计模式:并不是像链表和散列表那样可以被封装到类中并供人们直接重用的设计,也不是直接用于整个应用程序或者子系统的复杂的、专用于领域的设计。本书中的设计模式是对一些交互的对象和类的描述,我们通过定制这些对象和类来接觉特定上下问中的一半设计问题。
为了在领域驱动的设计中充分利用这些模式,我们必须同时从两个角度看待它们:从代码的角度看它们是技术设计模式,从模型的角度来看它们就是概念模式。
通过composite(组合)和strategy(策略)这两种模式来讲解,用一些经典的设计模式来解决领域问题
定义了一组算法,将每个算法封装起来,并使它们可以互换。
我们需要把过程中的易变部分提取到模型的一个单独的“策略”对象中。将规则与它所控制的行为区分开。按照strategy设计模式来实现规则,或可替换的过程。策略对象的多个版本表示了完成过程的不同方式。
传统上,人们把strategy模式看作是一种设计模式,这种观点的侧重点是它替换不同的算法的能力。而把它看作领域模型的侧重点,是其表达概念的能力,这里的概念通常是指过程或者策略固资规则。
解决条件判断太多的方法是,把这些起调节作用的参数分离到strategy中,这样它们就可以被明确地表示出来,并作为参数传递给routing service。
领域中一个至关重要的规则明确地显示出来了。在构建itinerary时用于选择leg的基本规则。它传达了这样一个知识:路线选择的基础是航段的一个特定属性,这个属性最后可归结为一个数字。这样就可以在领域语言中,用一句简单的话来定义Routing service的行为:routing service根据所选定的strategy来选择leg总规模最小的itinerary。
由多个部分组成的重要对象,这些部分本身又由其他一部分组成,进而又由其他部分组成。
当嵌套容器的关联性没有在模型中反映出来时,公共行为必然会在层次结构的每一层重复出现,而且嵌套也变得僵化。
定义一个把composite的所有成员都包含在内的抽象类型,在容器上实现一些用来查询信息的方法,这些方法可用来收集与容器内容有关的信息。“叶”节点基于它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。
不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的精化方案。
当发生了以下情况时,就应该重构
通过重构得到更深层理解是一个持续不断的过程。人们可能会发现一些隐含的概念,并把它们明确地表示出来。
无法通过分析对象来理解系统时,就需要掌握一些操作和理解大模型的技术。本书的这一部分将介绍一些原则,遵循这些原则,就可以对一些十分复杂的领域进行建模。
企业在概念和实现上把系统分解为较小的部分。问题是如何在不损害集成利益的前提下完成这种模块化的过程。从而使系统的不同部分能够进行相互操作。以便使各种业务相互协调。
这一部分探索了三个大的主题:上下文、精炼和大比例结构
上下文:最基本的主题,无论大小、成功的模型都必须在逻辑上保持整体的一致,不能有互相矛盾或重叠的定义。通过为每个模型显式地定义一个bounded context。然后在必要的情况下定义它与其他上下文的关系,建模人员就可以避免使用模型变得缠杂不清。
精炼:通过精炼可以减少混乱,并且把注意力集中到正确的地方。人们通常在领域的一些次要问题上花费了太多的精力。整体领域模型必须要突出系统中最有价值和最特殊的那些方面。
大比例结构:用来描述整个系统,在一个非常复杂的模型中,人们可能会”只见树木,不见森林“。如果不沿着一个主题来应用一些系统级的设计元素和模式的话。关系仍然可能非常混乱。概要介绍几种大比例结构的方法,然后详细讨论其中的一种模式–responsibility layer(职责层),通过这个示例来探索大比例结构的含义。
模型最基本需求是它应该保持内部的一致性、术语总具有相同的一意义且不包含互相矛盾的规则。尽管我们很少明确的考虑这些需求。模型的内部一致性由叫做“统一”,这样每个术语都不会有模棱两可的意义,也不会有规则冲突。
在大型项目中尝试把所有软件统一到一个模型中,可能会有下面的风险
权力上的划分和管理级别的不同也要求把模型分开。
通过预先决定什么应该统一,并实际认识到什么不能统一,就能够创建一个清晰的、共同的视图。
需要用一种方式来标记处不同模型之间的边界和关系,需要有意识的选择一种策略,并一致地遵守它。
本章将介绍一些用于识别、沟通和选择模型边界以及关系的技术。限界上下文定义了每个模型的应用范围,而上下文图则给出了项目上下文以及它们之间关系的总体视图。
区分出那些具有共享内核的紧密关联的上下文,以及那些具有独立方式的松散耦合的模型。
一个模型只在一个上下文中使用
说来半天也没教怎么去划分限界上下文。
定义完一个bounded context后,必须让它保持合理化
极限编程(xp)在这样的环境中真正显示了其特性。很多xp实践都是针对在很多人频繁更改设计的情况下如何维护设计的一致性这个特定问题而出现的。xp是一种非常适合在bounded context中维护模型完整性的形式,但是无论是否使用xp,都很有必要采取一些continuous integration过程。
持续集成:指把一个上下文中的所有工作足够频繁地合并到一起,并使它们经常保持一致。以便当
模型发生分裂时,可以迅速发现并纠正问题。持续集成也有两个级别的操作
大部分有效的方法都具有下面的特征
建立一个经常把所有代码和其他实现工件合并到一起的过程,并通过自动测试来快速查明模型的分裂问题。严格坚持使用通用语言,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。
描述模型之间的接触点,明确每次交流所需的转换,并突出任何共享的内容。画出现有的范围。为稍后的转换做好准备。
两个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构成,很容易测试。
独立自主
集成总是代价高昂,有时却获益却很小
声明一个与其他上下文毫无关联的bounded context,使开发人员能够在这个小范围内找到简单、专用的解决方案(而不是一定要强行集成)
separate way开发的模型是很难合并的。如果最终仍然需要集成,那么转换层将是必要的,而且坑很复杂。
定义一个协议,把你的子系统作为一组service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
其他子系统就变成了与open host的模型相连接,而其他团队则必须学习host团队所使用的专用术语。在某些情况下,使用一个众所周知的published language(公开发布的语言)作为交换模型可以减少耦合并简化理解。
一个良好文档化的,能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
举例:xml。cml
需要为正在设计中的整个设计使用一个bounded context。你可能希望采用shared kernel模式,并把几组相对独立的功能划分到bounded context中。在这些bounded context中,如果有两个上下文之间的所有依赖性都是单向的,就可以建成为customer/supplier development team。
集成和不集成
bounded context策略的选择,将对部署产生影响。在分布式系统中,一个好的做法是把context之间的转换层保持在单个进程中,这样就不会出现多个版本共存的情况。
围绕当前组织结构来加强团队的工作。在context中改进continuous integration。把所有分散的转换代码重构到 anticorrruption layer中。
像建模和设计的其他方面一样,有关bounded context的决策也是可以改变的。分割context是很容易的,但是合并它们或者改变它们的关系却很难。下面将介绍几种有代表性的修改。