首页 > 其他分享 >关于DDD和COLA的一些总结和思考

关于DDD和COLA的一些总结和思考

时间:2024-05-10 14:16:35浏览次数:26  
标签:String void 业务 面向对象 amount 思考 DDD public COLA

写在前面: 其实之前一直想汇总一篇关于自己对于面向对象的思考以及实践的文章,但是苦于自己的“墨迹”,一延再延,最近机缘巧合下仔细了解了一下COLA的内容,这个想法再次被勾起,所以这次一鼓作气,准备好好梳理一篇。至于标题,因为是被DDD和COLA唤起的,索性就叫这个吧。

思维:面向对象和面向过程

领域驱动设计本质上是讲的面向对象,但是谈面向对象,始终无法绕开面向过程,所以我们先好好说一下面向过程和面向对象这两个概念。

什么是面向过程呢,其实就是我们学习编程时最初被植入的逻辑,这也是很多人即便学了面向对象后,写的代码却四不像的原因,因为这个思维是根深蒂固的。我想大多数院校科班生,第一次接触编程都是C语言,从一个hello word开始,然后便是if elsefor循环,其实if else 思维便是面向过程的基本逻辑,就是做事的 “步骤”,比如经典的图书管理系统的课设,基于面向过程去设计编码,想的是新增图书、修改图书名字、描述;是根据输入的参数,进行if else的选择,然后进入对应的流程,在流程里去先做什么,再做什么,关注点在图书的操作上, 是专注于事情本身,专注于做事的流程;所以面向过程更适合去做一些底层的,基于硬逻辑的内容。当需要做的东西规模过大且频繁变化时,代码量和改动成本也会增加。

面向对象相对于面向过程,它更偏向于概念的抽象和建设模型。同样图书管理系统,面向对象考虑的重点则变成了图书(而不是操作图书数据这件事),一条条的图书数据,在内存里就是一个个的个体对象,至于图书的各种操作那是细节的内容,它不会去关心,只要定义了图书这个对象,那对于图书的操作那都在对象自身的事情,面向对象专注于事情的主体,是以主体以及它们之间的行为组合去构建程序。当然对象自身内部的行为又是一个个的面向过程组成,这就是在编码的时候最容易让人模糊和把握不准的地方。面向对象把程序设计又拔高了一层,把细节忽略,站在更高维度去构建程序。

通过一个简单的例子来对比一下这两种思想:数据库中存在所有学生的数据,比如姓名、学校、专业,下面需要实现一个自我介绍的功能,描述方式为:我是XX,毕业于XXX学校,用面向过程的思维实现是这样的:

public class test{

   public static void main(String[] args){
       desc("张三");
   }

   public static void desc(String name){
     //查询数据库
     connection = DbManager.createConnection(root,XXX,3306);
     //查询数据
     Map<String,Object> a = connection.query("SELECT name,school FROM tb_student WHERE name = #{name}",name);
     System.out.print("我是"+a.get('name')+",毕业于"+a.get("school");
   }
}

拿到需求,我们关注的自我介绍这件事,只要完成这件事就好了,所以直接定义一个过程(方法、函数)然后过程里去根据需求把这件事完成;

而使用面向对象的话,面对需求,首先需要确定主体,也就是学生对象,然后学生对象有姓名、学校、专业这些属性和一个自我描述的能力。

public class Student{
    private String name;
    private String school;
    private String discipline
    public Student(Map map){//省略构造函数内容}
    public void introduce(){
    System.out.print("我是"+this.name+",毕业于"+this.school);
}
}

然后定义另一个对象,数据库对象,数据库对象有一个可以查询学生对象的能力

public class Db{
   private String url;
   private String username;
   //其他属性…… 
  public Student searchStudent(String name){
    Map a = connection.query("SELECT name,school FROM tb_student WHERE name = #{name}",name);  
    return new Student(a);
  }
}

最后通过使用两个对象,来完成这件事

public class test{

   public static void main(String[] args){
      Student object = Db.searchStudent("张三")
      object.introduce();
   }
}

通过上面的代码,可以发现,面向对象的实现似乎需要更多的代码来完成这件事,没错,这是事实,虽然在设计上我们忽略细节,可是编码上是无法忽略的,甚至使用面向对象成本更高,但是注意我这里说的是针对咱们这个场景需求,当前场景如果戛然而止,确实面向过程方式更精简,但是如果需求继续增加,随着业务增加、需求变大、边界变宽,面向过程可能就需要追加更多的过程代码去完成,而面向对象可能需要的是调整对象的组合方式或者对象本身的扩展去完成,所以,面向对象在代码层面最大的优势就是 复用和扩展

业务开发面向对象理论:领域驱动

说完面向过程和面向对象,再说一下关于面向对象的集成方法论—领域驱动,它的本质是统一语言、边界划分和面向对象分析的方法。简单点来讲就是将OOA、OOD和OOP融汇贯通到系统开发中,充分发挥面向对象的优势和特点,去降低系统开发过程中的熵增。狭义一点解释就是如何用 java 在业务开发中写出“真正面向对象”的系统代码。

在概念上,领域驱动又分为贫血模式和充血模式。

贫血模式

贫血模式很多人不陌生, 也是大多数Java开发使用的MVC架构,实体类仅有get和set方法,在整个系统中,领域对象几乎只作传输介质的作用,不会影响到层次的划分,业务逻辑多集中在Service中,也就是绝大多数使用Spring框架进行开发的Web项目的标准形态,即Controller、Service、Dao、POJO;

这里看一个例子:

/**
 * 账户业务对象
 */
public class AccountBO {

    /**
     * 账户ID
     */
    private String accountId;

    /**
     * 账户余额
     */
    private Long balance;
    /**
     * 是否冻结
     */
    private boolean isFrozen;

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public Long getBalance() {
        return balance;
    }

    public void setBalance(Long balance) {
        this.balance = balance;
    }

    public boolean isFrozen() {
        return isFrozen;
    }

    public void setFrozen(boolean isFrozen) {
        this.isFrozen = isFrozen;
    }

}

/**
 * 转账业务服务实现
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
        AccountBO fromAccount = accountMapper.getAccountById(fromAccountId);
        AccountBO toAccount = accountMapper.getAccountById(toAccountId);

        /** 检查转出账户 **/
        if (fromAccount.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        if (fromAccount.getBalance() < amount) {
            throw new MyBizException(ErrorCodeBiz.INSUFFICIENT_BALANCE);
        }
        fromAccount.setBalance(fromAccount.getBalance() - amount);

        /** 检查转入账户 **/
        if (toAccount.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        toAccount.setBalance(toAccount.getBalance() + amount);

        /** 更新数据库 **/
        accountMapper.updateAccount(fromAccount);
        accountMapper.updateAccount(toAccount);
        return Boolean.TRUE;
    }
}

贫血模型的问题和难点在于,在面对较为庞大体量的业务系统时,业务逻辑层的膨胀导致代码的混乱。因为贫血的特性(POJO仅仅是数据),导致业务代码本身就不“面向对象”化,随着业务的积累和缝缝补补,Service层更像是面向过程,堆满了if else ,会不断膨胀和混乱,边界不易控制,内部的各模块、包之间的依赖会变得不易管理。

充血模式

充血模式更符合领域设计,简单来讲就是OOA和OOP的最佳实践,将业务逻辑和持久化等内容均内聚到实体类中,Service层仅仅充当组合实体对象的画布,负责简单封装部分业务和事务权限管理等;

如果使用充血模式完成上面的例子则是这样:

/**
 * 账户业务对象
 */
public class AccountBO {

    /**
     * 账户ID
     */
    private String accountId;

    /**
     * 账户余额
     */
    private Long balance;

    /**
     * 是否冻结
     */
    private boolean isFrozen;

    /**
     * 出借策略
     */
    private DebitPolicy debitPolicy;

    /**
     * 入账策略
     */
    private CreditPolicy creditPolicy;

    /**
     * 出借方法
     * 
     * @param amount 金额
     */
    public void debit(Long amount) {
        debitPolicy.preDebit(this, amount);
        this.balance -= amount;
        debitPolicy.afterDebit(this, amount);
    }

    /**
     * 转入方法
     * 
     * @param amount 金额
     */
    public void credit(Long amount) {
        creditPolicy.preCredit(this, amount);
        this.balance += amount;
        creditPolicy.afterCredit(this, amount);
    }

    public boolean isFrozen() {
        return isFrozen;
    }

    public void setFrozen(boolean isFrozen) {
        this.isFrozen = isFrozen;
    }

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public Long getBalance() {
        return balance;
    }

    /**
     * BO和DO转换必须加set方法这是一种权衡
     */
    public void setBalance(Long balance) {
        this.balance = balance;
    }

    public DebitPolicy getDebitPolicy() {
        return debitPolicy;
    }

    public void setDebitPolicy(DebitPolicy debitPolicy) {
        this.debitPolicy = debitPolicy;
    }

    public CreditPolicy getCreditPolicy() {
        return creditPolicy;
    }

    public void setCreditPolicy(CreditPolicy creditPolicy) {
        this.creditPolicy = creditPolicy;
    }
}


/**
 * 入账策略实现
 */
@Service
public class CreditPolicyImpl implements CreditPolicy {

    @Override
    public void preCredit(AccountBO account, Long amount) {
        if (account.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }        
    }

    @Override
    public void afterCredit(AccountBO account, Long amount) {
        System.out.println("afterCredit");
    }
}

/**
 * 出借策略实现
 */
@Service
public class DebitPolicyImpl implements DebitPolicy {

    @Override
    public void preDebit(AccountBO account, Long amount) {
        if (account.isFrozen()) {
            throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
        }
        if (account.getBalance() < amount) {
            throw new MyBizException(ErrorCodeBiz.INSUFFICIENT_BALANCE);
        }
    }

    @Override
    public void afterDebit(AccountBO account, Long amount) {
        System.out.println("afterDebit");
    }
}

/**
 * 转账业务服务实现
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Resource
    private AccountMapper accountMapper;
    @Resource
    private CreditPolicy creditPolicy;
    @Resource
    private DebitPolicy debitPolicy;

    @Override
    public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
        AccountBO fromAccount = accountMapper.getAccountById(fromAccountId);
        AccountBO toAccount = accountMapper.getAccountById(toAccountId);
        //此处采用轻量化地方式解决了自身面向对象和Spring的bean管理权矛盾
        fromAccount.setDebitPolicy(debitPolicy);
        toAccount.setCreditPolicy(creditPolicy);

        fromAccount.debit(amount);
        toAccount.credit(amount);
        accountMapper.updateAccount(fromAccount);
        accountMapper.updateAccount(toAccount);
        return Boolean.TRUE;
    }
}

充血模型的问题和难点在于:

  • Spring框架本身的限制

  • 业务复杂程度是否匹配

  • 如何把握domain层的边界以及各层间的关系

先说Spring框架本身的限制问题,使用Spring其实默认了使用贫血模型,这是由Spring本身框架特点决定的,首先Spring家族作为Java系的行业老大哥框架,积累和沉淀非常丰富,各方面已经封装的很全面,所以留出的“自由度”就比较少,它的宗旨就是让开发人员减少重复造轮子,仅仅专注功能的开发就好,所以Spring官方的demo以及一些使用导向都是贫血模式;例如Spring的根基Bean管理机制,把对象的管控牢牢把握在框架中,这种将实体类的管理也交由Spring本身也降低了二次开发时面向对象的属性,导致在Spring中进行bean之间的引用改造会面临大范围的bean嵌套构造器的调用问题。

其次是业务的复杂程度是否适配,绝大多数的项目,说难听点,都是面向数据的概念意淫、CRUD的“建筑行业工地式”项目而已,使用Spring+贫血的经典模式足够满足,而且贫血模式在开发成本上更适合面向数据开发(需求变更的驱动成因、团队的管理方式、研发团队的素质),如果过分追求面向对象反而有些舍近求远,所以能否根据业务场景决定是否使用充血的领域驱动是挺难的(毕竟很多优秀的面向对象研发是很难舍弃面向对象的诱惑)

最后就是最难的,如何划分业务逻辑到domain层,即什么样的逻辑应该放在Domain Object中,什么样的业务逻辑应该放在Service中,这是很含糊的,如果没有面向领域开发流程以 OO思想的充分沉淀积累,很难做到;即使划分好了业务逻辑,由于分散在Service和DomainObject层中,不能更好的分模块开发。熟悉业务逻辑的开发人员需要渗透到Domain中去,而在Domian层又包含了持久化,对于开发者(习惯于贫血模型的团队)来说这十分混乱。

关于COLA框架

COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。由阿里大佬张建飞所提出的一种基于DDD和代码整洁理论所诞生的实践理论框架,详细内容可阅读《程序员的底层思维》和相关git代码去了解

项目地址:GitHub - alibaba/COLA:

标签:String,void,业务,面向对象,amount,思考,DDD,public,COLA
From: https://www.cnblogs.com/TheGCC/p/18184043

相关文章

  • DDD面试题:DDD聚合和表的对应关系是什么 ?(来自蚂蚁面试)
    文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录博客园版为您奉上珍贵的学习资源:免费赠送:《尼恩Java面试宝典》持续更新+史上最全+面试必备2000页+面试必备+大厂必备+涨薪必备免费赠送:《尼恩技术圣经+高并发系列PDF》,帮你实现技术自由,完成职业升级,薪......
  • 蚂蚁面试:DDD外部接口调用,应该放在哪一层?
    文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录博客园版为您奉上珍贵的学习资源:免费赠送:《尼恩Java面试宝典》持续更新+史上最全+面试必备2000页+面试必备+大厂必备+涨薪必备免费赠送:《尼恩技术圣经+高并发系列PDF》,帮你实现技术自由,完成职业升级,薪......
  • PCIE思考:简单路由
    上电:主机设备上电,BIOS通过扫描下游设备的BAR,为其注册响应的空间,当需要对这些空间进行操作的时候,就会转换成TLP包的形式进行访问,当然直接和PCIE设备交互的还是RC;其中BAR的低位(具体情况具体分析)作为寻址其的地址;简单DMA读步骤(PCIE设备发起读):1.下游设备发起请求;2.CPU把数据写到......
  • 软件测试过程中的痛点思考
    前几天无意中看到了TesterHome发起的《2023年度软件质量保障行业调查报告》,文中提到了几点调查结果和分析结论让我很感兴趣。针对这份调查报告,我想就下述三点结论谈谈我的一些理解和思考。 一、测试参与度分析在这一调查报告结论中,提到了需求评审、测试计划和测试评审是整个......
  • [读书]-像计算机一样 思考python
    目录前述第二章:变量、表达式、和语句第三章:函数第五章:条件和递归第六章:有返回数值的函数第七章:迭代第八章:字符串第十章:列表第十一章:词典第十二章:tuple第十四章:文件14.2读和写14.3格式化字符串,两种方式14.4os模块14.5读写异常14.7pickle14.9模块相关14.10其他前述编程语......
  • 河南大学大礼堂火灾事故引发安防监控对智能分析技术应用的思考
    一、方案背景2024年5月2日,在修缮施工期间的河南大学河南留学欧美预备学校旧址大礼堂发生火情。现场航拍画面显示,大礼堂经过火灾,房顶已经基本坍塌,被火烧过的建筑呈焦黑状。公开资料显示,大礼堂属河南留学欧美预备学校旧址,2006年,河南留学欧美预备学校旧址被国务院公布为第六批全国......
  • 拿去面试!一个基于 DDD 的高性能短链系统
    众所周知,商城、RPC、秒杀、论坛、外卖、点评等项目早早就烂大街了,翻开同学的简历一看10个里面有9个是这些,翻遍全网再很难找到一个既有含金量又能看得懂的项目,针对此,我研发了这样一个可以快速上手又具有较多技术点的短链项目:高性能短链系统EZLink!技术栈如下:DDD架构Reac......
  • 程序员天天 CURD,怎么才能成长,职业发展的思考(2)
    接着上一篇:程序员天天CURD,怎么才能成长,职业发展思考上一篇写到了用年限来谈程序员的发展,在4-6年这个时间段需要做的一些事情,接着写这个时间段的。第4、5年时候,你可能会做一些关于基层管理工作。这个时期会遇到一些困难。这个时期,既要编写代码,又要做基层管理工作,你肯定很......
  • 关于矢量瓦片技术支持前端渲染带来的思考
    前言书接上回,此前提到地图瓦片切片技术的发展。矢量切片技术将瓦片的渲染由服务端迁移到客户端,此操作带来的影响力不可谓不大,基于此,完全可以随心所欲的定义地图的表达。那么在实际的应用当中,当渲染从服务端迁移后客户端后,是否会带来一些其他的问题?超20M的瓦片数据此事发生在202......
  • 关于在Interface和Abstract Class间选择的一些思考
    本文系笔者在学习软件构造课程期间所写,不保证通用性和正确性,仅供参考。基于课程要求,本文所涉及语言为Java。目录前言接口:组件思想"CompositionoverInheritance"何时选择继承类结语一、前言与简要介绍在学习软件构造课程之前,自己写代码遇到需要复用类中功能时,基本......