Johnny's Collections

生活总是有太多的无奈与失望,让我们以在努力学习和工作中获得的成就感和快乐来冲淡它们。

BlogJava 首页 新随笔 联系 聚合 管理
  10 Posts :: 0 Stories :: 80 Comments :: 0 Trackbacks

2010年4月28日 #

    一直对BIO、NIO、AIO不太理解,特别是阻塞与异步的区别。Google了一下,一篇文章中的4张图很形象的表述了4种IO模型的原理和区别,收藏一下。

    首先,贴一张表示四种IO模型的图:



同步阻塞IO:


同步非阻塞IO:



异步阻塞IO:


异步非阻塞IO:
posted @ 2012-05-20 00:12 Johnny.Liang 阅读(918) | 评论 (0)编辑 收藏

6月下旬,由于个人发展的原因,离开了做银行外包的F公司,也离开了S城,结束了2个半月短暂的S城之旅,重新回到了土生土长的G城,也加入了一家互联网公司,工作这么多年,都是做企业级应用,这次是我第一次从事互联网技术工作,我终于落网了。

从事互联网技术工作与企业级应用有很大的不同,考虑的东西更多了。由于缺乏互联网相关的开发经验,很多东西需要去学习,同时,加入新公司后,终于真正的当上了Leader,这次终于可以自己一手一脚的组件自己的团队了,所以,除了技术工作,还有一大堆管理工作要做。几个月来,晚上9点半过后下班,周末带电脑回家继续工作已经成为了习惯。总之,每天都有忙不完的工作,生活的70%时间都在工作上。

正因如此,本来想坚持写的博客暂停了,《领域驱动设计》的系列文章也暂停了很久。在此,向一直关注我的文章的同学说声抱歉,我可能有一段时间要暂停写博了。不过,技术依然是我的最爱,IT依然是我的事业,等我在新公司的工作上轨之后,我就会回来。各位,再会!

posted @ 2010-12-14 22:46 Johnny.Liang 阅读(1130) | 评论 (3)编辑 收藏

领域驱动设计系列文章(3)——有选择性的使用领域驱动设计

 

本系列的第一篇博文抛砖引玉,大谈领域驱动设计的优势,这里笔者还是希望以客观的态度,谈谈领域驱动设计的缺点及其不适合使用的场景,以让读者可以有选择性的使用领域驱动设计。

 

       我们知道,没有最好,只有最合适,设计也是一样。因此,所谓设计,就是以你和你的团队的知识、经验和智慧,全面充分的考虑各种内外因素后,在你们的设计方案中作出合理的选择的过程。而这些影响你们选择的因素主要有:

 

  • 技术框架的特征和约束(如果你的项目决定使用C语言进行开发,那么首先在设计方法上,就需要使用面向过程而非面向对象的设计方法)。

 

  • 时间的压力和约束(你永远不可能告诉你的老板,给我10年时间,我和我的团队将为你设计出世界上最优秀的软件)。

 

  • 你的团队的能力、经验、性格、价值观等因素的约束(你不能期望一个长期从事遗留系统维护项目或大部分成员是缺乏经验的高校毕业生的团队能很好的按照你的设计意图去实现你的高度抽象的优秀设计,同时你也别指望一帮家里经济条件不错,本着过来熬时间的家伙会乐意与你一起刻苦钻研、精益求精)。

 

  • 你的系统的特征(如果你想把一个足够简单而且在可以预计的将来都不存在很大规模的需求变更的系统设计得很复杂,很精妙,具有很好的扩展性,但要为此付出巨大的时间、人力成本,这显然是一种不理智的过度设计(Over design))。

 

  • 其他外在因素的约束(你的项目需要参与投标,你必须压缩人力、时间等以让你的项目成本成为巨大的竞争资本)。

 

当然,上述的考虑因素站在比较高的角度,通常是项目经理、架构师需要考虑的问题,但这当中你应该会得到一些启发。回到我们的主题,我们首先看看,领域驱动设计相对于传统的面向过程式的设计,有什么缺点:

 

  • 复杂化:面向过程思维之所以一直很受欢迎,是因为它很直观,非常符合大部分人的思维习惯,大部分人拿到一个问题,通常都是会很直观的想第一步做什么、第二步做什么,如果怎样,应该怎样,否则怎样……,可以说,任何水平的程序员,都能很好的使用面向过程的方法进行设计和开发。同时,由于我们教育水平的落后和整体IT环境的制约,可以这样说,真正掌握面向对象思维和设计方法的程序员的比例非常低(虽然绝大部分都使用面向对象的语言和工具),而本身面向对象思维要求人有很好的抽象思维能力,因为你需要把一个复杂的系统一层层的抽象为简单的部分,需要把现实世界的事物(有些是可见的,但有些是不可见)合理的抽象为计算机世界的不同元素。这些都不是一些很容易做的事情,要做得好,就更难。


  • 团队的抗拒:如果你的团队(很大可能)大部分人都习惯于用很直观的面向过程的方式进行设计和开发,你需要推动你的团队转换思维来采用面向对象的方式进行领域驱动设计,通常会遭到多数人的抗拒。因为人都是有惰性的,他们习惯安于现状,而改变是痛苦的,他们要付出额外的努力,需要进行学习,但以笔者的经验,相当一部分程序员,特别是有一定工作年限的程序员,他们从事IT工作都只是为了获得一份不错的报酬,因此他们的学习动力非常有限,而且,他们都相当自负,被说服的难度比较大。



  • 管理、开发和维护的成本高:复杂度更高,意味着你需要花更多的时间进行设计,同时需要花出额外的时间进行必要的培训,而且需要有更完善的文档(设计文档,API文档,培训文档等)。领域驱动设计的抽象程度比较高,因此必需有良好的文档,否则,随着项目的不断迭代、升级和维护,它很容易因为后来者的误解而慢慢回归面向过程的设计,甚至会变得“四不象”,领域驱动设计的成本优势是随着时间的推移慢慢体现的(见下图),如果出现这种情况,所有前面付出的努力都会付诸东流。


系统的初始阶段,领域驱动设计需要付出更大的成本,但随着时间的推移,领域驱动设计的成本效益优势会逐步显现

  • 性能比较低:使用纯面向对象的方式进行领域驱动设计的程序,其系统开销通常要比面向过程设计的程序高,从而性能相对较低(关于具体的例子,后续的博文会提及)。

 

那么,假设我们在时间、团队能力及各种资源都允许的情况下,是否就可以麻木的全盘使用领域驱动设计呢?正如本博文的标题一样,答案是否定的,我们需要有选择性的使用。让我们来看看一些指导性原则:

 

  • 使用领域驱动设计,并不代表整个系统的方方面面都必须遵从领域驱动设计的原则,需要根据实际情况,让适合的部分使用领域驱动设计,让不适合的部分使用面向过程的设计。


  • 对于那些业务规则非常简单,通常只有增、删、改、查的简单操作,而且也不大可能发生大规模需求变更的模块,可以让业务实体成为一个“贫血模型”,例如一些基础数据:系统参数、商品类型、国家、地址信息(注:对于这点,本人持保留态度,因为这些业务虽然非常简单,但既然选择了领取驱动设计,即使把这些业务实体设计为“充血模型”,即把极其简单的业务逻辑也封装在业务实体中,也并不比使用“贫血模型”成本高,而它却带来了统一设计风格的好处)。


  • 对于查询操作,特别是复杂的查询操作,出于性能的考虑,可以用结构化查询逻辑一次性完成,并把这些逻辑封装在Repository(即技术上的DAO)中(这方面的具体例子,后面关于“查询通道”和“领域对象仓库”的博文会更具体的阐述)。我们可以看到,对于一些业务视图,以及报表模块,很明显不适合使用面向对象的方式设计,因为这些“视图”和“报表”,本质上就不是业务实体。


  • 同样出于性能的考虑,在业务实体的实现逻辑中,某些操作不适合过度偏执的使用面向对象方式。例如,在“订单”的“新增订单明细”(order.addOrderItem(orderItem))中,如果业务逻辑规定一张订单中包含优惠商品的明细数目不能超过20条,使用面向对象的方式,就需要把订单中的所有订单明细全部加载,然后逐个明细判断其对应的商品是否优惠商品,再统计出优惠商品的数目,这样很明显是低效率和高开销的,这里只需要使用Repository提供的一个统计方法,用一个结构化查询逻辑返回统计结果即可,而这就是非面向对象的方式。


本博文给有志于领域驱动设计的读者泼了一下冷水,提出一些“反模式”(Bitter),是为了让读者冷静一下,在领域驱动设计过程中作出更灵活和更合理的选择。关于这方面的论述,笔者在这里浅尝则止,限于水平、经验和表达能力,不敢胡乱卖弄,建议读者可以参考阅读Martin Fowler的《Patterns of Enterprise Application Architecture》一书的相关观点。

posted @ 2010-06-26 17:47 Johnny.Liang 阅读(4078) | 评论 (2)编辑 收藏

上一篇文章作为一个引子,说明了领域驱动设计的优势,从本篇文章开始,笔者将会结合自己的实际经验,谈及领域驱动设计的应用。本篇文章主要讨论一下我们经常会用到的一些对象:VODTODOPO

由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念:

 

概念:

VOView Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。

DTOData Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。

DODomain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。

POPersistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

 

模型:

       下面以一个时序图建立简单模型来描述上述对象在三层架构应用中的位置


 

      

l         用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO

l         展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。

l         服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。

l         服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。

l         对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。

 

VODTO的区别

       大家可能会有个疑问(在笔者参与的项目中,很多程序员也有相同的疑惑):既然DTO是展示层与服务层之间传递数据的对象,为什么还需要一个VO呢?对!对于绝大部分的应用场景来说,DTOVO的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举,但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在VODTO,因为两者有着本质的区别,DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。

       用一个例子来说明可能会比较容易理解:例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。

       理论归理论,这到底还是分析设计层面的思维,是否在实现层面必须这样做呢?一刀切的做法往往会得不偿失,下面我马上会分析应用中如何做出正确的选择。

 

VODTO的应用

       上面只是用了一个简单的例子来说明VODTO在概念上的区别,本节将会告诉你如何在应用中做出正确的选择。

       在以下才场景中,我们可以考虑把VODTO二合为一(注意:是实现层面):

l         当需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把VODTO区分开来,这时候VO可以退隐,用一个DTO即可,为什么是VO退隐而不是DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如JavaScript)或其他机制(JSTLELCSS

l         即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让VO退隐

 

以下场景需要优先考虑VODTO并存:

l         上述场景的反面场景

l         因为某种技术原因,比如某个框架(如Flex)提供自动把POJO转换为UI中某些Field时,可以考虑在实现层面定义出VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。

l         如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的DTO来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。

 

DTODO的区别

       首先是概念上的区别,DTO是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而DO是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别,例如UserInfoUser(对于DTODO的命名规则,请参见笔者前面的一篇博文),对于一个getUser方法来说,本质上它永远不应该返回用户的密码,因此UserInfo至少比User少一个password的数据。而在领域驱动设计中,正如第一篇系列文章所说,DO不是简单的POJO,它具有领域业务逻辑。

 

DTODO的应用

       从上一节的例子中,细心的读者可能会发现问题:既然getUser方法返回的UserInfo不应该包含password,那么就不应该存在password这个属性定义,但如果同时有一个createUser的方法,传入的UserInfo需要包含用户的password,怎么办?在设计层面,展示层向服务层传递的DTO与服务层返回给展示层的DTO在概念上是不同的,但在实现层面,我们通常很少会这样做(定义两个UserInfo,甚至更多),因为这样做并不见得很明智,我们完全可以设计一个完全兼容的DTO,在服务层接收数据的时候,不该由展示层设置的属性(如订单的总价应该由其单价、数量、折扣等决定),无论展示层是否设置,服务层都一概忽略,而在服务层返回数据时,不该返回的数据(如用户密码),就不设置对应的属性。

       对于DO来说,还有一点需要说明:为什么不在服务层中直接返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:

l         两者在本质上的区别可能导致彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至两者存在多对多的关系。

l         DO具有一些不应该让展示层知道的数据

l         DO具有业务方法,如果直接把DO传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于AOP拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用DO的业务方法也会因为事务的问题,让事务难以控制。

l         对于某些ORM框架(如Hibernate)来说,通常会使用“延迟加载”技术,如果直接把DO暴露给展示层,对于大部分情况,展示层不在事务范围之内(Open session in view在大部分情况下不是一种值得推崇的设计),如果其尝试在Session关闭的情况下获取一个未加载的关联对象,会出现运行时异常(对于Hibernate来说,就是LazyInitiliaztionException)。

l         从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把DO暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。

 

对于DTO来说,也有一点必须进行说明,就是DTO应该是一个“扁平的二维对象”,举个例子来说明:如果User会关联若干个其他实体(例如AddressAccountRegion等),那么getUser()返回的UserInfo,是否就需要把其关联的对象的DTO都一并返回呢?如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果getUser除了要返回User的基本信息外,还需要返回一个AccountIdAccountNameRegionIdRegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”,笔者目前参与的项目是一个分布式系统,该系统不管三七二十一,把一个对象的所有关联对象都转换为相同结构的DTO对象树并返回,导致性能非常的慢。

 

 

DOPO的区别

       DOPO在绝大部分情况下是一一对应的,PO是只含有get/set方法的POJO,但某些场景还是能反映出两者在概念上存在本质的区别:

l         DO在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类DO是不存在对应的PO的。

l         同样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPOPO,但这个PO在业务领域没有任何现实的意义,它完全不能与任何DO对应上。这里要特别声明,并不是所有多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个PO之间的关系会影响具体业务,并且这种关系存在多种类型,那么这种多对多关系也应该表现为一个DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个DO——“权限”。

l         某些情况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。例如客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把CustomerContacts两个DO数据合并到一张数据表中。反过来,如果一本图书Book,有一个属性是封面cover,但该属性是一副图片的二进制数据,而某些查询操作不希望把cover一并加载,从而减轻磁盘IO开销,同时假设ORM框架不支持属性级别的延迟加载,那么就需要考虑把cover独立到一张数据表中去,这样就形成一个DO对应对个PO的情况。

l         PO的某些属性值对于DO没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来说是没有任何业务意义的,它不应该在DO中存在。同理,DO中也可能存在不需要持久化的属性。

 

DOPO的应用

       由于ORM框架的功能非常强大而大行其道,而且JavaEE也推出了JPA规范,现在的业务应用开发,基本上不需要区分DOPOPO完全可以通过JPAHibernate Annotations/hbm隐藏在DO之中。虽然如此,但有些问题我们还必须注意:

l         对于DO中不需要持久化的属性,需要通过ORM显式的声明,如:在JPA中,可以利用@Transient声明。

l         对于PO中为了某种持久化策略而存在的属性,例如version,由于DOPO合并了,必须在DO中声明,但由于这个属性对DO是没有任何业务意义的,需要让该属性对外隐藏起来,最常见的做法是把该属性的get/set方法私有化,甚至不提供get/set方法,但对于Hibernate来说,这需要特别注意,由于Hibernate从数据库读取数据转换为DO时,是利用反射机制先调用DO的空参数构造函数构造DO实例,然后再利用JavaBean的规范反射出set方法来为每个属性设值,如果不显式声明set方法,或把set方法设置为private,都会导致Hibernate无法初始化DO,从而出现运行时异常,可行的做法是把属性的set方法设置为protected

l         对于一个DO对应多个PO,或者一个PO对应多个DO的场景,以及属性级别的延迟加载,Hibernate都提供了很好的支持,请参考Hibnate的相关资料。

 

 

    到目前为止,相信大家都已经比较清晰的了解VODTODOPO的概念、区别和实际应用了。通过上面的详细分析,我们还可以总结出一个原则:分析设计层面和实现层面完全是两个独立的层面,即使实现层面通过某种技术手段可以把两个完全独立的概念合二为一,在分析设计层面,我们仍然(至少在头脑中)需要把概念上独立的东西清晰的区分开来,这个原则对于做好分析设计非常重要(工具越先进,往往会让我们越麻木)。第一篇系列博文抛砖引玉,大唱领域驱动设计的优势,但其实领域驱动设计在现实环境中还是有种种的限制,需要选择性的使用,正如我在《田七的智慧》博文中提到,我们不能永远的理想化的去选择所谓“最好的设计”,在必要的情况下,我们还是要敢于放弃,因为最合适的设计才是最好的设计。本来,系列中的第二篇博文应该是讨论领取驱动设计的限制和如何选择性的使用,但请原谅我的疏忽,下一篇系列博文会把这个主题补上,敬请关注。
posted @ 2010-05-27 00:07 Johnny.Liang 阅读(38177) | 评论 (16)编辑 收藏

    我一直很喜欢攀藤的植物,之前在家里的阳台种了不少田七,偶然发现一个奇怪的问题,一些新长出来的嫩枝,过了几天突然凋谢了,一直不知道为什么,直到最近我找到了答案。

   来到**城后,新”家”有个很大的窗,外面是防盗网,于是从家里带来了两颗田七苗,由于春天和向南的缘故,两颗小苗长得很快,每天都见它们向上攀爬了一大 截。两天前,发现它们爬到防盗网顶了,防盗网顶部是封闭的,它们没有了向上发展的空间,长出一截吊在半空。今晚浇水的时候,我突然发现吊在半空的嫩枝都凋 谢了,我终于明白了原因。田七非常聪明,当发现“前面是绝路”的时候,它会很明智的选择放弃。种过田七的人会知道,田七虽然是攀藤植物,但它是可以长出分 支的,而有些种植常识的人会知道,把生长过长的枝茎进行修剪,会让养分更充足,从而促进主茎中长出分支,形成更好的植株形状。我相信田七“放弃”的原因也 正在于此,相信很快它们就会长出分支来。

    这个小小的发现其实蕴藏着一种显浅的生活智慧,这种智慧对于很多生物都是与生俱来的,但对于人来说,反而很多人都不懂,又或者做不到。

    借用我的前总监经常告诫我的一句话告诫大家:“设计其实是一种选择,没有最好的设计,只有最合适的设计。”
posted @ 2010-05-26 00:32 Johnny.Liang 阅读(2922) | 评论 (5)编辑 收藏

领域驱动设计系列文章(1)——通过现实例子显示领域驱动设计的威力

 

       曾经参与过系统维护或是在现有系统中进行迭代开发的软件工程师们,你们是否有过这样的痛苦经历:当需要修改一个Bug的时候,面对一个类中成百上千行的代码,没有注释,千奇百怪的方法和变量名字,层层嵌套的方法调用,混乱不堪的结构,不要说准确找到Bug所在的位置,就是要清晰知道一段代码究竟是做了什么也非常困难,最终,改对了一个Bug,却多冒出N个新Bug;同样的情况,当你拿到一份新的需求,需要在现有系统中添加功能的时候,面对一行行完全过程式的代码,需要使用一个功能时,不知道是应该自己编写,还是应该寻找是否已经存在的方法,编写一个非常简单的新、删、改功能,却要费尽九牛二虎之力,最终发现,系统存在着太多的重复逻辑,阅读、测试、修改非常困难。在经历了这些痛苦之后,你们是否会不约而同的发出一个感慨:与其进行系统维护和迭代开发,还不如重新设计开发一个新的系统来得痛快?

       面对这一系列让软件嵌入无底泥潭的问题,基于面向对象思想的领域驱动设计方法是一个很好的解决方法。从事过系统设计的富有经验的设计师们,对职责单一原则、信息专家、充血/贫血模型、模型驱动设计这些名词或概念应该不会感到陌生。面向对象的设计大师Martin Fowler不止一次的在他的Blog和著作《企业应用架构模式》中倡导过上述概论在设计中的巨大威力,而另外一位领域模型的出色专家Eric Evans的著作《领域驱动设计》也为我们提供了不少宝贵的经验和方法。

       笔者从事系统设计多年,将会在本系列文章中把本人对领域驱动设计的理解,结合工作过程中积累的实际项目经验进行浅析,希望与大家交流学习。

       在本系列博文的开篇中,我将会拿出一个显示的例子,先用传统的面向过程方式,使用贫血模型进行设计,然后再逐步加入需求变更,让读者发现,随着系统的不断变更,基于贫血模型的设计将会让系统慢慢陷入泥潭,越来越难于维护,然后再用基于面向对象的领域驱动设计重新上述过程,通过对比展示领域驱动设计对于复杂的业务系统的威力。


       假设现在有一个银行支付系统项目,其中的一个重要的业务用例是账户转账业务。系统使用迭代的方式进行开发,在1.0版本中,该用例的功能需求非常简单,事件流描述如下:

主事件流:

1)  用户登录银行的在线支付系统

2)  选择用户在该银行注册的网上银行账户

3)  选择需要转账的目标账户,输入转账金额,申请转账

4)  银行系统检查转出账户的金额是否足够

5)  从转出账户中扣除转出金额(debit),更新转出账户的余额

6)  把转出金额加入到转入账户中(credit),更新转入账户的余额

备选事件流:

4a)如果转出账户中的余额不足,转账失败,返回错误信息

 

面向过程的设计方式(贫血模型)

 

设计方案如下(忽略展示层部分):

1)  设计一个账户交易服务接口AccountingService,设计一个服务方法transfer(),并提供一个具体实现类AccountingServiceImpl,所有账户交易业务的业务逻辑都置于该服务类中。

2)  提供一个AccountInfo和一个Account,前者是一个用于与展示层交换账户数据的账户数据传输对象,后者是一个账户实体(相当于一个EntityBean),这两个对象都是普通的JavaBean,具有相关属性和简单的get/set方法。

 

下面是AccountingServiceImpl.transfer()方法的实现逻辑(伪代码):

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              
if(srcAccount.getBalance().compareTo(amount)<0)

                     
throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));

       }

}

 

public class Account implements DomainObject {

       
private Long id;

       
private Bigdecimal balance;

      

/**

 * getter/setter

 
*/

}

 

       可以看到,由于1.0版本的功能需求非常简单,按面向过程的设计方式,把所有业务代码置于AccountingServiceImpl中完全没有问题。

       这时候,新需求来了,在1.0.1版本中,需要为账户转账业务增加如下功能,在转账时,首先需要判断账户是否可用,然后,账户的余额还要分成两部分:冻结部分和活跃部分,处于冻结部分的金额不能用于任何交易业务,我们来看看变更后的代码:

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              
if(!srcAccount.isActive() || !destAccount.isActive())

                     
throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE);

              BigDecimal availableAmount 
= srcAccount.getBalance().substract(srcAccount.getFrozenAmount());

              
if(availableAmount.compareTo(amount)<0)

                     
throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));

       }

}

 

public class Account implements DomainObject {

       
private Long id;

       
private BigDecimal balance;

       
private BigDecimal frozenAmount;

      

/**

 * getter/setter

 
*/

}

 

       可以看到,情况变得稍微复杂了,这时候,1.0.2的需求又来了,需要在每次交易成功后,创建一个交易明细账,于是,我们又必须在transfer()方面里面增加创建并持久化交易明细账的业务逻辑:

             

     AccountTransactionDetails details= new AccountTransactionDetails(…);
     accountRepository.save(details);

      

       业务需求不断复杂化:账户每笔转账的最大额度需要由其信用指数确定、需要根据银行的手续费策略计算并扣除一定的手续费用……,随着业务的复杂化,transfer()方法的逻辑变得越来越复杂,逐渐形成了上文所述的成百上千行代码。有经验的程序员可能会做出类此“方法抽取”的重构,把转账业务按逻辑划分成若干块:判断余额是否足够、判断账户的信用指数以确定每笔最大转账金额、根据银行的手续费策略计算手续费、记录交易明细账……,从而使代码更加结构化。这是一个好的开始,但还是显然不足。

       假设某一天,系统需求增加一个新的模块,为系统增加一个网上商城,让银行用户可以进行在线购物,而在线购物也存在着很多与账户贷记借记业务相同或相似的业务逻辑:判断余额是否足够、对账户进行借贷操作(credit/debit)以改变余额、收取手续费用、产生交易明细账……

       面对这种情况,有两种解决办法:

1)  AccountingServiceImpl中的相同逻辑拷贝到OnlineShoppingServiceImplementation

2)  OnlineShoppingServiceImpl调用AccountingServiceImpl的相同服务

显然,第二种方法比第一种方法更好,结构更清晰,维护更容易。但问题在于,这样就会形成网上商城服务模块与账户收支服务模块的不必要的依赖关系,系统的耦合度高了,如果系统为了更灵活的伸缩性,让每个大业务模块独立进行部署,还需要因为两者的依赖关系建立分布式调用,这无疑增加了设计、开发和运维的成本。

有经验的设计人员可能会发现第三种解决办法:把相同的业务逻辑抽取成一个新的服务,作为公共服务同时供上述两个业务模块使用。这只是笔者将会马上讨论的方案——使用领域驱动设计。

 

 

 

面向过程的领域驱动设计方式(充血模型)

 

       为了节省篇幅,这里就直接以最复杂的业务需求来进行设计。

领域驱动设计的一个重要的概念是领域模型,首先,我们根据业务领域抽象出以下核心业务对象模型:


 

Account:账户,是整个系统的最核心的业务对象,它包括以下属性:对象标识、账户号、是否有效标识、余额、冻结金额、账户交易明细集合、账户信用等级。

AccountTransactionDetails:账户交易明细,它从属于账户,每个账户有多个交易明细,它包括以下属性:对象标识、所属账户、交易类型、交易发生金额、交易发生时间。

AccountCreditDegree:账户信用等级,它用于限制账户的每笔交易发生金额,包含以下属性:对象标识、对应账户、信用指数。

BankTransactionFeeCalculator:银行交易手续费用计算器,它包含一个常量:每笔交易的手续费上限。

 

我们知道,领域对象除了具有自身的属性和状态之外,它的一个很重要的标志是,它具有属于自己职责范围之内的行为,这些行为封装了其领域内的领域业务逻辑。于是,我们进行进一步的建模,根据业务需求为领域对象设计业务方法:


 

根据职责单一的原则,我们把功能需求中描述的功能合理的分配到不同的领域对象中:

Account

  • credit:向银行账户存入金额,贷记
  • debit:从银行账户划出金额,借记
  • transferTo:把固定金额转入指定账户
  • createTransactionDetails:创建交易明细账
  • updateCreditIndex:更新账户的信用指数

(我们可以看到,后两个业务方法被声明为protected,具体原因见后述)

 

AccountCreditDegree

  • getMaxTransactionAmount:获取所属账户的每笔交易最大金额

 

BankTransactionFeeCalculator

  • calculateTransactionFee:根据交易信息计算该笔交易的手续费

 

经过这样的设计,前例中所有放置在服务对象的业务逻辑被分别划入不同的负责相关职责的领域对象当中,下面的时序图描述了AccountingServiceImpl的转账业务的实现逻辑(为了简化逻辑,我们忽略掉事物、持久化等逻辑):


 

再看看AccountingServiceImpl.transfer()的实现逻辑:

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              srcAccount.transferTo(destAccount,amount);

       }

}

 

我们可以看到,上例那些复杂的业务逻辑:判断余额是否足够、判断账户是否可用、改变账户余额、计算手续费、判断交易额度、产生交易明细账……,都不再存在于AccountingServiceImplementationtransfer方法中,它们被委派给负责这些业务的领域对象的业务方法中去,现在应该猜到为什么Account中有两个方法被声明为protected了吧,因为他们是在debitcredit方法被调用时,由这两个方法调用的,对于AccountingServiceImpl来说,由于产生交易明细(createTransactionDetails)和更新账户信用指数(updateCreditIndex)都不属于其职责范围,它不需要也无权使用这些逻辑。

 

我们可以看到,使用领域驱动设计至少会带来下述优点:

  • 业务逻辑被合理的分散到不同的领域对象中,代码结构更加清晰,可读性,可维护性更高。
  • 对象职责更加单一,内聚度更高。
  • 复杂的业务模型可以通过领域建模(UML是一种主要方式)清晰的表达,开发人员甚至可以在不读源码的情况下就能了解业务和系统结构,这有利于对现存的系统进行维护和迭代开发。

 

再看看如果这时需要加入网上商城的一个新的模块,开发人员需要怎么去做,还记得上面提过的第三种方案吗?就是把账户贷记和借记的相关业务抽取到成一个公共服务,同时供银行在线支付系统和网上商城系统服务,其实这个公共的服务,本质上就是这些具有领域逻辑的领域对象:AccountAccountCreditDegree……,由此我们又可以发现领域驱动设计的一大优点:

  • 系统高度模块化,代码重用度高,不会出现太多的重复逻辑。

 

笔者经验尚浅,而且文笔拙劣,希望通过这样的一个场景的分析比较,能让读者初步认识到基于面向对象的领域驱动设计的威力,并在实际项目中尝试应用。本篇是领取驱动设计系列博文的第一篇,在系列文章的第二篇博文中,笔者将会浅析VODTODOPO的概念、用处和区别,敬请各位对本系列博文感兴趣的读者关注并给予指导修正。

 

      

posted @ 2010-05-15 21:58 Johnny.Liang 阅读(13309) | 评论 (20)编辑 收藏

笔者从事开发多年,有这样一种感觉,查看一些开源项目,如Spring、Apache Common等源码是一件赏心悦目的事情,究其原因,无外两点:1)代码质量非常高;2)命名特别规范(这可能跟老外的英语水平有关)。
要写高质量的代码,不是一件容易的事,需要长年累月的锻炼,是一个量变到质变的过程,但要写好命名,只需要有比较好的英语语法基础和一种自我意识即可轻松达到。本博文将会结合本人的开发经验,总结出若干命名规则,这些命名规则纯属个人的使用习惯,不代表是一种理想的规则,在这里列举出来,供大家交流讨论。

1.切忌使用没有任何意义的英语字母进行命名:
    for(int i=0; i<10; i++) {
        ...
    }
这是在很多教Java基本语法的书上常见的代码片断,作为教学材料,这样写无可厚非,但作为真正的代码编写,程序员必须要养成良好的习惯,不要使用这种没有任何含义的命名方式,这里可以使用“index”。

2.切忌使用拼音,甚至是拼音首字母组合:
    cishu =5; // 循环的次数
    zzje = 1000.00 // 转账金额
笔者在做代码检查的时候,无数次遇到过这样的命名,使人哭笑不得

3.要使用英文,而且要使用准确的英语,无论是拼写还是语法:
  • 名词单数,必须使用单数英文,如Account、Customer。
  • 对于数组,列表等对象集合的命名,必须使用复数,而且最好按照英文的语法基础知识使用准确的复数形式,如 List<Account> accounts、Set<Strategy> strategies。
  • 对于boolean值的属性,很多开发人员习惯使用isXXX,如isClose(是否关闭),但这里有两点建议:1)最好不要带“is”,因为JavaBean的规范,为属性生成get/set方法的时候,会用“get/set/is”,上面的例子,生成get/set方法就会变成“getIsClose/isIsClose/getIsClose”,非常别扭;2)由于boolean值通常反映“是否”,所以准确的用法,应该是是用“形容词”,上面的例子,最终应该被改为 closed,那么get/set方法就是“getClosed/isColsed/setClosed”,非常符合英语阅读习惯。

4.方法名的命名,需要使用“动宾结构短语”或“是动词+表语结构短语”,笔者曾看到过千奇百怪的方法命名,有些使用名词,有些甚至是“名词+动词”,而且,如果宾语是一个对象集合,还是最好使用复数:
    createOrder(Order order) good   
    orderCreate(Order order) bad
    removeOrders(List<Order> orders) good
    removeOrder(List<Order> order) bad

5.对于常见的“增删改查”方法,命名最好要谨慎:
  • 增加:最常见使用create和add,但最好根据英语的语义进行区分,这有助于理解,create代表创建,add代表增加。比如,要创建一个Student,用createStudent要比用addStudent好,为什么?想想如果有个类叫Clazz(班级,避开Java关键字),现在要把一个Student加入到一个Clazz,Clazz很容易就定义了一个 addStudent(Student student)的方法,那么就比较容易混淆。
  • 修改:常见的有alter、update、modify,个人觉得modify最准确。
  • 查询:对于获取单个对象,可以用get或load,但个人建议用get,解释请见第7点的说明,对于不分条件列举,用list,对于有条件查询,用search(最好不要用find,find在英文了强调结果,是“找到”的意思,你提供一个“查询”方法,不保证输入的条件总能“找到”结果)。
  • 删除:常见的有delete和remove,但删除建议用delete,因为remove有“移除”的意思,参考Clazz的例子就可以理解,从班级移除一个学生,会用removeStudent。

6.宁愿方法名冗长,也不要使用让人费解的简写,笔者曾经遇到一个方法,判断“支付账户是否与收款账户相同”,结果我看到一个这样的命名:
    checkIsOrderingAccCollAccSame(...) 很难理解,我马上把它改为:
    isOrderingAccountSameAsCollectionAccount(...),虽然有点长,但非常容易阅读,而且这种情况总是出现得比较少。

7.如果你在设计业务系统,最好不要使用技术化的术语去命名。笔者曾经工作的公司曾经制订这样的命名规则,接口必须要以“I”开头,数据传输对象必须以“DTO”作为后缀,数据访问对象必须以“DAO”作为后缀,领域对象必须以“DO”作为后缀,我之所以不建议这种做法,是希望设计人员从一开始就引导开发人员,要从“业务”出发考虑问题,而不要从“技术”出发。所以,接口不需要非得以“I”开头,只要其实现类以“Impl”结尾即可(注:笔者认为接口是与细节无关的,与技术无关,但实现类是实现相关的,用技术化术语无可口非),而数据传输对象,其实无非就是保存一个对象的信息,因此可以用“**Info”,如CustomerInfo,领域对象本身就是业务的核心,所以还是以其真实名称出现,比如Account、Customer,至于“DAO”,这一个词来源于J2ee的设计模式,笔者在之前的项目使用“***Repository”命名,意味“***的仓库”,如AccountRepository,关于“Repository”这个词的命名,是来源于Eric Evans的《Domain-Driven Design》一书的仓库概念,Eric Evans对Repository的概念定义是:领域对象的概念性集合,个人认为这个命名非常的贴切,它让程序员完全从技术的思维中摆脱出来,站在业务的角度思考问题。说到这里,可能有人会反驳:像Spring、Hibernate这些优秀的框架,不是都在用“I”作为接口开头,用“DAO”来命名数据访问对象吗?没错!但千万别忽略了语义的上下文,Spring、Hibernate框架都是纯技术框架,我这里所说的场景是设计业务系统。

8.成员变量不要重复类的名称,例如,很多人喜欢在Account对象的成员变量中使用accountId,accountNumber等命名,其实没有必要,想想成员变量不会鼓孤立的存在,你引用accountId,必须是account.accountId,用account.id已经足够清晰了。

“勿以善小而不为,勿以恶小而为之”、“细节决定成败”,有太多的名言告诉我们,要注重细节。一个优秀的程序员,必须要有坚实的基础,而对于命名规则这样容易掌握的基础,我们何不现行?
posted @ 2010-04-29 22:54 Johnny.Liang 阅读(6578) | 评论 (17)编辑 收藏

今天一个曾经共事的同行问我:“要从编码转为设计,大概需要多长时间?”
我的回答是:“编码本身就是一种设计,你可以设计你的代码。”

其实正如概要设计与详细设计,系统设计与架构设计一样,编码与设计也是没有明显的边界,每个正确成长的程序员,都必须从编码开始,慢慢锻炼抽象思维、逻辑思维、面向对象思维,然后慢慢的过渡到系统设计,再随着经验和知识的积累,慢慢过渡到架构设计。下面我将会以最近的一个手头的编码任务,简单介绍一下如何“设计”你的代码。

任务是这样的,某银行支付系统的客户端接收银行用户录入的转账数据,当转账数据被审批通过后,状态转变为“transfer”,同时,该客户端需要通过JMS以异步的方式向支付系统后台发送一条带有转账记录(Instruction)的消息,后端在接收到信息之后,需要根据Instruction的一些相关信息,首先确定这笔转账数据是直接发送给真正进行转账的清算(Clearing)银行系统,还是停留在后端系统,等待后端系统中需要执行的工作流程(work flow)。而后端系统需要对Instruction执行的工作流程有两个,同时需要根据Instruction的一些相关信息进行选择。
为了简化复杂度,我这里假设系统有一个InstructionHandleMsgDrivenBean,该bean有一个onMessage()方法,所有业务逻辑需要在该方法中实现。

同时解释一下详细的业务细节:
  • 判断Instruction是否需要停留在后端等待执行指定的工作流程有三个条件:xx、yy、zz,当三个条件都为true时,停留。
  • 判断Instruction需要走A流程还是B流程,由4个因素的组合确定,如果用“Y”代表true,“N”代表false,那么由这个四个因素组成的“XXXX”一共有16种组合,不同的组合分别走A和B流程,如:YYNN、YYNY to A,NNYY、NNNY to B,……不累赘。
好了,对于一个纯编程人员来说,拿到这样的需求,感觉逻辑很简单,可以直接编码了,于是,他开始一行一行的编写代码(伪代码):

public void onMessage(InstructionInfo instructionInfo) {
    if(xx && yy && zz) { // 停留在后端等待执行指定的工作流程
        // 根据每种组合进行条件判断,走哪个流程
        if(a==true && b==true && c==true && d==true {
            ...
        }
        else if(...) {...}
        else if(...) {...}
        ...
        else(...) {...}    
    }
}

这种做法是最为开发人员欢迎的,因为它简单、直接,但这种做法也恰恰反映了开发人员的通病——使用Java编写纯面向过程的代码。

好了,说了一大堆,如何“设计”你的代码呢?答案是:使用面向对象思维:

我们拿到需求之后,可以分析,这个需求大体上分为两部分:
  • 判断是否需要停留在后端等待执行指定的工作流程的部分
  • 选择走哪个工作流程的部分

有了这个前提,我可以设计出两个职责单一的对象了:

public class InstructionHandleDecisionMaker {
    public static boolean isHandledByBackEnd(InstructionInfo info) {
        return (isXX(...) && isYY(...) && isZZ(...));
    }

    private booolean isXX(...) {
        //TODO Implement the logic
        return false;
    }
    private booolean isYY(...) {
        //TODO Implement the logic
        return false;
    }
    private booolean isZZ(...) {
        //TODO Implement the logic
        return false;
    }
}

public class InstructionWorkFlowSelector {
    private static Map mapping = new HashMap();
    static {
        mapping.input("YYNN",WorkFlow.A);
        mapping.input("NNYY",WorkFlow.B);
        ...
    }

    public static WorkFlow getWorkFlow(Instruction info) {
        StringBuilder result = new StringBuilder();
        result.append(isA(...)).append(isB(...));
        result.append(isC(...)).append(isD(...));
        return mapping.get(result.toString());
    }
    private static String isA(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isB(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isC(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isD(...) {
        //TODO Implment the logic
        return "N";
    }
}

可以看到,我先按职责划分了类,再按职责抽取了私有方法,“框架”设计好 ,为了让编译通过,我上面完整的填写了代码的,然后加上TODO标识,然后,我可以编写我的onMessage方法了:

public void onMessage(InstructionInfo instructionInfo) {
    if( InstructionHandleDecisionMaker.isHandledByBackEnd(...) ) {
        WorkFlow wf =InstructionWorkFlowSelector.getWorkFlow(...);
        //TODO Implment the logic
    }
}

到目前为止,我已经用纯面向对象的思维方式“设计”好我的代码了,这时,我思维非常清晰,因而代码结构也非常清晰,职责单一,内聚高,耦合低,最后,我可以根据需求文档的细节(没有描述)慢慢的编写我的实现了。

复杂的事物总是由一些较简单的事物组成,而这些较简单的事物也是由更简单的事物组成,如此类推。因此,在编写代码的时候,先用面向对象的思维把复杂的问题分解,再进一步分解,最后把简单的问题各个击破,这就是一种设计。开发人员只要养成这种习惯,即使你每天都只是做最底层的编码工作,其实你已经在参与设计工作了,随着知识和经验的累积,慢慢的,你从设计代码开始,上升为设计类、方法,进而是设计模块,进而设计子系统,进而设计系统……,最终,一步一步成为一个优秀的架构师。

最后,有一个真理奉献给浮躁的程序员:

优秀的架构师、设计师,必定是优秀的程序员,不要因为你的职位上升了,就放弃编码。

补充说明:本博文纯粹是讨论一种思维习惯,不要把其做法生搬硬套,不管实际情况,直接在编码的时候这样做,不见得是最好的选择。在实际编码中,有如下问题你必须考虑:
  • 你需要考虑业务逻辑的可重用性和复杂程度,是否有必要设计出新的类或抽取新的私有方法来封装逻辑,或者直接在原方法上编码(如果足够简单)。
  • 新的业务逻辑,是否在某些地方已经存在,可以复用,即使不存在,这些逻辑是应该封装到新的类中,还是应该放置到现有的类中,这需要进行清晰的职责划分。
  • 需要在设计和性能上作出权衡。
  • 如果在现成的系统中增加新的功能,而现成系统的编码风格与你想要的相差很远,但你又没有足够的时间成本来进行重构,那么还是应该让你的代码与现成系统保持一致的风格。

posted @ 2010-04-28 00:51 Johnny.Liang 阅读(4918) | 评论 (8)编辑 收藏