暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【DDD】领域驱动设计之规约模式

将咖啡转化为程序的工具人 2021-09-08
3004

工具人发现一个很有意思的现象,在传统企业中,由于成果导向不同,所以采购师和工具人对产品功能的审美是截然相反的。

采购师以高颜值为价值取向,内在腐朽不重要,无论是加拿大火腿肠还是墨西哥鸡肉卷,只要能评奖,就是好产品。

而工具人则以内在美为审美标准,没有优秀的内部设计,再光鲜的面皮,也难逃照妖镜的聚光。


所以,工具人推荐使用 DDD的缘由在于可以管理业务的复杂度,避免在业务规则愈发复杂的状况下代码以及架构发生腐化,变的难以维护,最终退化成下一个WuYF。


大多数业务系统的复杂度体现在多个层面,比如:繁琐的流程,繁复的校验规则,数据的多样性等。

DDD 对于不一样层面的复杂度提供了不一样的应对模式,今天我们聊了解下规约模式( Specification 模式)是如何解决业务规则的复杂性的。

常见 html

常见的业务规则 

作为一个CRUD工具人,日常工作经常会遇到如下场景:

  • 校验业务对象的某些状态是否合法,例如理财服务中,常常需要校验理财产品是否处于申购或认购期,认购额度是否充足,风险等级是否匹配等。

  • 从业务对象的集合中筛选出符合条件的结果集,例如行情服务中,从所有的股票标的中找出融资融券标的。

  • 检查一个新建立的业务对象是否符合某些业务条件,例如卡券系统中,给用户派发一种优惠券,它对应的客户与产品都应该是系统的合法用户和在售产品。


以往我们常常会在Domain service中编写一些简单方法和校验逻辑,但是由于它们被分散在各处,当业务越来越复杂、不同的场景这些规则需要多种不同的排列组合关系时,传统的模式就会变得难以管理。

就像刚才我们提到的理财业务,其校验规则会比较复杂,例如理财购买,你可能需要验证用户账号是否可用、理财产品的剩余额度是否充足、产品是否处于认购期,与用户的风险等级是否匹配等等……仅仅是通过一连串的if判断,那就真的太不利于维护了,并且if嵌套的多了代码难于理解,不好说明白具体意图,而简单短小的方法若是任其分散在不同的 Domain Service 中,后续的开发过程当中就这些业务知识就很容易被丢失。因此需要将隐式业务规则转换成显示概念,这也是DDD的要求。



规约模式提炼隐式规则

而规约模式经常在DDD中使用,用来将业务规则(通常是隐式业务规则)封装成独立的逻辑单元,从而将隐式业务规则提炼为显示概念,并达到代码复用的目的。

DDD 中认为这些规则都是纯粹的“动词”,所以要单独的创建模型,而这些模型都应该是简单的值对象。

首先,定义规约接口

    public interface ISpecification<T> {
    boolean isSatisfied(T entity);
    }

    以风险等级规约为例:

      public class FinanceRiskSpecification implements ISpecification<FinanceProduct> {


      Customer customer;
      public FinanceRiskSpecification(Customer customer) {
      this.customer = customer;
      }


      @Override
      public boolean isSatisfied(FinanceProduct financeProduct) {
      return financeProduct.getRiskLevel().compareTo(this.customer.getRiskLevel()) > 0;
      }
      }

      同样,我们可以实现认购额度规约等:

        public class FinanceQuotaSpecification implements ISpecification<FinanceProduct{
            double purchaseQuota;
        public FinanceRiskSpecification(double purchaseQuota) {
        this.purchaseQuota = purchaseQuota;
        }


        @Override
        public boolean isSatisfied(FinanceProduct financeProduct) {
        return financeProduct.getRestQuota() > purchaseQuota;
        }
        }





        理财购买是一种组合规约的场景,多个规约需要同时满足,所以我们可以对规约模式进行扩展:

          public class AndSpecification<T> implements ISpecification<T> {


          private ISpecification<T> left;
          private ISpecification<T> right;


          public AndSpecification(ISpecification<T> left, ISpecification<T> right) {
          this.left = left;
          this.right = right;
          }


          @Override
          public boolean isSatisfied(T entity) {
          return this.left.isSatisfied(entity)
          && this.right.isSatisfied(entity);
          }
          }

          组合使用多个规约:

            ISpecification<FinanceProduct> compositSpecification = 
            new AndSpecification<FinanceProduct>(new FinanceRiskSpecification<FinanceProduct>(XXX),
                                                   new FinanceQuotaSpecification<FinanceProduct>(1000.00));


            规约模式过滤查询

            从业务对象的集合中筛选出符合条件的结果集,也是工具人日常工作中的常见场景。传统模式中,当数据量很大的情况下,我们常常将逻辑下沉至DAO,直接在SQL中完成。如查询所有的融资融券股票:

              // 以下代码为工具人 YY
              select * from t_security 
                  where t.margin_trade = '1'   //融资融券标识
                    and security_type = 'EQUIT' //证券类型为股票
                    and list_status = '1'// 标的状态为上市


              其实,这部分的逻辑原本应该是属于领域层的,如今却泄漏到了数据层,形成的后果就是维护的难度大大提高,不少业务系统到后期都是在和大段大段的 SQL 作斗争,而应该编写逻辑的 Service 层,Domain 层都成了摆设,退化成了纯粹的数据对象,又回到了传统模式的老路。而 规约 模式能够提供一种不错的解决思路。我们可以为每个规约增加一个queryWrapper的方法,返回一个类似查询包装对象(这里和使用的ORM框架有点耦合,工具人这里举例的是baomidou)。

                public interface ISpecification<T> {
                    boolean isSatisfied(T entity);
                QueryWrapper<T> queryWrapper(QueryWrapper<T> tQueryWrapper);
                }


                构建查询股票型基金产品的规约:

                  public class FinanceInvestStyleSpecification implements ISpecification<FinanceProduct{


                      String investStyle;
                      QueryWrapper<FinanceProduct> tQueryWrapper;
                      
                  public FinanceRiskSpecification(String investStyle,QueryWrapper<FinanceProduct> tQueryWrapper) {
                  this.investStyle = investStyle;
                  this.tQueryWrapper = tQueryWrapper;
                  }

                  ....

                  @Override
                      public QueryWrapper<FinanceProduct> queryWrapper() {
                          return tQueryWrapper.eq("invest_style", investStyle);
                  }
                  }


                  在理财产品仓库,我们则可以用findBySpecifications,统一抽象该类查询

                    public List<FinanceProduct> queryProductBySpec(ISpecification<FinanceProduct> spec)


                    最后,如果台风天气下,上班的工具人,请一定注意安全。






                    文章转载自将咖啡转化为程序的工具人,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论