业务逻辑层设计的一点思考

1,279 阅读10分钟
  最近看了业界大佬的一篇文章,mp.weixin.qq.com/s/HEyPBwbQZ… 

引起了很大的共鸣,对于代码质量以及设计方面在思想上面有很多共同点,有很多地方很赞同作者的观点,特此引用其中部分内容,同时加上个人的思考(链接文件内容很长,值得花时间阅读)。


工作经历中遇到的比较常见的项目问题:

        项目重复代码过多

       重复代码过多一方面是这些重复逻辑内嵌在方法里面,没有独立抽出来,一方面是rd在实现功能时不知道有已实现代码可用。  

  1.   对于内嵌方法可以加强codeview,rd在开发过程中尽量将其抽离成独立方法。
  2.   对于是否有可复用代码,rd需要先对需求熟悉,然后对功能了解下,可以询问对于原有功能开发的rd是否有可复用代码,对于model层,工具层等方法,可以全量搜索check下,这些一般比较好找。
    项目逻辑纠缠严重,职责复杂,层内依赖严重
      业界关于如何设计业务逻辑层 并没有标准和最佳实践,大多数项目中都是按照业务领域对象来设计;
      例如:领域实体对象有 Account、Order、Delivery、Campaign。于是业务逻辑层就设计出 AccountService、OrderService、DeliveryService、CampaignService
这种做法在项目简单是没什么问题,事实上项目简单时 你随便怎么设计都问题不大。
但是当项目变大和复杂以后,就会出现问题了:
  •   组件臃肿:Service 组件的个数跟领域实体对象个数基本相当,必然造成个别 Service 组件变得非常臃肿——API 非常多,代码行数达到几千行;
  •   职责模糊:业务逻辑往往跨多个领域实体,无论放在哪个 Service 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 Service 中;
代码重复 or 逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑 API 中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个 API。但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖;
复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大。

===============下述内容引用内容==============

当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。

我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类:

  • 输入合法性校验;
  • 业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考;
  • 数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为;
  • 外部接口调用行为;
  • 输出/返回值准备。

当然具体到某一个组件实例,可能不会包括上述全部 5 类业务逻辑,但是也可能每一类业务逻辑存在多个。

单这样看你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。

例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。

显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。

如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害:

  • 可读性变差:两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担;
  • 可维护性差:可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变的更困难,修复问题时也更容易出错;
  • 可扩展性无从谈起:扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。

上面的处理方式就是将 业务逻辑进行分层,与业务关联紧密的逻辑划分为high level,与业务逻辑关联不大的,比如dao操作,redis操作,参数比较等等划分为 low level(具体划分可以自定义)。
同时解决方案就是采用了 template method方法,

下面介绍一下 Template Method 设计模式的运用,简单归纳就是:

  • High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,形成一个业务逻辑的模板;
  • final function保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以abstract protected function形式预留扩展点;
  • 子类(一个匿名内部类)像“做填空题”一样,填充模板实现Low Level逻辑——实现那些protected function扩展点;由于扩展点在父类中是abstract的,因此编译器会提醒子类的程序员该扩展什么。

那么它是如何避免上面两个方案的 4 个局限性的:

  • Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动;
  • 无论是父类还是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即便是父类中的 public function 也不可见,因为只有持有类的实例对象才能访问到其中的 function;
  • 无论是父类还是子类,它们都是作为 XyzService 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的);
  • 组件依赖失控的问题当然也就不存在了。
=================================结束==============


     上面的那种解决方案,其实简单点说就是把功能尽量独立出来,方法不要太大,这样可以保证代码复用,方法比较清晰。

   对于解决下面这个问题个人感觉不是很合适
    职责模糊:业务逻辑往往跨多个领域实体,无论放在哪个 Service 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 Service 中;

  本人给出的解决方案是:

     目前都是按照领域实体设计的,所以大的方向还是不变,只是使得职责更加清晰。
     设计思路可以参考现实中的商业活动:

     比如一个互联网企业(腾讯) 和一个房地产企业(万达)都想进入电商领域,但是这领域和他们原有领域有一定交集但是也有比较大的差集(一个搞社交,但是有互联网基因,一个搞地产,有渠道基因) 如果可以融合就可以很好的合作了,那怎么融合呢,融合后是并入腾讯,还是融入万达,在大家看来其实都不合适,其实把它独立处理是一个比较好的选择,所以成立了飞凡,这个新的领域可以很好的融合两者的领域,同时又保证各自领域的独立性。

   同样在业务逻辑设计方面也可以采用这种这种思路,领域融合后成为新的领域,可以融合优势,又保证原有各自的独立性,同时业务逻辑比较清晰,不至于过于庞杂。
    在现实中这种例子很多,比如飞机制造,就是各个领域的集合体,从而成为一个新的领域,汽车领域,房地产领域,它们都是。单一领域很难满足需求了,也很难集合资源。

   如何具体实现呢

    假设目前有user,product 两个领域了,需要获取user下面有哪些product,这一业务逻辑放在user领域和product领域其实都是可以的,但是就是因为这样的二义性使得逻辑开始混乱,所以为了保证独立性,创建userproduct领域来独立处理这种业务逻辑。

  有了独立的领域后难免会有类命名和代码复用,以及领域之间调用问题,继续看下去:

  对于命名问题:

  目前大部分的设计思路是将业务逻辑放在service层中,比如在user领域有userservice类,product下面有productservice,在userproduct领域也可以有userservice类和productservice类,(不要介意同样类名称)

    这时如果需要查看user下面有哪些产品就可以直接寻找到userproduct领域下面的productservice。

   如果想获取所有用户信息,方法就应该写在user领域下面的userservice里边。

  对于领域之间调用问题:

    通过上面的命名逻辑以及领域设计可以看出来,领域之间是会有交集的,存在复用的可能,比如userProduct领域里边的productservice 是对product领域里边的productservice有所依赖的。

   在实际工程实现中对于小型的项目可以直接依赖,将对应的service引入就可以。一种比较好的处理方式是在领域里边新加一个类专门处理领域之间的依赖,方便后期维护及替换等等。

(注意领域里边不是只有一个类,比如user领域里边不是只有userservice,还有很多只和user相关的service,比如 时间统计user信息的service等等。)
  

(关键点:业务逻辑一定要进行拆分,将方法抽离成小函数,保证代码质量,提高后续的代码复用)

  同理service层可以采用上述设计方式,controll层也可采用上述设计方式。