前言:有赞scrm的复杂度越来越高,作为业务中台,为了给业务方爸爸们提供更好更快的底层能力支持,开始走向了平台化。之前听从部门架构师的建议,没有一定的业务积累,不要学习DDD相关的内容,当时只是很零碎的看了一些领域建模的文章。这次正好有幸作为第一批参与平台化建设的人,斗胆系统性的开始学习领域建模。
书中其实也在权衡技术细节与DDD的实现,有非常多的取舍的地方,所以完美的DDD实现在现有技术组件下几乎是不存在的。这反而有种为了实现DDD而实现DDD的感觉,而关于DDD到底能带给我们什么,由于我没有实际DDD的经验,所以我并不能很真实的感觉到。
1、将领域专家引入到团队
领域专家并不是一个职位,他可以是精通业务的任何人。他们可能了解更多的关于业务领域的背景知识,他们可能是软件产品的设计者,甚至有可能是销售员。
在实施DDD的过程中,最好将那些不怎么使用技术语言的人加进自己的团队。就像你会向他们学习一样,他们也会向你学习。
2、什么是领域模型
领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。
3、为什么我们需要DDD
4、难以捉摸的业务价值:P6
5、DDD如何帮助我们:P7
6、处理领域复杂性:P8
4、什么样的软件系统值得做出DDD投入
应该将DDD应用在最重要是业务场景下,值得投入的是那些最重要的、复杂的东西,因为这些东西将带来可观的回报。DDD的作用是简化,而不是复杂化。在使用DDD时,我们应该采用最简单的方式对复杂领域进行建模,而不是使问题变得更加复杂。
5、如何评价业务领域的复杂性
可参考书中的DDD计分卡。(P8)
6、什么是通用语言
通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流。事实上,团队中每个人都使用相同的通用语言。不管你再团队中的角色如何,只要你是团队的一员,都将使用通用语言。团队成员通过讨论、参考资料、引用标准、查阅字典等对通用语言进行改进。有时我们发现,有些我们曾经认为能很好表达业务的词汇不再适用了,而另外的一些词汇具有更好的效果。
7、使用通用语言的几点注意事项
8、DDD的业务价值
9、实施DDD所面临的挑战
10、如何选择开发方式
1、什么是领域
从广义上讲,领域即是一个组织所做的事情以及其中所包含的一切。商业机构通常会确定一个市场领域模型,然后在这个市场中销售产品和服务。每个组织都有它自己的业务范围和做事方式。这个业务范围以及其中所进行的活动便是领域。当你为某个组织开发软件时,你面对的便是这个组织的领域。这个领域对于你来说应该是明晰的,因为你在这个领域中工作。
2、核心域、支撑子域、通用子域
3、问题空间
问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速的浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
4、解决方案空间
解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。
5、实施解决方案之前,需要对问题空间和解决方案空间进行评估
6、解决方案空间在很大程度上受到现有系统和技术的影响,我们应该根据分离的限界上下文仔细的思考。考虑的问题:P51
7、限界上下文
限界上下文是一个显式边界,领域模型便存在于边界之内。在边界内,通用语言中的所有术语和词组都有特定的含义,而模型需要准确地反映通用语言。
8、如何划分限界上下文
限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。模块、聚合、领域事件、领域服务等基础部件都属于限界上下文。限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。
限界上下文的划分不因因为一些平台、框架、基础设施或者开发任务拆分等因素来创建,有时,我们可以使用模块来避免创建一些微小的限界上下文。模块可以将多个限界上下文减少到一个,因为我们可以用战术化的手段来管理团队任务分配,而不是限界上下文。
实施DDD的底线是,采用语言驱动,而不是技术手段。
应该尽量保证一个团队,一个限界上下文。
1、上下文映射图的表达方式
一个项目的上下文映射图可以用两种方式来表示。比较容易的一种是画一个简单的框图来表示两个或多个限界上下文之间的映射关系。该框图表示了不同的限界上下文在解决方案空间中是如何通过集成相互关联的。另一种更详细的方式是通过限界上下文集成的源代码实现来表示。
2、多个限界上下文之间的关系,各个团队之间的关系(P79)
3、对于一个非常详细的上下文映射图,我们很有可能无法对其进行实时更新。将映射图贴在墙上是有好处的,这样可以方便团队成员之间的交流。保持简单性和敏捷性,拒绝繁文缛节,这样我们所创建的上下文映射图将对项目起推动作用,而不是阻碍作用。
1、DDD的一大好处便是它并不需要使用特定的架构。
由于核心域位于限界上下文中,我们可以在整个系统中使用多种风格的架构。有些架构包围着领域模型,能够全局性的影响系统,而有些架构则满足了某些特定的需求。
2、选择合适的架构风格和架构模式
架构风格阐述如何实现某种架构,架构风格之于架构就像设计模式之于设计一样,它将不同架构实现所共有的东西抽象出来,例如客户端-服务器架构风格、分布式对象风格;而架构模式则关注一种架构中的某个方面,架构模式比设计模式更加宽泛。
在选择架构风格和架构模式时,我们应将软件质量考虑在内,而同时,避免滥用架构风格和架构模式,采用的架构是用来减少风险的,而不是增加失败风险。架构风格和模式的选择受到功能需求的限制,比如用例和用户故事,用例驱动架构在当今软件开发中依然适用。
3、非常精彩的实际项目的架构演进流程(P100)
关键词:DI、前后端分离、REST、六边形架构(端口与适配器)、SOA、CQRS、事件驱动架构、long-running process、Sagas、Event Sourcing
4、分层架构
原则:每层只能与位于其下方的层发生耦合。(较低层也是可以与较高层发生耦合的,但只局限于采用观察者模式或者调停者模式)
严格分层架构:某层只能与直接位于其下方的层发生耦合;
松散分层架构:允许任意上方层与任意下方层发生耦合。
5、依赖倒置原则结合DDD的分层架构(P108)
结合后的现象:当我们在分层架构中采用依赖倒置原则时,可能会发现,事实上已经不存在分层的概念了。无论是高层还是低层,他们都只依赖于抽象,好像把整个分层架构给推平了一样。
6、六边形架构(端口与适配器)(P110)
核心是,系统的输入、输出均通过适配器实现,内部应用层的统一的公共API接受适配器的请求。应用层的功能是根据应用程序的功能需求来创建用例,而不是客户数量或输出机制。
六边形架构可以用来支持系统中的其他架构。比如可能采用SOA架构、REST或者事件驱动架构;也有可能采用CQRS;或者数据网织或基于网格的分布式缓存;还有可能采用Map-Reduce这种分布式并行处理方式。
7、SOA的精神
8、CQRS的指导原则
9、事件驱动架构:基于消息的管道和过滤器处理过程的基本特征
10、事件驱动架构:设计长时处理过程(Saga)的几种方法(已支持Saga的消息机制:NServiceBus、MassTransit)
11、事件源技术上的优势(附录A详尽的阐述了如何在聚合上实现事件源)
12、事件源业务上的优势
13、数据网织和基于网格的分布式计算(P143)
1、为什么要使用实体
DDD并不总能满足我们的业务需求,有时,一个基于CRUD的系统会更加合适,节约了时间和金钱。但是,随着软件复杂性的增加,我们就越能体会到由错误的工具选择所带来的限制,这时维护一个基于CUD的系统可能是非常昂贵的。由于只从数据开发,CRUD系统是不能创建出好的业务模型的。
2、唯一标识:创建实体身份标识的策略
3、委托标识
有些ORM工具,比如Hibernate,通过自己的方式来处理对象的身份标识。Hibernate更倾向于使用数据库提供的机制,比如使用一个数值序列来生成实体标识。如果我们自己的领域需要另外一种实体标识,此时这两者将产生冲突。为了解决这个问题,我们需要使用两种标识,一种为领域所用,一种为ORM所使用,在Hibernate中,这被称为委派标识。
4、发现实体及其本质特征(这方面书中的方法论还是少了些,从书中并不能很好的知道分析的方法,还是得从实操中获得积累)
1、值对象的优点
值对象用于度量和描述事物,我们应该尽量使用值对象来建模而不是实体对象。即便一个领域概念必须建模成实体,在设计时也应该更偏向于将其作为值对象容器,而不是子实体容器。因为我们可以非常容易的对值对象进行创建、测试、使用、优化和维护。
2、如何确定一个领域概念应该建模成一个值对象?
当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。
3、值对象的特征
4、划分值对象范围的方法
如果你视图将多个属性加在一个实体上,但这样却弱化了各个属性之间的关系,那么此时你便应该考虑将这些相互关联的属性组合在一个值对象中了。每个值对象都是一个内聚的概念整体,它表达了通用语言中的一个概念。如果其中一个属性表达了一种描述性概念,那么我们应该把与该概念相关的所有属性集中起来。如果其中一个或多个属性发生了改变,那么可以考虑对整体值对象进行替换。
5、最小化集成
可能的情况下尽量使用值对象来完成限界上下文之间的集成,这对于许多需要消费标准类型的上下文来说都是适用的。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。
6、根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。
无论你使用上面技术来完成数据建模,数据库实体、主键、引用完整性和索引都不能用来驱动你对领域概念的建模。DDD不是关于如何根据范式来组织数据的,而是在一个一致的限界上下文中建模一套通用语言。在这个过程中,应该尽量避免数据模型从领域模型中泄漏到客户端中。
6、持久化值对象
文中只举例了Hibernate的相关方法,其他ORM框架需要再进行查阅。
1、领域服务(领域服务不是应用服务)
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。
不要滥用领域服务,滥用领域服务将导致贫血领域模型这种反模式。
2、应用服务
应用服务只用于协调任务,调用单个业务操作(领域服务),而由该业务操作去处理所有的业务细节。我们绝不能将业务逻辑放到应用层,即使业务逻辑非常简单,但它依然是业务逻辑。虽然我们不会讲业务逻辑放在应用层,但是应用层却可以作为领域服务的客户端。
3、领域服务命名?采用独立接口?
P245(不一定要遵循 Java EE规范)
1、领域事件
领域专家所关心的发生在领域中的一些事件。将领域中所发生的活动建模成一系列的离散事件,每个事件都用领域对象来标识。领域事件是领域模型的组成部分,表示领域中所发生的事情。
2、领域专家和领域事件
虽然领域专家在起初可能意识不到所有类型的领域事件,但是通过讨论之后,他们是应该能够了解到其中的原因的。当团队成员对领域事件达成一致之后,领域事件便是“通用语言”的正式组成部分了。
3、应用服务控制着事物。不要在事件通知过程中修改另外一个聚合试了,因为这样破坏了聚合的一大原则:在一个事务中,只对一个聚合进行修改。
4、相对分布式事务来讲,领域事件是一种更轻量级的最终一致性的实现方式。
5、如何保证领域模型存储和事件存储(消息设施所使用的持久化存储)之间的一致性?
6、时延与自治服务和系统
有些业务服务可能需要更高的吞吐量,此时我们需要好好的考虑最大容许时延,系统的架构应该满足在事件时延上的需求。对于自治服务和支持它们的消息设施来说,我们应该在可用性和可伸缩性上下足功夫,以便更好地完成那些非功能性的需求。
7、事件存储的方式
8、REST风格事件通知的优缺点
9、通过消息中件发布事件通知
1、模块的作用
在DDD中,模型中的模块表示了一个命名的容器,用于存放领域中内聚在一起的类,将类放在不同的模块中的目的在于达到松耦合性。由于DDD中的模块并不是一个通用的存储区域,因此对其进行适当的命名是重要的。事实上,模块名是通用语言的重要组成部分,应该反映出它们在领域中的概念。
2、设计模块的简单原则
3、先考虑模块,再是限界上下文
有时,通用语言可以很好的帮助我们做出正确的选择。但是另外的时候,其中的术语将变得非常含糊。在这种情况下,我们并不清楚如何划分上下文边界。此时,我们可以首先将它们放在一起,使用模块来对模型进行划分,而不是限界上下文。
但是,这并不意味着我们就应该限制对限界上下文的创建。我们应该通过通用语言的需求来划分模型边界。你应该知道,限界上下文不是用来代替模块的。使用模块的目的在于组织那些内聚在一起的领域对象,对于那些内聚性不强或者没有内聚性的领域对象来说,我们应该将它们划分在不同的模块中。
1、设计聚合的原则
2、一致性处理的指导原则
对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性,当然此时依然需要遵循其他聚合原则。如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。
3、打破原则的理由
4、迪米特法则
强调了“最小知识”原则。考虑一个客户端对象需要调用系统中其他对象的行为方法的场景,此时我们可以将后者称为服务对象。在客户端对象使用服务对象时,它应该尽量少的知道服务对象的内部结构。客户端对象不应该知道任何关于服务对象属性的信息。客户端对象可以根据表层接口调用服务对象上的命令方法。然而,客户端对象不应该渗入到服务对象的内部。如果客户端所需服务位于服务对象的内部,那么此时客户端对象不应该访问这样的服务。对于服务对象来说,它只应该提供表层接口,在接口方法被调用时,它将操作委派给内部方法以完成功能。
对迪米特法则做一个简单的总结:任何对象的任何方法只能调用以下对象中的方法:①该对象自身;②所传入的参数对象;③它所创建的对象;④自身所包含的其他对象,并且对那些对象有直接访问权。
5、“告诉而非询问”原则
一个对象不应该被告知如何执行操作。对于客户端来说,这里的“非询问”表示:客户端对象不应该首先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来“告诉”服务对象所要执行的操作。该原则和迪米特原则存在相似之处,但是使用起来更加简单。
6、实现原则
1、使用工厂的主要动机:
将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足。
2、聚合根中的工厂方法的好处
1、严格来讲,只有聚合才拥有资源库。存在两种类型的资源库设计:面向集合设计和面向持久化设计。
2、面向集合资源库
我们可以将面向集合的资源库看成是一种传统的方式,因为它体现了原生DDD资源库模式的基本思想。这种资源库模拟了一个集合,或者至少模拟了集合上的标准接口。此时,从资源库的接口来看,我们根本看不出其背后还存在着持久化机制,也感觉不到我们是在向存储区域中保存数据。
3、面向集合资源库精要
一个资源库应该模拟一个set集合。无论采用什么类型的持久化机制,我们都不应该允许多次添加同一个聚合实例。另外,当从资源库中获取到一个对象并对其进行修改时,我们并不需要“重新保存”该对象到资源库中。考虑下集合的情形,要修改其中的一个对象,我们只需要先从集合中获取到该对象的引用,然后在该对象上执行行为方法即可。
4、持久化机制支持面向集合资源库的方法:
5、面向集合资源库的高性能ORM工具:TopLink、EclipseLink
6、面向持久化资源库精要
在向数据存储中添加新建对象或修改既有对象时,我们都必须显示的调用put方法,该方法将以新的值来替换先前关联在某个键上的原值。这种类型的数据存储可以极大的简化对聚合的读写。正因如此,这种数据存储也称为聚合存储或面向聚合数据库。
7、额外的行为
8、管理事务的位置
通常来说,我们将事务放在应用层中。然后为每个主要的用例创建一个门面,门面中的业务方法通常都是粗粒度的,常见的情况是每一个用例流对应一个业务方法。业务方法对用例所需操作进行协调
对事务的管理绝对不应该放在领域模型和领域层中。通常来说与,与领域模型相关的操作都非常细粒度的,以至于无法用于管理事务,领域模型也不应该意识到事务的存在。
9、使用事务的警告
不要过度的在领域模型上使用事务。我们必须慎重的设计聚合以保证正确的一致性边界。有时,在测试环境下,在单个事务中修改多个聚合可能工作得很好,但是在产品环境下,却有可能出现由并发所导致的事务失败。
10、资源库VS数据访问对象(DAO)
一个DAO主要从数据库表的角度来看待问题,并且提供CRUD操作。表模块、表数据网关、活动记录这样的模式应该用于事务脚本程序中,需要与领域模型分离开来对待,这些与DAO相关的模式通常只是对数据库表的一层封装。
资源库和数据映射器则更加偏向于对象,因此通常被用于领域模型中。通常来说,你可以将资源库当做DAO来看待。但是注意一点,在设计资源库时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于将领域当作模型来看待,而不是CRUD操作。
1、集成限界上下文的方式
2、分布式系统原则
3、通过消息集成限界上下文,长时处理过程的状态机和超时跟踪器。
4、防腐层
1、应用程序与领域模型的关系
领域模型通常位于应用程序的中心位置。应用程序通过用户界面向外展示领域模型的概念,并且允许用户在模型上执行各种操作。用户界面使用应用服务来协调用例任务,管理事务,并执行一些必要的安全授权。另外,用户界面、应用服务和领域模型依赖于企业级的特定平台设施的支持。这些基础设施的实现细节通常包含组件容器、应用程序管理、消息系统和数据库等。
2、什么是应用程序
书中使用的“应用程序”标识那些支撑核心域模型的最近,通常包括领域模型本身、用户界面、内部使用的应用服务和基础设施组件等。至于这些组件中应该包含些什么,这是根据应用程序的不同而不同的,并且有可能受到所有架构的影响。
3、用户界面系统的类型(P469)
4、应用服务(P478)
将应用服务于领域服务等同起来是错误的,它们并不相同。我们应该将所有的业务领域逻辑放在领域模型中,不管是聚合、值对象或者领域服务;而将应用服务做成很薄的一层,并且只使用它们来协调对模型的任务操作。
5、基础设施(P489)
基础设施的职责是为应用程序的其他部分提供技术支持。这里,虽然我们避免对分层的讨论,但是保持着依赖倒置原则的心态依然是有用的。因此,从架构上讲,无论基础设施位于什么地方,只要它的组件依赖于用户界面、应用服务和领域模型中的接口,而这些接口又需要特殊的技术支持,那么它都能工作得很好。这样,在应用服务获取资源库时,它只会依赖于领域模型中的接口,而实际使用的则是基础设施中的实现类。
资源库的实现被放在了基础设施层中,因为它们负责处理数据存储,而这些不属于模型的职责。你可以使基础设施层实现那些与消息相关的接口,比如消息队列和E-mail等。如果还有一些特殊的用户界面组件来处理诸如图表之类的展现,那么它们也应该放在基础设施层中。
附录A:聚合与事件源 A+ES
P495