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

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

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

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

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


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

主事件流:模块化

1)  用户登陆银行的在线支付系统学习

2)  选择用户在该银行注册的网上银行帐户测试

3)  选择须要转帐的目标帐户,输入转帐金额,申请转帐spa

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的概念、用处和区别,敬请各位对本系列博文感兴趣的读者关注并给予指导修正。

相关文章
相关标签/搜索