(5) 基于领域分析设计的架构规范 - 充血模型之Service

本系列目录:java

  1. 改变与优点
  2. 领域分析基础
  3. 读写隔离
  4. 充血模型之实体
  5. 充血模型之Service
  6. 关于重构与落地

Entity与Service,相爱相杀

好,接上一篇。sql

既然采用order.cancel()这种模式,那么一个新的问题来了:安全

全部的命令操做都要变成这样子吗?那曾经巨大的OrderService的代码,岂不是只是单纯挪了一个位置,放在Order里面了,除了上面所谓的可读性的优点,那还有什么用?mybatis

并非,只是一部分放在实体类,其他的命令操做,依旧会采用一种Service来作。 因此,咱们必然须要一个能够清晰量化的规范,来肯定这些行为该放在哪里:app

若是一个命令操做,只修改了一个聚合对象内部的相关数据,那么,就归属给这个聚合 好比,订单取消这个行为,须要作的事情有:框架

  1. 订单状态标记为取消
  2. 订单变动记录插入一条,“订单取消”

根据咱们以前的图能够知道,这些修改操做,都在这个订单聚合内,很天然的归属给order分布式

订单聚合

注意,咱们反复强调了这里是“修改操做”,也就是说,若是须要咱们在此操做期间,查询其余聚合的信息,只要不作修改,那就是容许的!就像下面这样:工具

@Entity
public class Order{

  private OrderStatus status;
  private String customerName;
  private User orderCreator;   //假定这里是下单用户,省略了many-to-one的配置
  //...
  
  public void cancel(){
  
      //修改操做1:变动订单自身状态
      status = OrderStatus.CANCELLED;
      
      //查询用户信息,要记录到订单变动日志中,
      //这里若是是Hibernate,会直接触发sql查找,若是换成其余如mybatis,则用对应的repository操做便可,总之,是一个纯查询
      String userName = orderCreator.getUserName();
    
      //修改操做2:增长一条订单状态变动信息,具体实现省略
      createOrderTrack(OrderStatus.CANCELLED,userName);
  }
}

而后,就要从另一个角度来讲了 若是一个命令操做,而且要求是一个完整的事务,修改了多个聚合的数据,那么,须要为这个行为创建一个 Service 而这个Service,不会是一个{领域名称}+Service,而是一个{具体动做}+Service,好比OrderPayService,订单支付,假定有以下动做:优化

  1. 订单状态改成支付中
  2. 商品库存对应扣减
  3. 用户若使用了优惠券,则优惠券标记为使用中 这几个操做,是要在一个完整的事务中的,因此咱们写在一个Service中
@Transactional
public class OrderPayService{                            //-----------(1)

    @Autowired OrderRepository orderRepository;          //-----------(2)
    @Autowired CouponRepository couponRepository;

    public String execute(Long orderId,Long couponId){   

        //暂不考虑前置状态检查

        //订单属性变动
        Order order = orderRepository.getById(orderId);
        order.setStatus(OrderStatus.PAYING);

        //商品库存扣减,按以前的假定,一个订单只对应一个商品
        Prodect product = order.getProduct();
        product.minusStock(order.getQuantity);           //-----------(3)

        //变动优惠券状态
        Coupon usingCoupon = couponRepository.getById(couponId);
        usingCoupon.setStatus(CouponStatus.USING);

        //去交易中心获取支付unikey
        CreatePayResponse payResponse = payCenterApi.createPay(...各类参数...);
        return payResponse.getUnikey();
  }
}

//这时,上层入口(如Controller)就是这样调用了
orderPayService.execute(orderId,couponId);

好,老规矩,深刻探讨一下:.net

  1. 类被命名为订单支付服务,也就是{一个动做}+服务,代码的清晰性上来讲不言而喻,但也意味着一个操做就要有一个service,其实这是很是符合单一指责原则的,可是确定会有很多同窗以为这样作是容易产生过多的类,过分设计了。确实,会有这种状况,但我依旧推崇这样作,或者说,若是必定要一个service里多个行为,那至少表示这个行为是相关的好比都是订单,可是PC端下单APP下单等等,能够放在一块儿,这样职责不泛滥,也便于代码复用
  2. service,咱们能够给与其足够的权限,只要它须要,它能够无所顾忌地获取全部的上下文组件,无论是jdbc组件,仍是外部rpc组件,都是能够的。由于给它的定义,原本就是多聚合的事务处理类,因此,只要它能保证事务的安全性,保证业务的完整,这一切都是没问题的(这里暂时不讨论分布式事务问题,那是另外要一个议题)
  3. 库存扣减,咱们这里采用了product.minusStock(quantity),而不是直接对product进行属性修改。固然,直接进行属性修改也是可行的,可是为什么这里却封装成了一个方法呢?极可能的缘由是,最先的时候,是直接改属性的,但后来有不少地方都要扣减库存,因此,代码重构了,而后minusStock应运而生。关于,重构,咱们后面还会提到。

一个特别注意的点

再思考一个常见的例子:帐户转帐

应该是这样 account.transfer(otherAccount)吗?

这里,会有点争议,但我会提倡用AccountTransferService来作,由于虽然这个操做从宏观上来讲是属于一个领域聚合,可是,这个操做,倒是彻底不一样的对象! 是同时修改了两个对象的数据,并且天然要求完整的事务性。

因此,这种场景,也能够理解为,Service,是处理跨聚合对象


关于实体的Set方法

若是咱们使用不少ORM框架,因为框架的实现策略的缘故,实体类是须要把全部的Get和Set方法都要开放的,并且上面你们也看到当咱们用OrderPayService的时候,也直接使用的对象的setXXX方法,因此Set天然更加须要开放了。

但对于set,本文这套规范,极力倡导一个原则:在进行业务开发时,Set能调用的地方只有1个,那是就在service中! 其他的任何场景,任何地方,都不容许(或者不必)调用set方法,尤为是下面这种场景:

//在一个上层,好比Controller中
@GetMapping("/coupon/disable/{id}")      //失效某张优惠券,偷懒就不用Post了
public ActionResponse disableCoupon(@PathVariable("id") Long id){
      Coupon coupon = couponRepository.getById(id);
    
      //错误,禁止!!!
      coupon.setStatus(CouponStatus.DISABLED);
    
      //正确的应该是
      coupon.disable();--------------------------
}                                               |
                                                |
public class Coupon{                            |
                                                |
  private CouponStatus status;                  |
                                                |
  public void disable(){  <-------------------- |
      status = CouponStatus.DISABLED;
  }
}

必定会有同窗立刻提出疑问

才一行代码,为何不能直接用set?强迫症吗?

不否定,这个规范,的确有点强迫症,可是真的是有好处的。

领域设计的思想里,严格意义上来讲,Get和Set都是不能随便暴露的,尤为是Set,是在修改这个系统,是有必定风险与危害的,那么,任何一个set,都必定是有缘由的,必定是要归属到一个具体的业务命令操做中的。

其实,我在思考这套规范期间,一度将set方法直接设置成本包可见的级别,但愿经过Java编译报错来杜绝这种状况,可是这样又和上面提到的service的模式出现了冲突,最终只能做罢。

工厂

到此为止,咱们能够承认,全部的命令操做,都将会归类到Entity或者Service中

但有一个特例,这里有必要提出来单独说一下:一个实体的建立,也就是增删改查中的 增 的操做

由于删,改的操做,都是先找到一个实体,而后进行操做。但建立却不一样,由于在执行建立操做以前,这个实体都是不存在的,你怎么找?就更加不可能有相似 order.create(params)这种代码出现了,order尚且不存于世呢! 因此,这里,天然的想到经过建立OrderCreateService来处理,但考虑到新增的特殊性,建议直接用工厂模式来作,即OrderFactory

对于OrderFactory,它的责任并不是简简单单new Order()而后一堆setXX后完事。详细说明以下:

  1. 负责建立聚合根对象,好比订单聚合中的Order,每每建立会有诸多不一样的场景,好比建立一个空对象,或者建立有不少默认组件的对象等等,这个就根据业务场景来了。总之返回值必定是一个新建立的实体类。
  2. 负责建立聚合中其余对象,可是这种场景仔细想来,并不会太多。由于聚合中其余“附属实体”的建立每每会以聚合根实体的某一个命令操做相关。好比订单变动记录OrderTrack的建立每每是伴随Order的各类各样的操做,好比订单建立,支付,发货,取消等等,而大部分时候不须要单独出现诸如OrderFactory.createOrderTrack的状况。
  3. Factory中原则上是容许触发对其余领域聚合的数据变动的,由于它是一个特殊的领域Service。但从通常的业务场景来讲,这种状况并很少见,由于建立后当即变更某个其余领域的数据,每每会直接在应用层加入代码,或者经过事件来处理。

事件

领域事件,在最先的《领域驱动设计》一书中,并未说起。在以后的相关书籍,诸如《实现领域驱动设计》中,有将其做为一等公民的身份进行详细讲解。很惭愧,这一块我一直没有GET到其精髓,因此我只是结合更广义上的事件,来作了一些分析,若有不合适的地方,也欢迎各位拍砖。

说到事件,你们必定能联想到事件的广播,事件的处理。没错,事件是一个很是好的解耦和工具,也是一个很是舒服的“梳理工具”,由于在讨论需求的时候,常常可以看到很是“瓜熟蒂落”的事件场景描述:

当用户建立了一个订单后,要同时生成一个订单变动记录 ------ (1)
当用户的订单支付成功后,要同时为这个用户生成一个XX奖励券,而且用户活跃积分+10   ------ (2)

这一些,都太符合“事件”了,这些有些是在项目初版的时候就清楚了,有些则随着版本迭代不断加入。 可是回过头的仔细想一想,若是遇到这种需求场景的时候,你们在实际的开发中,都真的用了事件吗?不管是单机应用仍是分布式应用,咱们在不加入事件机制的前提下,上面这些功能经过直接“调接口”都是彻底能知足的。

没错,因此对于事件模式,咱们能够倡导一个原则:全部的事件,从重构中得来

从重构中得来,意味着,咱们不必在需求一开始就大量采用事件的作法,即便需求描述中有“当/若是...就...”,由于绝大部分时候,咱们每每没法得知以后的产品的发展方向是什么,过早的事件设计(尤为是分布式系统)会给代码的阅读流畅性,事务管理等等带来更大难度。

事件的优点在于一处广播,多处接收,因此,当“接收方”愈来愈多,事件机制的优点也越能体现出来。因此,我认为最佳的实践方式,或者更容易推广的实践方式,仍是跟着版本迭代来不断优化代码,在逐渐清晰地产品发展方向和扩展方向上,将原有的“直接调用”转变成“事件处理”。通常来讲,当“接收方”出现2-3个的时候,能够开始考虑转变成事件机制了,好比上面的(2)

固然,这里并不否定在第一时间就加入事件机制的作法,只是建议若是肯定要在一开始就这样作,但愿这种作法的开发负责人务必对业务的扩展方向有足够清楚的认识与了解。(好比一家公司在原有系统上开发新的升级版系统,这个时候,能够在第一时间就作好设计优化,由于有很好的业务背景基础)

至于具体的代码实现方式,在单机应用中,Spring有很好的事件机制,并且可以支持事务的完整性。而分布式系统中,更多的接用消息中间件来实现,具体技术细节就不更多展开了。

下一篇 关于重构与落地

相关文章
相关标签/搜索