浅析事务脚本和领域模型

本文探讨了面向过程的事务脚本和面向对象的领域模型在业务开发中的应用。事务脚本简单易懂但难以应对复杂逻辑,而领域模型通过抽象核心概念提高了代码可读性和扩展性。通过分期账单入账的业务场景举例,展示了两种模式的优缺点。在实际项目中,根据业务复杂度选择合适的架构模式至关重要。
摘要由CSDN通过智能技术生成

前言

最近为了准备团队内分享,重新翻了下Martin Fowler的《企业应用架构模式》。对其中的事务脚本和领域模型比较感兴趣,选取了这一部分在团队内进行了分享。在准备过程中,重新看这一章节后,产生了一些共鸣。想到自己在实际业务开发过程中,见到的大部分都是事务脚本类型的代码,也被叫做面条代码或者胶水代码。这种代码可读性、可维护性差,经常出现祖传代码现象。遗留代码没人看得懂,也没人敢改。导致业务系统不断腐败下去,维护代码的人心力憔悴。同时也影响了业务迭代的速度,没办法快速响应市场需求。作为技术人员,肯定没人愿意自己的技术水平停滞不前。没人愿意自己的编码水平停留在面条代码这个层次。本文尝试着基于原著的demo,分享下自己的理解。

术语介绍

模式:Pattern。提到模式,我们第一时间肯定会想到设计模式。我们都知道许多流行开源软件大量使用设计模式来组织代码,以保证软件具有较高的可读性、可扩展性。什么是模式呢?我理解模式是前人总结的经验。有句话叫,太阳下没有新鲜事。意思是你遇到的事别人肯定都遇到过,历史就是不断的重复。当前人遇到一些软件设计上的问题时,他们把最佳的实际方案记录下来,当遇到这种情况,使用这种解决方案就可以更好的解决当前问题。所以,模式是前人对问题最佳解决方案的总结。

事务脚本:我理解的事务脚本类似于面向过程编程。将一个大问题拆解为若干小步骤,然后依次实现这些步骤。编程面对的主体往往是一个个步骤。这种编程方式的特点往往是一个类承担较多的职责,主要逻辑分布在一个或少量几个类中。例如、常常命名为xxxService、xxxManager等。

领域模型:我理解和Eric Evans提倡的DDD,也就是领域驱动设计,是类似的概念。可以类比为面向对象编程。编程的主体是领域概念,这些概念最终落地为职责明确的一个个类。业务领域是非常广泛的,每个领域都是一个垂直的行业。例如、金融领域、智能客服领域、电商交易领域等。每个领域都有自己独特的领域知识,这些领域知识就是业务逻辑。基于领域知识,可以抽象出核心领域模型,也就是核心的概念和概念之间的关联关系。

分期账单入账

事务脚本的概念是比较抽象的。下面让我们从一个demo来看一下,具体什么样的编程风格是事务脚本。这个demo是一个分期账单的业务逻辑。消费者购买了一个商品之后,有多种支付方式。可以一次性全款付清,也可以分为两期、三期或者更多期。当分期账单到期时,这笔钱才会算作入账。这个示例中有三个核心的业务概念,商品、合同、分期账单。

商品有商品名称、商品价格属性。

product{
	productId
	price
}

当消费者购买一个商品后,就会生成一个销售合同。合同拥有总货款、下单时间、下单商品等属性。

contract{
	contractNum
	amount
	sign_date
	productId
}

每签署一个合同,就会对应的生成相应的分期账单。分期账单记录了当前关联的合同、当前分期账单的金额、当前账单的到期时间。

revenue_recognition{
	contractNum
	Amount
	date
}

每个商品有不同的售卖策略。例如,A商品售出后,需要在签订合同时付清货款。B商品售出后,签合同时需要付一半货款,另一半货款1个月之后再付。

结合上面的业务场景,我们举个例子。巨硬公司销售word、excel、powerPoint三种商品。

product

productId

price

word

10

excel

20

powerPoint

30

客户张三每个商品都买了一份,所以生成了3分合同。

contract

contractNum

sign_date

productId

100

2022-01-01

word

200

2022-01-01

excel

300

2022-01-01

powerPoint

因为3种商品的销售策略不同。word需要全款购买、excel需要2期付清、powerPoint需要3期付清。所以,共生成了6条分期账单。

revenue_recognition

contractNum

amount

date

100

10

2022-01-01

200

10

2022-01-01

200

10

2022-02-01

300

10

2022-01-01

300

10

2022-02-01

300

10

2022-03-01

问题

介绍完业务背景,下面就到了解决问题的时候了。我们的问题是给定一个时间和合同号,计算出当前时间这个合同已经入账的金额是多少?这个问题可以用以下伪代码来表示。

Money findRecognitionFor(contractId, givenDate)

事务脚本

实现

事务脚本是最容易想到的一种实现方式,也是实现门槛最低的方式。首先,我们要设计两个方法。第一个是用于生产分期账单的方法

calculateRevenueRecognitions(long contractNum)

签订合同后,contract表中生成一条合同数据。基于这条合同数据,需要生成对应的分期账单数据。第二个是用于查询给定合同和时间已入账金额的方法。

Money findRecognitionFor(contractId, givenDate)

具体实现如下所示

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class RecognitionService {

    @Autowired
    private DaoImpl dao;

    public void calculateRevenueRecognitions(long contractNum) throws SQLException {
        ResultSet contract = dao.findContract(contractNum);
        contract.next();
        int price = contract.getInt("price");
        java.sql.Date sign_date = contract.getDate("sign_date");
        String product = contract.getString("productId");
        if ("word".equals(product)){
            dao.insertRecognition(contractNum, price, sign_date);
        }else if ("excel".equals(product)){
            int amount = price / 2;

            dao.insertRecognition(contractNum, amount, sign_date);

            //入账时间推迟一个月
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.MONTH, 1);
            dao.insertRecognition(contractNum, amount, instance.getTime());
        }else if ("powerPoint".equals(product)){
            int amount = price / 3;
            dao.insertRecognition(contractNum, amount, sign_date);

            //入账时间推迟一个月
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.MONTH, 1);
            dao.insertRecognition(contractNum, amount, instance.getTime());

            //入账时间推迟一个月
            instance.add(Calendar.MONTH, 1);
            dao.insertRecognition(contractNum, amount, instance.getTime());
        }
    }

    public int recognizedRevenue(long contractNum, Date date) throws SQLException {
        ResultSet resultSet = dao.findRecognitionsFor(contractNum, date);
        int total = 0;
        while (resultSet.next()){
            total += resultSet.getInt("amont");
        }
        return total;
    }
}

RecognitionService依赖DaoImpl来执行与db的交互。数据访问层提供如下三个能力,插入单条分期账单、根据条件查询分期账单、根据合同号查询合同。

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class DaoImpl {

    public void insertRecognition(long contractNum, int amount, Date date) {
        //insert into revenue_recognition values (contractNum, amount, date)
    }

    public ResultSet findRecognitionsFor(long contractNum, Date date) {
        //select amount from revenue_recognition where contractNum = ${contractNum} and date <= ${date}
        return null;
    }

    public ResultSet findContract(long contractNum) {
        //select * from contract c, product p where c.contractNum = ${contractNum} and c.productId = p.id
        return null;
    }

}

优点

  • 简单,容易理解

直接将业务逻辑罗列于少量的几个类。对于简单的业务场景,一眼就可以看出个大概逻辑。

  • 性能较好

不存在较多的对象之间的交互,较少的使用继承、多态等特征,代码执行效率稍微高一些。不过在分布式大行其道的今天,这些性能的提升根本微不足道。一次rpc调用的网络消耗就远远大这种性能的节约。

  • 对开发者能力要求低

因为不需要良好的设计,不需要抽象。平铺直叙就行,想到哪写到哪。开发者可以快速上手,带来的问题就是编码质量迅速恶化,后续维护成本迅速上升。

缺点

  • 无法处理复杂逻辑

当业务逻辑非常复杂时,无法以清晰的方式组织代码。对于使用事务脚本方式实现的复杂业务系统简直是后续维护者的噩梦。

  • 冗余代码较多,无法复用

缺乏明确的职责分配,导致不同的业务逻辑实现中存在较多的逻辑冗余。

  • 代码可读性差

缺乏设计的系统经过不断的迭代和人员更替,注定寄生着大量没人看得懂,没人敢改的僵尸代码

领域模型

领域模型方式要求分析领域知识,从业务逻辑中抽象出核心概念。基于核心概念讨论业务逻辑,进行系统组件设计。通过分析,我们可以总结出以下核心概念及关系。

核心业务逻辑如下

1、合同对应一个或多个账单,对应个商品,负责生成分期账单和计算当前入账金额
2、一个合同对应一个商品
3、一个商品对应一种分期策略。分期策略被抽象成接口,有多种全额支付、分两期账单、分三期账单等多种分期策略实现。

实现

基于以上的业务逻辑分析,合同类实现如下

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class Contract {

    List<RevenueRecognition> revenueRecognitionList = new ArrayList<>();
    Product product;
    Date signDate;
    int id;

    public Contract(Product product, Date signDate) {
        this.product = product;
        this.signDate = signDate;
    }

    public int recognizedRevenue(Date date){
        int total = 0;
        for (RevenueRecognition revenueRecognition : revenueRecognitionList){
            if (revenueRecognition.isRecognizableBy(date)){
                total += revenueRecognition.getAmount();
            }
        }
        return total;
    }

    public void calculateRecognitions(){
        product.calculateRevenueRecognitions(this);
    }

    public List<RevenueRecognition> getRevenueRecognitionList() {
        return revenueRecognitionList;
    }

    public Date getSignDate() {
        return signDate;
    }

    public Product getProduct() {
        return product;
    }
}

账单类实现

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class RevenueRecognition {

    int amount;

    Date date;
    public RevenueRecognition(int amount, Date date) {
        this.amount = amount;
        this.date = date;
    }

    public int getAmount() {
        return amount;
    }

    boolean isRecognizableBy(Date dateToCompare){
        return dateToCompare.after(date) || dateToCompare.equals(date);
    }

}

商品类实现

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class Product {

    String name;
    int price;
    RecognitionStrategy recognitionStrategy;

    public Product(String name, int price, RecognitionStrategy recognitionStrategy){
        this.name = name;
        this.recognitionStrategy = recognitionStrategy;
        this.price = price;
    }

    public static Product buildWordProduct(){
        return new Product("word", 10, new CompleteRecognitionStrategy());
    }

    public static Product buildExcelProduct(){
        return new Product("excel", 20, new TwoWayRecognitionStrategy(30));
    }

    public static Product buildPowerPoint(){
        return new Product("powerPoint", 30, new ThreeWayRecognitionStrategy(30, 60));
    }

    void calculateRevenueRecognitions(Contract contract){
        recognitionStrategy.calculateRevenueRecognitions(contract);
    }

    public int getPrice() {
        return price;
    }
}

分期策略定义

public interface RecognitionStrategy {
    void calculateRevenueRecognitions(Contract contract);
}

全额支付实现

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class CompleteRecognitionStrategy implements RecognitionStrategy {
    @Override
    public void calculateRevenueRecognitions(Contract contract) {
        contract.getRevenueRecognitionList().add(
            new RevenueRecognition(contract.getProduct().getPrice(), contract.getSignDate()));
    }
}

分两期支付实现

/**
 * @Author canglong
 * @Date 2022/6/29
 */
public class TwoWayRecognitionStrategy implements RecognitionStrategy {

    int firstOffset;

    public TwoWayRecognitionStrategy(int firstOffset) {
        this.firstOffset = firstOffset;
    }

    @Override
    public void calculateRevenueRecognitions(Contract contract) {
        int price = contract.getProduct().getPrice();
        int amount = price / 2;
        contract.getRevenueRecognitionList().add(new RevenueRecognition(amount, contract.getSignDate()));

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DAY_OF_MONTH, firstOffset);
        contract.getRevenueRecognitionList().add(new RevenueRecognition(amount, instance.getTime()));

    }
}

优点

  • 消除很多条件判断

对比事务脚本和领域模型实现,可以发现领域模型实现方式代码中少了很多if else风格的代码。复杂性从判断逻辑迁移到了对象间的关系之中。从一个对象到另一个对象的连续传递可以把行为传递给最有资格处理的对象。而且,这种实现方式创建了更多粒度更小的类。

  • 更容易扩展

当我们需要更改一种产品的分期策略时,只需要更改当前商品绑定的分期策略即可。当需要新增分期策略时,也只需要新增一个分期策略实现即可。符合“对扩展开发,对修改关闭”的设计原则。

缺点

  • 代码理解门槛高

即使简单的任务,出于职责明确的目的,也需要多个类交互来完成。阅读代码时,需要在多个类直接跳来跳去,以便明白它们如何交互。

  • 对开发者能力要求高

不像事务脚本任何开发者都可以快速上手,领域模型方式要求开发者对所在业务领域有较深入的理解和抽象能力。

总结

领域模型方式少了很多丑陋的if else风格的代码,将业务逻辑合理的分散的不同的业务组件中去。领域模型方式用到了面向对象语言的特征,继承、封装、多态,通过使用这些特征,将复杂性从判断逻辑迁移到了对象间的关系之中。复杂、多变的业务场景使用领域模型模式,否则使用事务脚本模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值