Spiga

分类为编程思维的文章

DDD(五):聚合和聚合根

2015-12-09 11:40:49

摘要:我们先回顾下上一讲,在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(BoundedContext)中,并在限界上下文内完成领域建模。 那你知道为什么要在限界上下文和实体之间增加聚合和聚合根这两个概念吗?它们的作用是什么?怎么设计聚合?这就是我们这一讲重点要关注的问题。 聚合 在 DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。 那聚合在其中起什么作用呢? 举个例子。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作,朝着一个最大的目标前进,发挥出更大的力量。 领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。 你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。 聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。 聚合在DDD分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。 聚合根 聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。 传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,…… 阅读全文

DDD(四):实体和值对象

2015-11-26 22:26:30

摘要:这两个概念都是领域模型中的领域对象。它们在领域模型中起什么作用,战术设计时如何将它们映射到代码和数据模型中去?就是我们这一讲重点要关注的问题。 另外,在战略设计向战术设计过渡的这个过程中,理解和区分实体和值对象在不同阶段的形态是很重要的,毕竟阶段不同,它们的形态也会发生变化,这与我们的设计和代码实现密切相关。 接下来,我们就分别看看实体和值对象的这些问题,从中找找答案。 实体 我们先来看一下实体是什么东西? 在 DDD中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。没理解?没关系!请继续阅读。 实体的业务形态 在DDD不同的设计过程中,实体的形态是不同的。在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。你可以这么理解,实体和值对象是组成领域模型的基础单元。 实体的代码形态 在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。 实体的运行形态 实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID一直保持不变,它始终是同一个商品。 实体的数据库形态 与传统数据模型设计优先不同,DDD是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。 在领域模型映射到数据模型时,一个实体可能对应 0 个、1个或者多个数据库持久化对象。 大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置…… 阅读全文

DDD(三):限界上下文

2015-11-13 18:42:05

摘要:在 DDD领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在DDD中就出现了“通用语言”和“限界上下文”这两个重要的概念。 这两者相辅相成,通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。你是不是感觉这么描述很抽象?没关系,接下来我会给你一一详细讲解。 在这之前,请你先看这样两个问题,这也是今天内容的核心。 为什么要提出限界上下文的概念(也就是说除了解决交流障碍这个广义的原因,还有更具体的吗)? 限界上下文在微服务设计中的作用和意义是什么? 什么是通用语言? 为了更好地理解限界上下文,回答这两个问题,我们先从通用语言讲起。 怎么理解通用语言这个概念呢?在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。 那么,通用语言的价值也就很明了了,它可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。 但是,对这个概念的理解,到这里还不够。 通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。 通用语言贯穿DDD的整个设计过程。作为项目团队沟通和协商形成的统一语言,基于它,你就能够开发出可读性更好的代码,将业务需求准确转化为代码设计。 下面我带你看一张图,这张图描述了从事件风暴建立通用语言到领域对象设计和代码落地的完整过程。 在事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个项目团队统一语言的过程。 通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。 微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。 这…… 阅读全文

DDD(二):领域、子域、核心域、通用域和支撑域

2015-11-05 18:53:15

摘要:DDD的知识体系提出了很多的名词,像:领域、子域、核心域、通用域、支撑域、限界上下文、聚合、聚合根、实体、值对象等等,非常多。这些名词,都是关键概念,但它们实在有些晦涩难懂,可能导致你还没开始实践 DDD就打起了退堂鼓。因此,在基础篇中,我希望能带着你一起做好实践前的准备工作。 除此之外,这些名词在你的微服务设计和开发过程中不一定都用得上,但它可以帮你理解DDD 的核心设计思想和理念。而这些思想和理念,在 IT 战略设计、业务建模和微服务设计中都是可以借鉴的。 今天就围绕以上这些DDD 关键概念来彻底理清它们与微服务的关系,了解它们在微服务设计中的作用。今天重点了解 DDD的领域、子域、核心域、通用域和支撑域等重要概念。 如何理解领域和子域? 我们先看一下汉语词典中对领域的解释:“领域是从事一种专门活动或事业的范围、部类或部门。”百度百科对领域的解释:“领域具体指一种特定的范围或区域。” 两个解释有一个共同点——范围。对了!领域就是用来确定范围的,范围即边界,这也是DDD 在设计中不断强调边界的原因。 在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD的领域就是这个边界内要解决的业务问题域。 既然领域是用来限定业务边界和范围的,那么就会有大小之分,领域越大,业务范围就越大,反之则相反。 领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。 我们知道,DDD是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂度。那么面对错综复杂的业务领域,DDD是如何使业务从复杂变得简单,更容易让人理解,技术实现更容易呢? 其实很好理解,DDD的研究方法与自然科学的研究方法类似。当人们在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。当所有问题子域完成研究时,我们就建立了全部领域的完整知识体系了。 我们来看一下上面这张图。这个例子是在讲如何给桃树建立一个完整的生物学知识体系。初中生物课其实早就告诉我们研究方法了。它的研究过程是这样的。 第一步:确定研究对象,即研究…… 阅读全文

DDD(一):为什么选择DDD

2015-11-03 18:26:20

摘要:我们知道,微服务设计过程中往往会面临边界如何划定的问题,微服务到底应该拆多小,不同的人会根据自己对微服务的理解而拆分出不同的微服务,于是大家各执一词,谁也说服不了谁,都觉得自己很有道理。那在实际落地过程中,很多项目在面临这种微服务设计困惑时,是靠拍脑袋硬完成的,上线后运维的压力就可想而知了。那是否有合适的理论或设计方法来指导微服务设计呢?当你看到这一讲的题目时,我想你已经知道答案了。没错,就是DDD。那么今天我就给你详细讲解下:“微服务设计为什么要选择领域驱动设计?” 软件架构模式的演进 在进入今天的主题之前,我们先来了解下背景。我们知道,这些年来随着设备和新技术的发展,软件的架构模式发生了很大的变化。软件架构模式大体来说经历了从单机、集中式到分布式微服务架构三个阶段的演进。随着分布式技术的快速兴起,我们已经进入到了微服务架构时代。 我们先来分析一下软件架构模式演进的三个阶段。 第一阶段是单机架构: 采用面向过程的设计方法,系统包括客户端UI 层和数据库两层,采用 C/S 架构模式,整个系统围绕数据库驱动设计和开发,并且总是从设计数据库和字段开始。 第二阶段是集中式架构: 采用面向对象的设计方法,系统包括业务接入层、业务逻辑层和数据库层,采用经典的三层架构,也有部分应用采用传统的SOA架构。这种架构容易使系统变得臃肿,可扩展性和弹性伸缩性差。 第三阶段是分布式微服务架构: 随着微服务架构理念的提出,集中式架构正向分布式微服务架构演进。微服务架构可以很好地实现应用之间的解耦,解决单体应用扩展性和弹性伸缩能力不足的问题。 我们知道,在单机和集中式架构时代,系统分析、设计和开发往往是独立、分阶段割裂进行的。 比如,在系统建设过程中,我们经常会看到这样的情形:A负责提出需求,B 负责需求分析,C 负责系统设计,D负责代码实现,这样的流程很长,经手的人也很多,很容易导致信息丢失。最后,就很容易导致需求、设计与代码实现的不一致,往往到了软件上线后,我们才发现很多功能并不是自己想要的,或者做出来的功能跟自己提出的需求偏差太大。 而且在单机和集中式架构这两种模式下,软件无法快速响应需求和业务的迅速变化,最终错失发展良机。此时,分布式微服务的出现就有点恰逢其时的意思了。 微服务设计和拆分的困境 那进入微服务架构时代以后,微服务确实也解决了原来采用集中式架构的单体应用的很多问题,…… 阅读全文

追MM与23种设计模式

2011-02-11 17:49:09

摘要:创建型模式 FACTORY—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory1 工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。 BUILDER—MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖) 建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。 FACTORY METHOD—请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。 工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。 PROTOTYPE—跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要) 原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。 SINGLETON—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老…… 阅读全文

模式开发之旅(28):总结

2010-10-07 17:35:02

摘要:设计的初衷是提高代码质量 创业时,我们经常会讲到一个词:初心。这词说的其实就是,你到底是为什么干这件事。不管走多远、产品经过多少迭代、转变多少次方向,“初心”一般都不会随便改。当我们在为产品该不该转型、该不该做某个功能而拿捏不定的时候,想想它符不符合我们创业的初心,有时候就自然有答案了。 实际上,应用设计模式也是如此。应用设计模式只是方法,最终的目的,也就是初心,是提高代码的质量。具体点说就是,提高代码的可读性、可扩展性、可维护性等。所有的设计都是围绕着这个初心来做的。 所以,在做代码设计的时候,你一定要先问下自己,为什么要这样设计,为什么要应用这种设计模式,这样做是否能真正地提高代码质量,能提高代码质量的哪些方面。如果自己很难讲清楚,或者给出的理由都比较牵强,没有压倒性的优势,那基本上就可以断定这是一种过度设计,是为了设计而设计。 实际上,设计原则和思想是心法,设计模式只是招式。掌握心法,以不变应万变,无招胜有招。所以,设计原则和思想比设计模式更加普适、重要。掌握了设计原则和思想,我们能更清楚地了解为什么要用某种设计模式,就能更恰到好处地应用设计模式,甚至我们还可以自己创造出来新的设计模式。 设计的过程是先有问题后有方案 如果我们把写出的代码看作产品,那做产品的时候,我们先要思考痛点在哪里,用户的真正需求在哪里,然后再看要开发哪些功能去满足,而不是先拍脑袋想出一个花哨的功能,再去东搬西凑硬编出一个需求来。 代码设计也是类似的。我们先要去分析代码存在的痛点,比如可读性不好、可扩展性不好等等,然后再针对性地利用设计模式去改善,而不是看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑到底合不合适,最后如果有人问起了,就再找几个不痛不痒、很不具体的伪需求来搪塞,比如提高了代码的扩展性、满足了开闭原则等等。 实际上,很多没有太多开发经验的新手,往往在学完设计模式之后会非常“学生气”,拿原理当真理,不懂得具体问题具体分析,手里拿着锤子看哪都是钉子,不分青红皂白,上来就是套用某个设计模式。写完之后,看着自己写的很复杂的代码,还沾沾自喜,甚至到处炫耀。这完全是无知地炫技,半瓶子不满大抵就是这个样子的。等你慢慢成长之后,回过头来再看自己当年的代码。 学习设计模式最重要的是培养分析问题、解决问题的能力。这样,看到某段代码之后,你就能够自…… 阅读全文

模式开发之旅(27):访问者模式

2010-10-02 16:41:48

摘要:访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 大部分设计模式的原理和实现都很简单,不过也有例外,比如今天要讲的访问者模式。它可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议不要使用访问者模式。 访问者模式诞生的思维过程 假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。 实现这个功能并不难,不同的人有不同的写法,我将其中一种代码实现方式贴在这里。其中,ResourceFile 是一个抽象类,包含一个抽象方法 Extract2txt()。PdfFile、PPTFile、WordFile 都继承 ResourceFile 类,并且重写了 Extract2txt() 方法。在 ToolApplication 中,我们可以利用多态特性,根据对象的实际类型,来决定执行哪个方法。 public abstract class ResourceFile { protected string filePath; public ResourceFile(string filePath) { this.filePath = filePath; } public abstract void Extract2txt(); } public class PPTFile : ResourceFile { public PPTFile(string filePath):base(filePath) { } public override void Extract2txt() { //...省略一大坨从PPT中抽取文本的代码... //...将抽取出来的文本保存在跟filePath同名的.txt文件中... Console.WriteLine(Extract PPT.); } …… 阅读全文

模式开发之旅(26):解释器模式

2010-09-29 11:01:03

摘要:解释器模式:为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。 解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。 要想了解“语言”要表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。 解释器模式的代码实现比较灵活,没有固定的模板。应用设计模式主要是应对代码的复杂性,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。 实战 接下来我们用解释起模式来实现一个自定义的接口告警规则: 在我们平时的项目开发中,监控系统非常重要,它可以时刻监控业务系统的运行情况,及时将异常报告给开发者。比如,如果每分钟接口出错数超过 100,监控系统就通过短信、微信、邮件等方式发送告警给开发者。 一般来讲,监控系统支持开发者自定义告警规则,比如我们可以用下面这样一个表达式,来表示一个告警规则,它表达的意思是:每分钟 API 总出错数超过 100 或者每分钟 API 总调用数超过 10000 就触发告警。 api_error_per_minute 100 || api_count_per_minute 10000 在监控系统中,告警模块只负责根据统计数据和告警规则,判断是否触发告警。至于每分钟 API 接口出错数、每分钟接口调用数等统计数据的计算,是由其他模块来负责的。其他模块将统计数据放到一个字典中(数据的格式如下所示),发送给告警模块。接下来,我们只关注告警模块。 为了简化讲解和代码实现,我们假设自定义的告警规则只包含“||、、、、”这五个运算符,其中,“、、”运算符的优先级高于“||、”运算符,“”运算符优先级高于“||”。在表达式中,任意元素之间需要通过空格来分隔。除此之外,用…… 阅读全文

模式开发之旅(25):备忘录模式

2010-09-25 17:58:56

摘要:备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保持的状态 备忘录模式也叫快照模式,这个模式的定义表达了两部分内容:一部分是存储副本以便后期恢复;另一部分是要在不违背封装原则的前提下,进行对象的备份和恢复。 备忘录模式的应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。 对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。 下面用保存游戏进度的示例来看一下备忘录模式的实现: class GameRole { public int Vitality { get; set; } public int Attack { get; set; } public int Defense { get; set; } public void StateDisplay() { Console.WriteLine(角色当前状态:); Console.WriteLine($生命力:{Vitality}); Console.WriteLine($攻击力:{Attack}); Console.WriteLine($防御力:{Defense}); Console.WriteLine(); } public void GetInitState() { Vitality = 100; Attack = 100; Defense = 100; } public void Fight() { Vitality = 0; Attack = 0; Defense = 0; //与大Boss战斗后死亡 } //…… 阅读全文