基于命令模式的业务逻辑层设计

问题

   1.业务发展太快,需求不停的在变

   目前作的业务系统业务逻辑非常复杂,网站承载的用户访问量也在逐渐增大,更要命的是需求的不确定性,天天都有新需求,每次的改动量都比较大,而且很多时候几个团队拉同一个分支来开发以满足业务发展的需求

   2.系统架构设计不合理,完全没有什么开闭原则,面向对象设计基本为0,看不到设计的影子

   相信大多数开发业务系统的同学都遇到同样的问题,就是当我们要搭建一个XX系统时,大家第一想到的都是ssh,三个框架各司其职,三层结构也很清晰,web,service,dao,然后就开始埋头往里面写代码了,所谓的架构设计貌似就是把框架搭建起来,然后各个层次都通过接口相互依赖(虽然99%的情况下我们的接口只有一个实现类~),然后大家就开始不停的望各个service里添加新代码,系统不大时,这个貌似是个不错的选择

   3.万恶的2/8原则

   就拿笔者目前所参与的几个大型系统开发维护来看,我们系统中有80%的业务集中在20%的service类里面--其实每个业务系统都类似,都有一些核心领域对象,绝大多数的业务操作都是围绕着这些核心对象展开,所以当写XXServiceImpl的时候,就发现这个类的长度越来越大(笔者项目中有一个超过2000行的类!!),这里面充斥者各个业务逻辑方法(用例)

   4.标准没有统一化,百花齐放

   如业务逻辑验证,到底是放到web层去校验还是在业务逻辑层中,还是两层中都要放,错误信息怎么通知到web层,是抛异常还是返回错误码,dao异常到底要不要捕捉,捕捉到怎么处理,当一个团队越来越大,承载的责任越来越多,人员流动很大时,这个时候必须要有统一的规范来约束

   5.代码合并的噩梦

   和第一点的介绍类似,当业务代码过于集中,多个团队都在修改同一份代码(几乎就是修改一个XXService类!!)时,最后发布的分支代码总是最痛苦的,因为他要检查所有先前分支的冲突,以保重先前的改动不会被冲掉或改错,每当看到团队成员为合并代码的问题头大纠结的时候,就在考虑--是否应该让这个问题成为常态,时候有更好的方式来避免冲突(更小的降低代码合并冲突)

解决方案

   1.问题的根源

     代码过于集中,完全面向过程的代码,面条式的代码,没有花时间在系统设计上,目前整个系统的‘架构设计’如图现有业务系统架构

目前大多的mvc框架(struts,springMVC)都是由一个前端控制器(ServletDispatcher)统一处理请求,根据url映射配置(struts.xml)将请求分发给相应的Action,这个是比较典型的Command模式的应用,在web层上目前逻辑都是分离开的,如一个注册和登录功能都放在两个不同的action中去,但是到了service层情况却有些失控了,我们已经习惯了面向过程编程,习惯的把某个属于某个业务对象(或数据库表)的业务操作聚合在一起,于是系统中常常出现类一个个巨大无比的上帝类,而且系统上层的业务方的需求要求我们不断的添加,修改业务,所有的操作都围绕着这个上帝类转,因为dao层一般是面向表的,单表-单dao这样,而且dao中是不放业务的,sql都放在xml配置文件中,所以dao不会出现和service相同的问题

   2.解决方案(以下的代码以struts2为例)

     2.1把上帝拆了,捐了!

        XXService中的业务太多,给他瘦身是必须滴,把各个业务方法搬迁到独立的业务类中去是一个不错的选择

     2.2命令模式的继续--让我们的service也基于命令实现

        2.1中考虑了决定搬迁,这部分考虑的是搬迁的细节了--即要解决怎么拆,往那里搬,怎么管理搬迁后的对象, 其实所有的用户与系统的交互都可以抽象成用户发送请求->服务器对请求进行响应->系统执行结果返回用户,如图

交互

        简洁就是美,我们可以对整个用户的请求进行抽象,用户的请求为一个个的Command命令,系统会为用户发出的Command分配一个命令处理器CommandHandler,系统处理完成返回结果CommandResult,CommandDispatcher用来把命令发送给指定的处理类,是连接Command和CommandHandler的桥梁,达到命令和处理解耦的目的,在基于Spring的处理程序中,我们一般是直接指定handler在spring中配置的beanID,然后handler内部通过spring的factory获得指定的对象,Command命令接口我们设计为标示接口,具体的实现类为用户实际的输入,先看一下类之间的关系

command类图

从上面不同颜色的类图可以看出,我们的command和handler是结对出现的,当有新的命令(新的业务需求)添加的时候,也会新增相应的业务处理类,从而达到对扩展开放的效果,如果某个业务本身变化,影响也只是限制在一个类的级别上,不会对殃及其他,看一下目前整个系统的架构图新的系统架构图

     再来看一下我们核心类的代码

     Command标示接口--没做,仅仅是个标示和我们常用的Serializable,Clonable接口的作用相同,用来表示同类的东东

 

一个command的实例 

CommandHandler接口--这里用到了泛型,这就要求客户端在调用的时候必须指定具体的命令类型

 

CommandResult类-泛型的使用,这样我们在取的时候就能得到实际数据类型了

 

DefaultCommandDispatcher默认的实现,这里我们实现了ApplicationContextAware接口,在spring中配置后,用的时候就可以取得applicationContext对象了,核心的dispatch方法内部会先根据handlerId去获取具体的handler,然后调用handler的handle方法,handle方法将执行结果返回,执行流程图如下

command dispatcher

一个具体的Handler

 

handler的实际原则

     执行后的结果只返回正确的数据,所有的出错信息都通过异常抛出,为什么这么做?第一点,我们的前段调用程序不需要进行一堆堆繁琐的if(result.isSuccess()){}else{}重复操作,第二,统一异常抛出便于我们对异常进行统一的管理,见下一部分web层对Exception的处理机制

     2.3模板化的Action层

     action层负责调用service层接口,进行具体的业务操作,如果有异常,进行异常处理--就是这么简单,那么我们可以为所有的action按照这个步骤配置一个模板方法,先看一下我们模板类的类图

Template类图

execute()方法是外部框架调用的,我们设置成final这样保证整个模板框架不会被子类恶意的覆盖掉,这个方法里代码很简单,就是一个try{}catch{}语句块,如下

 

这里我们捕捉所有异常,但是对捕捉到的异常怎样来处理,就交看子类注,册过的ExceptionHandler了--如捕捉到业务异常,则把异常信息取出,放到context中,然后返回跳转页面,对于未注册过的异常,则默认提供一个DoNothingExceptionHandler,继续将异常往上层抛出,看一段目前我们action中的代码,只需要继承TemplateAction类,实现doExecute方法,注册异常处理类,然后调用业务处理,o了~

     2.4异常处理框架

     2.4.1框架的设计

     一个业务操作可能会抛出各种异常,而且每个业务场景可能有自己特有的异常,所以需要异常处理框架足够灵活,既可以支持通用的异常处理,也可以灵活的扩展来满足特殊需求,看一下我们的异常处理类设计

exceptionHandler

异常的核心处理接口是ExceptionHandler,有个handle方法,接受的参数是当前的异常对象,这个Handler要放到ExceptionHandlerExecuteContext中执行,这个类持有一个ExceptionHandler的map聚合,key是以异常类的class,value为实际的处理类,这个类还有一个比较有用的方法就是registExceptionHandler,这个就是前面将到的需要有足够的灵活性,让调用程序来添加自己的异常处理类

     2.4.2异常的设计

     系统中的异常一般可以分为两类,一类是系统异常,通常是比较严重的错误,如数据库连接不通或者sql执行失败了,远程服务不可用了等等,另一类是业务异常,如我们程序里有逻辑需要用户必须通过支付宝实名认证才能注册,否则会抛出个自定义的异常来。对于第一类的异常信息,我们程序即使捕捉到了也无法进行恢复性的处理,所以对于这种异常,我们只要把异常堆栈打印输出到Log,或通过aop方式统一捕捉告警就可以了,而对于第二类异常,是需要我们程序来进行捕获处理的,如提示用户出错信息,引导用户处理错误。从另一个纬度上来分析异常,可以分为RuntimeException和Exception,对于前者,是不需要显式的去写try{}catch{}的,但对于后者,如果一个方法抛出了Exception,则在我们调用这个方法的时候必须写try{}catch{}块来进行处理--或者直接把异常往上抛,对于我们系统中异常的处理,建议都设计为RuntimeException,否则程序里到处充斥着恶心的try{}catch{}块,但实际上能进行处理的却很少,这两类的RuntimeException都在service层中抛出,交给web层具体的ExceptionHandler去处理

     2.4.3业务异常错误码

     现在我们系统中所有的错误都统一以异常的形式抛出,有这样的需求,如一个用户注册页面,表单字段可能有用户名,密码,邮箱,我们在业务处理类中需要把很多个错误同时抛出,页面一次就能拿到所有字段的出错信息,然后将错误展示在表单字段的后面,这个时候如果单纯的让exception.getMessage是不能满足我们的需求的,必须在Exception中设计一个map类的错误聚合,key就是错误码,value就是该错误码所对应的错误信息,但是这个错误码如何设计,是字符串还是数字,还是枚举??这里我们新建了一个错误码对象来处理ErrorCode,这个类里定义了一个默认的错误类型key,这个类是用来被扩展的,里面没有任何方法,只有多个static final ErrorCode对象--我们系统中的错误码是固定的,不需要被外部实例化,所以构造函数是protected的,不需要被外部改动所以是static final类的,我们可以为每个领域模型建立一个相关的XXErrorCode子类,来扩展自己的错误key,业务层的方法抛出异常时,将各种errorCode组装起来,web层接受到以后,通过解析errorCode,实现errorCode与页面个字段错误的匹配

   3.总结

    3.1.整体思路

        基于Command模式实现业务层设计,达到命令与处理类解耦,两者可以分开独立变化,某个类的改动不会殃及其他

        基于Template模式的Web层设计,将web请求简单化,模板化,子类只需要关注自身业务逻辑和异常处理就可以

        基于命令的业务请求--所有的业务层方法的参数都只有一个XXCommand    

        异常处理--通过注册器的方式实现异常处理器动态添加满足特性需求的目的

        整个设计简洁化,统一输入参数,统一返回结果,针对接口编程,规范,理解成本底

        业务拆分化,不再有万能的上帝类,所有的业务逻辑都被拆分到了具体的类

    3.2问题

        类爆炸?以前业务逻辑是以方法的形式存放在sevice类中的,现在每个逻辑都被拆分到了具体的类中,类的书目是要比以前的多很多

        被架空的Service接口--因为业务逻辑都被分散都了类中,导致service层接口只做为command指定handler--类似与dao方法中指定要执行的sql的id

 

 

 

 

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值