领域驱动设计思考(持久层)

如果不考虑数据库的具体实现,只考虑代码中执行的保存操作,大致可以分为以下几种情况

1 有Repository 由repository.save(entity)来完成持久化操作

2 无Repository,由entity.save()来完成保存

3 无repository,无entity.save(),在调用业务方法后自动保存到数据库

按照DDD书上的介绍推荐的是第一种,理由是把持久化的责任归到entity会到导致混乱,有损entity专注于业务逻辑,也导致开发者的关注力收到影响(那就是为了保护你所以故意这样做)

但是采用第一种也会导致一个问题,那就是继承:

        如果有一个父类订单Contract和两个子类CommonContract和SpecialContract都有作废操作并且含义一致.对于Application层有两种方案,A方案是:

public ContractService{
    private ContractRepository repository;
    public Contract cancel(String id){
        Contract contract=repository.retrieve(id);
        contract.cancel();
        repository.save(contract);
        return contract;
    }
}

这样就给ContractRepository出了一个难题,那就是我save到哪儿呢

B方案是提供两个service

public CommonContractService{
    private CommonContractRepository repository;
    public CommonContract cancel(Stringid){
        CommonContract contract=repository.retrieve(id);
        contract.cancel();
        repository.save(contract);
        return contract;
    }
}

public SpecialContractService{
    private SpecialContractRepository repository;
    public SpecialContract cancel(Stringid){
        SpecialContract contract=repository.retrieve(id);
        contract.cancel();
        repository.save(contract);
        return contract;
    }
}

这样做实现方虽然简单了但毫无疑问增加了调用方的难度.一种缓解调用方难度的方案是采用Restful风格的接口,这个我后面会在restful风格api设计中介绍

B方案的另一个缺点是感觉没有复用(这个问题也可以归结于service这种东西本身就不够OO)

也许有人会问第一种方案里repository.retrieve()如何实现呢,这里可以委托子类的repository(但子类太多而导致频繁调用也是问题);但是在save的时候总不能把每个子类的repository都调用一遍吧.就算可以也会遇到方法不兼容问题.原因如下

interface ContractRepository{
    void save(Contract contract);
}
interface CommonContractRepository{
    void save(CommonContract contract);
}
interface SpecialContractRepository{
    void save(SpecialContract contract);
}

从方法入参可以看出如果在ContractRepository里调用CommonContractRepository里save方法是要强制类型转换的.

下面再说第二个方案:

public ContractService{
    private ContractRepository repository;
    public Contract cancel(String id){
        Contract contract=repository.retrieve(id);
        contract.cancel();
        contract.save();
        return contract;
    }
}

abstract class Contract{
     abstract void save();
     private boolean isCanceled;
    //这里是个示例,实际操作与操作前的判断及操作后的消息通知可能更加复杂
     public void cancel(){isCenceled=true;}
}

这样调至的一个明显好处是体现了OO中的继承,继承在调用父类拥有的方法时很好用,不用care是哪个子类,但是作为入参并且要求方法对于不同子类采用不同操作时就比较麻烦了.

方案三可以避免方案一可能遇到的另一个问题,场景如下

        client发起了一个退费申请,然后本地服务受到请求后调用一个远程第三方服务,在受到请求后返给client.这时会遇到一个问题,那就是由于网络原因或者代码bug导致远程服务成功后本地服务没有收到或者没有写库成功,那么当第三方提供对账单时将无法在本地找到相应订单记录.所有一个解决方案就是在调用第三方前先写一次本地数据库,在得到调用结果后在写一次本地数据库.如果出了问题就等到对账单到了找到本地的订单记录并把状态改对就可以了.

        这个场景的解决也有两种方案,一种是DDD推荐的使用Service来处理

class RefundService{
    private RemoteService remoteService;
    public Response refund(String accountId,Request request){
        Account account=repository.retrieve(accountId);
        account.refund(request);
        repository.save(transfer);//第一次
        Response repose=remoteService.refund(request);
        account.update(response);
        repository.save(account);//第二次
        return response; 
    }
}

这个在我看来让Entity和Service责任混为一体,这样导致只要有远程调用就需要service的介入,这对今后一个服务拆分为多个服务是不利的.而采用第三种方案可以这样处理

class RefundService{
    private RemoteService remoteService;
    public Response refund(String accountId,Request request){
        Account account=repository.retrieve(accountId);
        account.refund(request);
        repository.save(transfer);
        return response; 
    }
}
class Account{
    private RemoteService remoteService;
    void refund(Request request){
        //进行规制判断
        if(getRemainAmount()<request.getAmount()){
            throw new InsufficientAmountException();
        }
        //保存请求数据
        this.request=request;
        save();
        //更新执行结果
        Response response=remoteService.refund(request);
        this.response=response;
        save();
    }
    void save(){
        repository.save(this);
    }
}

这样的好处是Entity处理了所有的业务操作,外边的Application层只是负责把远程调用(可能是json或xml格式,可能有加解密等等)转换为一次对entity的本地方法调用.然后entity搞定其他的,甚至和其他微服务之间的交互也可以转化为了一次对另一个Entity的调用.每次entity的写方法结束后就会完成持久化和发送消息(而按照DDD书上介绍这是Application的责任)

上面讲到的第三个方案有一个缺点,就是有时候我们接收到一次前端调用的时候也许不需要持久化.

比如一些带确认动作的操作.就是用户输入了一些(例如使用了一个满减红包和一个折扣红包),那么页面的显示会随之变动(例如订单金额),有时是前端的信息不足(例如该用户是会员可以打折),如果这个计算操作后端是肯定要进行了,总不能拿着前端金额作为订单金额,另外是红包有可能被并发使用.总而言之就是后端不能信任前端的结果需要自己根据原始数据重新计算.这样就导致了两个问题,一个就是前后端计算结果可能不一致,就算一致也加重了前端的开发量.所以一个方案就是用户在输入时前端实时把数据抛给后端,由后端计算后返给前端呈现,这样前端就只负责呈现而不用考虑业务逻辑.

这个时候后端需要提供两个接口,一个是根据前端原始数据计算并返回,另一个是计算并保存.可以看出来第二个方法是第一个方法+持久化.如果如果持久化本身就在entity的方法里执行了岂不是entity还要专门提供一个计算但不持久化的方法.而我认为这是不必要的,这个可以通过service来控制

class ContractService{
    public Contract doRefund(String contractId,Request request){
        Lock lock=lockFactory.create(contractId);
        if(lock.tryLock()){//为防止并发而加锁
            try{
                Contract contract=repository.retrieve(contractId);
                contract.refund(request);
                contract.save();
                return contract;
            }finally{
                lock.unLock();
            }
        }else{
            return null;
        }
    }
    public Contract calculate(String contractId,Request request){
        Contract contract=repository.retrieve(contractId);
        contract.refund(request);
        return contract;
    }
}

可以看出来entity专注于处理业务逻辑,而一些技术上的问题,例如持久化和加锁交给了Service来处理.

也许你也注意到了,这样会导致refund()内部无法save()的问题.

猜你喜欢

转载自blog.csdn.net/u012220365/article/details/81807661