领域驱动设计——项目分层与落地

什么是Evans DDD

        2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计 )简称Evans DDD。
        领域驱动设计事实上是针对OOAD的一个扩展和延伸,DDD基于面向对象分析与设计技术,对技术架构进行了分层规划,同时对每个类进行了策略和类型的划分。
        领域模型是领域驱动的核心。采用DDD的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象(类)组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。基于领域驱动的设计,保证了系统的可维护性、扩展性和复用性,在处理复杂业务逻辑方面有着先天的优势。

分析设计发展的三个阶段

        第一阶段:围绕数据库的驱动设计,新项目总是从设计数据库及其字段开始。
                过去软件系统分析设计总是从数据库开始,这种围绕数据库分析设计的缺点非常明显:
                1.分析方面:不能迅速有效全面分析需求。
                2.设计方面:导致过程化设计编程,丧失了面向对象设计的优点。
                3.运行方面:导致软件运行时负载集中在数据库端,系统性能难于扩展,闲置了中间件J2EE服务器处理性能。
                对象和关系数据库存在阻抗,本身是矛盾竞争的。


         第二阶段:面向对象的分析设计方法诞生后,有了专门的分析和设计阶段之分,但是分析阶段和设计阶段是断裂的。
                第二阶段比第一阶段进步很多,开始采取面向对象的方法来分析设计需求。
                分析人员的职责:是负责从需求领域中收集基本概念。面向需求。
                设计人员的职责:必须指明一组能北项目中适应编程工具构造的组件,这些组件必须能够在目标环境中有效执行,并能够正确解决应用程序出现的问题 


        第三阶段:融合了分析阶段和设计阶段的领域驱动设计(Evans: DDD)。
                分析模型会有知识不断消化的过程,但在编码时这些知识会被遗弃。
                开发人员被迫为设计进行新的抽象,那么分析人员嵌入模型中知识不能保留和重新发现。
                分析模型很多重要发现更改往往出现在设计实现过程,预先的模型可能深入研究无关主题,忽视重要主题。

什么是领域模型 Domain Model:

        在软件工程中,领域模型是包含属性和行为的业务模型。
        领域模型只有业务(真实世界的概念),没有技术。只表达需求真实世界模型,和软件架构技术无关。在项目中,领域层为整个项目的核心,其他的都作为辅助层。

        举一个机器人的例子:


是

        机器人的领域模型:


 

分层领域:

        将领域概念和其他软件技术相关概念分离,可以避免混淆、在系统庞大中失去对领域的把握。领域驱动设计的分层架构和构成要素,这部分内容在Eric Evans的书中有非常详尽的描述:


 

        表现层:负责显示,解析用户命令
        应用层:指挥领域对象实现功能,必须保持简练,不是核心。
        领域层:核心 业务概念
        基础层:与软件技术相关。

        拿一个航运系统举例子:


 

        项目的分层逻辑:

                表现层:接受用户输入(屏幕绘制 )
                               向用户显示信息 (显示结果)
                领域层:   执行业务逻辑 (查询所有城市 城市和货物关联)
                基础层: 访问数据库(查询数据库 提交数据库更改网络通信) 
                不要将UI 数据库和其他支持代码直接写到业务对象中,虽然简单容易,但是拓展困难,难于自动化测试。

        分层的优点:

                每个层都是内聚的,并且只依赖它的下层,为了实现各层的最大解耦,IOC/DI容器是当前Java业务层的最好选择 。
                将所有与需求领域有关的代码集中在领域层,并与用户UI层 应用层和基础层分离。领域对象就可以重点关注表达领域模型,不关心自己的显示存储和管理应用任务等事情。
                适合分布式部署,提升性能,高可伸缩性。


领域模型的切割:

领域驱动设计除了对系统架构进行了分层描述,还对对象(Object)做了明确的职责和策略划分:

        1、实体(Entities):具备唯一ID,能够被持久化,具备业务逻辑,对应现实世界业务对象。
        2、值对象(Value objects):不具有唯一ID,由对象的属性描述,一般为内存中的临时对象,可以用来传递参数或对实体进行补充描述。
        3、工厂(Factories):主要用来创建实体,目前架构实践中一般采用IOC容器来实现工厂的功能。
        4、仓库(Repositories):用来管理实体的集合,封装持久化框架。
        5、服务(Services):为上层建筑提供可操作的接口,负责对领域对象进行调度和封装,同时可以对外提供各种形式的服务。


领域驱动设计实践:

        基于分布式的DDD项目分层格式:

        如果开发者是用maven做的项目管理,给大家推荐一个十分实用的多模块项目结构,模块的分层和依赖关系如下:

        基于rpc的DDD项目分层格式:


        我们用这个分层格式实现一个token获取与token验证的服务,我们可以将token作为一个领域。
        TokenModel 作为实体对象
        TokenRule作为值对象
        将TokenModel 中的属性与行为合并为TokenDomain作为领域对象

        项目结构:


应用层 Controller代码:

@Controller
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private TokenService tokenService;

    private static final Gson gson = new Gson();

    @RequestMapping("/apply/{bizCode}/{bizId}")
    @ResponseBody
    public String apply(@PathVariable String bizCode, @PathVariable String bizId){
        TokenCodeRule rule = new TokenCodeRule();

        rule.setBizCode(bizCode);
        rule.setBizId(bizId);
        rule.setTimeout(100);
        TokenModel tokenModel = tokenService.applyCode(rule);
        return gson.toJson(tokenModel);
    }

    @RequestMapping("/check/{bizCode}/{bizId}")
    @ResponseBody
    public boolean check(@PathVariable String bizCode, @PathVariable String bizId){
        TokenCodeRule rule = new TokenCodeRule();

        rule.setBizCode(bizCode);
        rule.setBizId(bizId);
        return tokenService.checkCode(rule);
    }

}

领域层 (service, domain, model, repository):

service:

@Service("tokenService")
public class TokenServiceImpl implements TokenService {

    @Autowired
    private TokenDomainRepository tokenDomainRepository;

    @Override
    public TokenModel applyCode(TokenCodeRule rule) {
        TokenDomain domain = tokenDomainRepository.findTokenDomain(rule.getBizCode(), rule.getBizId());

        if(null != domain){
            // 存在库中,但已经无效或者已经过期,可以使用,更改其状态以及相关信息
            domain.getTokenCode().setBizCode(rule.getBizCode());
            domain.getTokenCode().setBizId(rule.getBizId());
            domain.getTokenCode().setGeneratedDt(System.currentTimeMillis());
            domain.getTokenCode().setTimeout(rule.getTimeout());
            domain.update();
        }else {
            // 没有存在,可以使用,并存入库中
            domain = tokenDomainRepository.createTokenDomain();
            domain.getTokenCode().setBizCode(rule.getBizCode());
            domain.getTokenCode().setBizId(rule.getBizId());
            domain.getTokenCode().setGeneratedDt(System.currentTimeMillis());
            domain.getTokenCode().setTimeout(rule.getTimeout());
            domain.store();
        }
        return domain.getTokenCode();
    }

    @Override
    public boolean checkCode(TokenCodeRule rule) {
        TokenDomain domain = tokenDomainRepository.findTokenDomain(rule.getBizCode(), rule.getBizId());
        return domain.isAvaliable();
    }
}

仓储repository:

@Service("tokenDomainRepository")
public class TokenDomainRepository {

    @Autowired
    private TokenCodeDao tokenCodeDao;

    public TokenDomain createTokenDomain() {
        TokenDomain domain = new TokenDomain(this);
        return domain;
    }

    public TokenDomain findTokenDomain(String pk){
        TokenModel tokenModel = this.find(pk);
        if(null == tokenModel){
            return null;
        }
        TokenDomain domain = new TokenDomain(this);
        domain.fillDomain(tokenModel);
        return domain;
    }

    public TokenDomain findTokenDomain(String bizCode, String bizId){
        return findTokenDomain(this.generatePK(bizCode, bizId));
    }

    private String generatePK(String bizCode, String bizId) {
        if (StringUtils.isBlank(bizCode) || StringUtils.isBlank(bizId)) {
            return StringUtils.EMPTY;
        }
        return new TokenModel(bizCode, bizId).getPk();
    }

    public TokenModel find(String pk){
        return tokenCodeDao.find(pk);
    }

    public boolean insert(TokenModel tokenModel){
        return tokenCodeDao.insertOrUpdate(tokenModel);
    }

    public boolean update(TokenModel tokenModel){
        return tokenCodeDao.insertOrUpdate(tokenModel);
    }

    public boolean remove(String pk){
        return tokenCodeDao.remove(pk);
    }
}

领域对象:

public class TokenModel {

    /**
     * 存储主键
     */
    protected String pk;
    /**
     * Token属于哪个业务或者商户Code
     */
    private String bizCode;

    /**
     * Token所属子业务ID[比如:用户ID]
     */
    private String bizId;

    /**
     * Token生成时间
     */
    protected long generatedDt;

    /**
     * Token过期时间(单位:秒)
     */
    protected int timeout;

    public TokenModel() {
    }

    public TokenModel(String bizCode, String bizId) {
        this.bizCode = bizCode;
        this.bizId = bizId;
        this.generatedDt = System.currentTimeMillis();
        this.timeout = 1000;
    }


    public String getPk() {
        if (StringUtils.isNotBlank(this.pk)) {
            return this.pk;
        } else if (StringUtils.isNotBlank(this.bizCode) && StringUtils.isNotBlank(this.bizId)) {
            this.pk = DigestUtils.digest(new StringBuffer(this.bizCode).append("_").append(this.bizId).toString());
            return this.pk;
        }
        return StringUtils.EMPTY;
    }

    public void setPk(String pk) {
        this.pk = pk;
    }

    public long getGeneratedDt() {
        return generatedDt;
    }

    public void setGeneratedDt(long generatedDt) {
        this.generatedDt = generatedDt;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public String getBizCode() {
        return bizCode;
    }

    public void setBizCode(String bizCode) {
        this.bizCode = bizCode;
    }

    public String getBizId() {
        return bizId;
    }

    public void setBizId(String bizId) {
        this.bizId = bizId;
    }
}

public class TokenDomain {

    private TokenDomainRepository tokenDomainRepository;

    protected TokenModel tokenCode = new TokenModel();

    public void fillDomain(TokenModel tokenCode) {
        this.tokenCode = tokenCode;
    }

    public TokenModel getTokenCode() {
        return tokenCode;
    }

    public TokenDomain(TokenDomainRepository tokenDomainRepository) {
        this.tokenDomainRepository = tokenDomainRepository;
    }

    public boolean isAvaliable(){
        return this.tokenCode.getGeneratedDt() + 1000 * this.tokenCode.getTimeout() > System.currentTimeMillis();
    }

    /**
     * 插入一条数据
     */
    public void store() {
        this.tokenDomainRepository.insert(this.tokenCode);
    }

    /**
     * 更新一条数据
     */
    public void update() {
        this.tokenDomainRepository.update(this.tokenCode);
    }

    /**
     * 清除记录
     */
    public void remove() {
        this.tokenDomainRepository.remove(this.tokenCode.getPk());
    }

}

在基于rpc的项目中,我们将领域对象分成了 TokenModel,TokenDomain,因为我们需要将TokenModel作为依赖让client去引用,而TokenDomain中的一些数据库操作等是无法被依赖的,所以需要单提出来。


基础设施层:

@Component("tokenCodeDao")
public class TokenCodeDaoImpl implements TokenCodeDao{

    private static final Map<String, TokenModel> dbMock = new ConcurrentHashMap<String, TokenModel>();

    @Override
    public TokenModel find(String pk) {
        return dbMock.get(pk);
    }

    @Override
    public boolean insertOrUpdate(TokenModel tokenModel) {
        dbMock.put(tokenModel.getPk(), tokenModel);
        return true;
    }

    @Override
    public boolean remove(String pk) {
        dbMock.remove(pk);
        return true;
    }
}

        一个简单的DDD项目大概包括上述东西,因为互联网的应用都是基于存储的,我们将领域对象的生成模块(factory)合并到了仓储模块(repository)中。在处理复杂的业务系统时,DDD相对于传统的mvc架构具有先天的优势。大家在今后的项目开发中,不妨尝试用DDD的方式实现,时刻学习一些新的东西,在问题来临时,才能从容面对。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值