DDD思考(数据保存)

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

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方法是要强制类型转换的.

如果把Repository理解为仓库,那么这里有个问题就是谁来处理应该存哪个仓库,一种是根据domain来确定,另一种情况是使用文档数据库(使用DB的json也类似)来将一个大类下面各种又存放到一起.

下面再说第二个方案:

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是哪个子类,但是作为入参并且要求方法对于不同子类采用不同操作时就比较麻烦了.

这个方案相对于Repository.save()而言看上去更加OO但其实并没有,因为其完成了继承却没有完成多态.

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

        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()的问题.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: DDD(领域驱动设计)是一种软件架构风格,它强调在设计软件时要着重于领域模型。领域模型是专门用来表示业务领域的模型,它的目的是帮助理解和描述业务流程。 在DDD项目,通常会有一个专门用来存放领域模型的目录,这个目录通常被称为“领域层”(domain layer)或“领域模型层”(domain model layer)。这个目录通常会包含以下内容: - 实体(entity):表示业务的持久化对象,例如客户、订单等。 - 值对象(value object):表示业务的不可变对象,例如地址、颜色等。 - 抽象基类(abstract base class):为实体和值对象提供公共的属性和方法。 - 仓储接口(repository interface):定义了对实体的持久化操作的方法,例如保存、查询等。 - 服务接口(service interface):定义了与业务流程相关的方法,例如创建订单、计算价格等。 通常情况下,这些内容都会被放在单独的文件或文件夹,以方便维护和管理。 此外 ### 回答2: DDD(领域驱动设计)数据领域模型项目目录结构可以按照以下方式组织: 1. 应用层(Application Layer):这个目录包含了应用层的代码,主要负责接收并处理用户的请求,协调各个领域服务的调用。其可能包括处理用户身份验证、授权、输入参数校验等功能。 2. 领域层(Domain Layer):这个目录包含了领域模型的代码,用来表示业务领域的概念、规则和逻辑。其可能包括实体(Entity)、值对象(Value Object)、领域服务(Domain Service)等。 3. 基础设施层(Infrastructure Layer):这个目录存放与基础设施相关的代码,用来处理与外部系统的交互和数据持久化。可能包括数据库操作、消息队列、缓存、外部API调用等。 4. 接口层(Interface Layer):这个目录用来定义与外部系统交互的接口,可以包括RESTful API、GraphQL接口、消息队列接口等。 5. 共享内核(Shared Kernel):这个目录存放一些通用的领域模型、工具类、扩展方法等,可以被整个项目共用。 6. 测试目录(Test):这个目录存放各个层面的测试代码,包括单元测试、集成测试、端到端测试等。 7. 配置文件(Config):这个目录存放项目的配置文件,可以包括数据库连接配置、日志配置、缓存配置等。 以上仅是一种可能的DDD项目目录结构,具体结构可以根据项目的规模、复杂度和需求来进行调整和扩展。重要的是保持代码的组织结构清晰,遵循领域驱动设计的原则,将业务逻辑和领域概念体现在代码,提高代码的可读性和可维护性。 ### 回答3: DDD(领域驱动设计)数据领域模型项目的目录结构会根据具体项目的需求和技术栈而有所不同,但一般包括以下几个核心部分: 1. 领域模型层(Domain Model):包含了业务领域的核心概念、实体对象和值对象等。在这个目录,可以根据业务域划分包或文件来组织模型,比如可以有用户(User)、订单(Order)、产品(Product)等业务领域模型。 2. 应用服务层(Application Services):负责处理应用层与领域模型之间的交互逻辑。这个目录通常会包含一些应用服务接口(接口定义)和实现类,它们调用领域模型层,提供一些对外的接口供上层应用(如控制器)调用。 3. 基础设施层(Infrastructure):包含了与外部系统的交互、数据存储等功能。这个目录可能包含一些与数据库相关的代码、与外部服务交互的代码等。比如可以有数据库访问层、第三方服务接口层等。 4. 用户界面层(User Interface):负责与用户进行交互,通常包含了控制器、表现层逻辑和前端页面等。这个目录可以包含一些控制器、视图模板、静态资源等。 5. 配置文件和脚本:项目的一些配置文件和部署脚本等。这些文件可以包括数据库连接配置、日志配置、缓存配置等。 以上是一个基本的目录结构,但实际项目可以根据具体需求进行调整和扩展。根据项目的规模和复杂性,可以进一步划分子目录,或者引入模块化的设计来组织代码结构。在DDD项目,目录结构的合理组织可以提高代码的可读性和可维护性,同时也更好地支持领域模型的驱动设计思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值