《领域驱动设计与模式实战》读书笔记
关于领域驱动开发(DDD)在大三的时候就有过了解,当时做的一个项目还尝试实践来着。由之前的面向数据库编写程序到以领域为主导。其实之前也分层,也采用了OO设计,但是说白了就是对数据库表的映射,这样导致的后果就是面向关系的架构设计,对于简单的应用也没发现什么问题,复杂的估计也能满足。但是始终觉得不够面向对象,有表现层,服务层,DAO层。一些类就是所谓的数据载体,拿专业术语叫“失血模型”。在大三的那个项目里尝试者做了些改进,增加Repository,对于对象的持久化进行了封装和隐藏,让然对象交给Repository,你并不需要立即持久化。让业务逻辑层拆分,使得领域对象变得“充血”,行为更丰富,分离出来的服务层提供整合服务,它其实是比较薄的一层,一般是单例或者静态方法调用。
看完这本书,其实感觉没有太多新的收获,将概念和思想又重新回顾下,不过后面关于SOA的东西讲的不错。本书是以.net语言来编写样例代码的。书中一开始强调了TDD和重构对于DDD的重要性,分别花了章节将TDD的实践以及重构的步骤作了介绍,后面讲到领域驱动时实践以及设计,以及持久化在.net中使用NHibernate进行举例。有机会还是看下EricEvans所著的《领域驱动设计》这部经典之作,看有没有新的领悟。
Martin Flower在他的《Pattern of Enterprise Application Architecture》一书中讨论了在应用程序架构中建立主逻辑结构的三种方式 ,分别是事务脚本,表模块和领域模型 。1,事务脚本类似于批处理程序,其中所有功能都是在方法中从头至尾描述的。事务脚本很简单,适合处理简单问题,但用于处理高度复杂问题时就显得无能为力了,否则重复将很难避免。但是,很多非常大的系统仍然是用这种方式构建的。
2,表模块对Recordset进行封装,然后可以通过调用表模块上的方法来获取客户的有关信息,为了获取名字,我们调用一个方法,并将ID作为参数。表模块在内部使用Recordeset来应答请求。这当然具有其自身的优点,特别是当具有很好的Recordset模式实现的时候。但是一个问题是它也可能会在不同表模块之间引入重复。另一个缺点是通常无法使用多态解决方案来解决问题,因为客户根本不会看到对象标识,而只会看到基于值的标识。这有点像使用关系模型来代替面向对象模型。
3,相反,领域模型使用面向对象来描述模型,而且尽可能接近所选中领域的抽象描述。领域模型特别适合处理复杂性,这有两个原因,一是它充分利用了面向对象的强大功能,二是它容易恰当地描述领域的细节。
领域驱动设计将重点集中在领域上,并且对软件产生极大影响。模型是开发人员与用户之间极好的沟通工具,而且这两者之间的沟通越好,所开发出来的软件就会越好。之前的旧模型主要关注基础架构和技术概念。新模型没有将注意力分散到这些地方,而是完全集中在核心领域,领域概念以及当前领域问题上。这是一次大的思想转变。软件工厂的思想是在软件公司中有两条生产线。一条生产线负责创建架构,框架等一些用于应用程序家族的对象。另一条生产线则负责使用第一条生产线生产的产品来创建应用程序,从而摊抵框架在多个项目上的成本。软件工厂的核心是领域特定语言(DSL)。一个通俗的描述是,DSL是使用专门针对当前任务的预言来处理子问题,预言本身可以是图形化或文本化的,也可以是通用预言。领域模型不太适合复杂的应用 ,因为很难进行隔离,往往是很大的一个整体,这样就对于分工协作造成了苦难。我们经常遇到的一个问难是:“应该如何处理复杂性?”答案也很简单:分而治之。使用层次结构就是分而治之的一种方式。DDD在很大程度上就是有关层次结构的。在DDD中有限界上下文的概念,它强调了领域的边界,简化了工作,降低了耦合度,并使得连接更清晰,但是这也是有代价的,在通信和性能方面,连接可能会带来大量的开销。通常应保持它的简单,而且当获益大于代价时,再增加复杂性。
以下关于领域设计的内容摘自老王的博客,写的很好,理解果然比我到位。呵呵。直接就摘录过来了。原文:http://hi.baidu.com/thinkinginlamp/blog/item/807b2834dab21f3b5bb5f51f.html
领域的含义:
简单的说,每个软件程序都会与其用户的活动或兴趣相关,其中使用程序的主要环境称为软件的“领域”。
领域中形形色色的业务逻辑构成了软件丰富多采的行为。举例来说,银行财务系统中,领域逻辑就包括了诸如开户,转帐等等操作。可能你会说,普通程序员很少会接触银行系统,这样的例子不够浅显,那我举一个更常见的例子,大凡程序员应该都接触过文章管理系统,它里面的置顶,加精等操作就是领域逻辑。这样看来,似乎用例对应的动作都是领域逻辑了,但是答案是否定了,比如说,文章管理系统中保存文章往往就不是领域逻辑,因为它只是一个和持久化相关的动作而已,是纯粹的技术实现,但是银行财务系统中的保存现金通常却被划为领域逻辑,因为它就是我们常说的存款,有明确的业务含义。看到这,似乎大家又有些Faint 了,这里给出一个判断是否是领域逻辑的原则:就是这个逻辑动作是否有明确的业务上的含义,或者说是否是业务相关的,而不仅仅是技术相关的。
只有将技术实现手段从领域问题中剥离才能保证领域本身的精炼,保证程序员可以把精力集中到领域问题本身上来,而不会满脑子都是技术实现手段。
领域的组成:
按照Eric的表述,通常将领域中的组成角色分为以下五种:
实体(Entity):拥有唯一标识的对象。
值对象(ValueObject):没有唯一标识的对象。
工厂(Factory):定义创建实体的方法。
仓储(Repository):管理实体的集合并封装其持久化过程。
服务(Service):实现不能指派或封装在一个单一对象上的操作。领域的思考:
下面针对上面介绍的五种领域角色来逐一讨论。
实体 的概念是比较好理解的,这样的例子很多,比如说每一个人都可以看作是一个“与众不同”的实体,我之所以用与众不同这个词是为了强调实体必须是能够唯一标识出来的,即便是在我们看作长得一模一样的双胞胎,他们也是能更根据一些标识来区分开,比如指纹,可能你会抬杠,要是没有手的残疾人怎么办?那样我们还可以使用DNA检测,当然,这些都是笑谈了,实际编程的时候,一般是使用一个自增数来作为标识,比如在MySQL数据库中保存实体的时候可以使用 anto_increment属性的自增字段。需要注意的是如果想判断两个实体是否相等,不能根据实体的属性来判断,必须绝对依赖实体的标识,十年前的你和现在的你虽然在身高,体重,年龄等众多重要的属性中多或多或少的发生了变化,但你还是你,因为你的DNA不会因为这些属性的变化而变化,如果期间你的 DNA发生的变异则属于意外情况,这里不考虑。这些理解起来似乎有些哲学的味道了。
值对象 的含义,老实说相对实体来说比较模糊,很多人喜欢把数据传输对象也称为值对象(数据传输对象和我们这里说的值对象是有差别的)让人们对值对象的理解产生过很多歧义,而且值对象的例子不如实体那么直接。从字面上来理解,值对象没有唯一标识,大多数情况下,值对象是不变的,所以系统不用实时的跟踪他们,用的时候就实例化一个,不用的时候就销毁,但是,什么时候使用值对象?把哪些属性划为值对象?值对象的作用是什么?这些都是值得考虑的问题。通常来说,当我们进行领域建模的时候,优先把唯一标识和经常用来检索对象的信息作为实体的属性,而其他信息根据相关性或者划分到其他实体中,或者划分为值对象,举例来说:一个CMS系统中,对于文章实体而言,文章编号,文章标题等都应该作为文章实体的属性存在,而对于文章有效性期限的开始时间,结束时间两个信息则应该被放在一个独立的值对象中,这其中,只有开始时间或结束时间,或者开始时间和结束时间同时都存在或不存在,会代表不同的逻辑意义,合理使用值对象,既有利于屏蔽一些相关逻辑的复杂性,也可以保持实体对象的简洁。
工厂 相对与前两者会好理解的多,毕竟从名字上就能体现出它的职责,那就是创建对象。既然是创建对象,那我们直接实例化一个不行么?简单的情况是可以的,但是工厂往往会带来巨大的好处,简单的说就是屏蔽了创建对象的复杂性。领域创建对象强调的关联,一组相关的对象应该被看作一个整体,对于其中任何对象的访问也应该从这个整体的“根”开始(通常整体中最重要的实体作为根),所以复杂的关联必然会使创建过程同样复杂起来,那我们可不可以在“根”实体的构造函数中完成对象的组装呢,简单的情况可以,复杂的不合适,比如说组装汽车,通常是在工厂里由组装工人和机器人来操作完成,如果我们在“根”的构造函数里完成组装,无异与在汽车里配备了组装工人和机器人,这当然是不必要的,汽车一旦组装出厂,就不需要组装工人和机器人了,此时再附带他们是一种累赘。
仓储的概念和一些人常说的数据访问对象(DAO)有些类似,但是并不等同,二者一个很大的不同是仓储有“根”的概念,而数据访问对象往往是按照数据库的表来划分的。使用仓储主要是为了查询和持久化领域对象,而领域对象之间往往会有复杂的聚合关系,为了保证不变量,所以才引入根的概念,对领域对象中某个子对象的访问必须通过根来导航。这样说可能不易理解,我举一个简单的例子:轿车,轮胎可以看成是一个领域对象的聚合,轿车是这个聚合的根,如果我们想访问轮胎,必须通过轿车的导航来进行,为什么如此规定,因为轿车和轮胎之间存在一个不变量:一个轿车有四个轮胎,如果允许客户端直接访问轮胎,那么就很难保证此逻辑不被破坏。
服务 这个名词被用过很多次了,但是以前人们说的服务大多是从技术角度而言的,从分层来看属于应用层。一般是诸如注册成功发送一个邮件之类的东西,领域驱动设计中的服务不是这个范畴的概念,它强调的是实体之间的相互关系,而不是纯粹意义上的技术手段。举一个例子来说:CMS系统里,如果一篇文章被加入精华,则文章作者的经验值加一。此逻辑中涉及两个实体:文章实体和作者实体。经验值加一的逻辑不管是建立在文章实体里,还是作者实体里都显得冗余,所以有必要在实体之上在抽象出一个服务层来处理。这里可能有人会问:这样的逻辑我们放到传统意义上的应用层不行么?那样做不能说不行,但是多数情况不好,因为此逻辑属于领域逻辑,而不是应用逻辑,如果放在应用层,领域逻辑就外泄了,领域层也就成为了摆设,但是也有例外,有时候我们可能一时很难分辨一个逻辑是领域逻辑还是应用逻辑,这个时候把此逻辑加入到应用层是没有问题的,如果以后发现其作为领域逻辑更合适的话再重构不迟。
作者还谈了一段SOA,领域模型用于SOA。
分布式的好处:
1,容错性:如果所有程序都运行在一台机器上,那么如果这台机器发生故障,就有麻烦了。对于一些应用程序来说,这个风险无关紧要,但对于其他一些程序,其坏处将超乎我们的想象。
2,安全性:对于将应用程序分布到多态服务器上是否会提高安全性这个不言自明。
3,可伸缩性:一些应用程序的负载太高了,因此无法再一台机器上处理。还有一个金钱的原因是,当问题升级时,希望添加更多机器来解决问题,同时不淘汰原有的机器。
不过,虽然分布式有很多好处,不过martin在《重构》书中提到,分布式第一定律:“尽量不要使用分布式”,呵呵,使用分布式还是要考虑很多的。SOA是一种软件开发方式。SOA处理的是分布式系统或应用程序。分布是一个主要的架构问题。基于要在分布式环境中运行不同设计的软件,应用程序将透明的使用非本地资源,而不必知道这些资源的位置,SOA就是这样的一种架构的尝试。
SOA跟之前的分布式系统开发实践有些不同,当前的分布式系统是设计一些层,然后将这些层部署到物理层中,相比之下,SOA将两个概念同一到“服务”当中,服务同时作为设计单元和部署单元。SOA与当前实践的另一个区别是服务之间的通信编码,虽然在层的通信方式上没有约束,但服务必须根据某一模式定义的消息交换模式进行通信。
面向服务有四条原则:
1,服务是自治的。
2,服务具有显式的边界。
3,服务公开模式和契约,而不公开类或类型
4,服务兼容性事基于策略确定的。
前两条原则是互相关联的,每个服务都独立于其他服务,而且显然一个服务结束的地方就是另一个服务开始的地方。这这方面的一些衍生规则包括:当某个服务崩溃时,不应导致其他服务崩溃。因此,在最基本层面上,不同服务应该在不同的进程中运行,因此,在调用方法时,某个线程不能进入一个不同的服务。在更深的层面上,某个服务不应该锁定另一个服务中的资源,ACID类型的事务应该被锁定在单个服务的范围。
第三条原则的关注点是分离,就是说,将实现所需行为的类从“调用它”的外部客户分离出来。这与javaRMI,COM或.net远程调用是完全不同的,在这些技术中,客户端代码必须引用服务器类型,在这种层面上将HSF用的RMI,还不能称之为SOA的架构,虽然被称为"服务框架"。通过模式,而不是类来暴露服务,当使用模式时,需要在外部消息格式与内部类调用之间执行一个显式的映射。这样就可以更改服务的内部实现,具体说就是在不影响外部客户的情况下对调用哪个类或接口进行更改。第三条原则还有一层更深的含义,即版本控制。版本控制可以使得更新服务后,或者说部署服务的不同版本,不会对所有的客户端造成影响,保证兼容。虽然使用类可以组成层次结构,但是事实证明对类型进行版本控制非常困难,而使用
XML的可扩展性则容易很多。
第四条原则强化了前面两点,由服务决定是否对给定请求作出响应,而且它添加了另一个维度,即元数据是作为请求的一部分传递的。这种数据称为策略。在大多数给定的表示中,策略是用加密或身份验证这样的示例来描述的。这种思想基于关注点分离,定义了逻辑通信的模式与加密策略是分开的。如果有些要在服务间传递的信息,而又不想再模式中表示出来,那么一种方法就是使用策略。
什么是服务?
简单的说一个应用程序就是”服务“,它们不仅能够与用户进行交互,而且还能够与其他”服务“进行交互。不过要注意的是,分布式应用程序不能被称为”分布式服务“,而是作为”协同工作的一组服务“这样一种模型。一个不易理解的经验规则是”每个服务有一个可执行文件“,或者对于web应用程序来说是”每个服务有一个web根目录“。另一方面就是数据库,虽然不一定每个服务都必须有一个数据库,但两个服务是不应该共享一个数据库的。共享数据库实际上为服务打开了后门,实质上是绕过了验证检查和逻辑,这样就违反了自治性原则,并增加了服务之间的耦合。
OO在SOA中的定位:
SOA和OO是在不同的层次上操作的。OO处理的是单个部署单元中的设计和代码,而SOA则处理多个部署单元构成的解决方案。SOA并不意味着用新的或替代的方式来设计或编写给应用程序或服务的内部逻辑。没有理由阻止SOA和OO称为两种协作技术。
抛弃传统的CS模式,改用单向异步消息传递.SOA可以通过对请求进行排队来调整负载。在负载不断增加的情况下,这能够避免服务性能的下降,因为影响性能的资源(例如线程)不会被耗尽。通过保持在任意负载下资源利用率均接近为常数,可以提供最大化的性能,这样使用单向异步消息传递的服务可以实现更好的性能特征。
SOA服务的设计:
在这种设计中,当收到消息时,除了将它放到队列中之外,不进行其他处理(这样最大限度的减少了应用服务器打开的线程数,不管负载或处理时间如何),当然前提是客户发出的请求是异步的,不太关心服务何时得到响应。如果此服务需要向其他服务发送消息,那么接受响应的方式将与其他任何请求相同。此外,处理线程不需要等待这些响应,而是可以继续处理新的消息,先前消息的处理状态可以保存在某个存储中,当响应到达时,在从这里检索出来。注意,当使用进程队列时,则可以用进程来代替线程,以便通过单独的机器上运行这些进程来更好的支持向外扩展的增加。
当然跟传统的RPC方式不同,RPC在尝试连接已经关闭的远程服务器时,将收到异常,在SOA设计中,根本不会收到任何异常或响应,这样就需要以某种方式指导请求的处理是否超时。一种简单的方式是为每个发送的请求分配一个定时器。在这种情况下,处理超时的代码不仅是与消息处理代码分离的,而且在时间上也是分离的。