8. 建造者模式:复杂对象的构建、Builder模式的定义、链式调用实现

说实话,我第一次接触建造者模式,是在做一个电商订单系统的时候。那个订单对象,参数多得吓人——收货地址、商品列表、优惠券、支付方式、发票信息……十几个字段,有的必填,有的选填,有的还有依赖关系。用构造方法?那得写多少个重载啊。用setter?调用方得写一堆啰嗦的代码,还容易漏掉必填项。

嗯,当时我就想,肯定有更好的办法。后来翻到《Effective Java》里讲Builder模式的那一章,豁然开朗。说白了,建造者模式就是专门用来对付这种“构造参数太多、太复杂”的场景的。

8.1 什么是建造者模式?

建造者模式(Builder Pattern)的核心思想很简单:将一个复杂对象的构建过程,与它的表示分离开来。这样,同样的构建过程,可以创建出不同的表示。

你想想看,我们平时点外卖,选套餐的过程就是典型的建造者模式。你告诉店员:“我要一个汉堡,加一份薯条,饮料要可乐。”店员按照你的要求,一步步把套餐组装好。至于这个套餐最后是用纸袋装还是托盘装,那是表示层的事,跟构建过程无关。

核心定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

8.2 为什么需要建造者模式?

我总结了一下,主要有这么几个痛点,建造者模式都能解决:

  • 参数太多:一个类有5个以上构造参数,调用方很容易搞混顺序。
  • 参数有依赖:比如选了“加冰”就必须选“冷饮”,这种校验逻辑散落在各处。
  • 对象不可变:我们希望对象创建后就不能修改,但用setter又破坏了不可变性。
  • 构建过程复杂:需要分步骤、按顺序组装,每一步都可能出错。

我记得有一次,团队里有个同事写了一个类,构造方法有8个参数,全是String类型。调用的时候,传参顺序错了,结果线上出了个隐蔽的bug。排查了一下午才发现,是“手机号”和“身份证号”传反了。从那以后,我就在团队里强制推行了建造者模式。

8.3 建造者模式的经典结构

建造者模式通常包含这几个角色:

角色 职责 类比
Product(产品) 最终要构建的复杂对象 套餐
Builder(抽象建造者) 定义构建步骤的接口 套餐制作流程
ConcreteBuilder(具体建造者) 实现构建步骤,并返回产品 店员A(按流程做)
Director(指挥者) 控制构建的顺序和逻辑 店长(指挥怎么做)

下面这张图,把建造者模式的整体流程画出来了:

Director 指挥者 调用 <<interface>> Builder + buildPartA() + buildPartB() 实现 ConcreteBuilder + buildPartA() + buildPartB() + getResult() 创建 Product + partA + partB 最终获取产品

8.4 链式调用的实现

在实际开发中,我们最常用的其实是链式调用的变体。说白了,就是把Builder的方法返回this,这样就可以连续调用了。Java里最经典的例子就是StringBuilder和StringBuffer。

下面我写一个实际的例子。假设我们要构建一个Computer对象,它有CPU、内存、硬盘、显卡等配置,有些是必填的,有些是选填的。

public class Computer {
    // 必填参数
    private final String cpu;
    private final String memory;
    
    // 选填参数
    private final String hardDisk;
    private final String graphicsCard;
    private final String powerSupply;
    
    // 私有构造方法,只能通过Builder创建
    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.memory = builder.memory;
        this.hardDisk = builder.hardDisk;
        this.graphicsCard = builder.graphicsCard;
        this.powerSupply = builder.powerSupply;
    }
    
    // 静态内部类 Builder
    public static class Builder {
        // 必填参数
        private final String cpu;
        private final String memory;
        
        // 选填参数,给默认值
        private String hardDisk = "机械硬盘 1TB";
        private String graphicsCard = "集成显卡";
        private String powerSupply = "500W";
        
        // Builder的构造方法,只接收必填参数
        public Builder(String cpu, String memory) {
            this.cpu = cpu;
            this.memory = memory;
        }
        
        // 链式调用方法,返回this
        public Builder hardDisk(String hardDisk) {
            this.hardDisk = hardDisk;
            return this;
        }
        
        public Builder graphicsCard(String graphicsCard) {
            this.graphicsCard = graphicsCard;
            return this;
        }
        
        public Builder powerSupply(String powerSupply) {
            this.powerSupply = powerSupply;
            return this;
        }
        
        // 最后调用build()方法,创建Computer对象
        public Computer build() {
            // 可以在这里做参数校验
            if (cpu == null || memory == null) {
                throw new IllegalStateException("CPU和内存不能为空");
            }
            return new Computer(this);
        }
    }
    
    // getter方法,没有setter,保证不可变性
    public String getCpu() { return cpu; }
    public String getMemory() { return memory; }
    public String getHardDisk() { return hardDisk; }
    public String getGraphicsCard() { return graphicsCard; }
    public String getPowerSupply() { return powerSupply; }
}

调用的时候,就非常清爽了:

Computer computer = new Computer.Builder("i7-13700K", "32GB DDR5")
        .hardDisk("三星 990 Pro 2TB")
        .graphicsCard("RTX 4080")
        .powerSupply("850W 金牌")
        .build();

System.out.println(computer.getCpu());        // i7-13700K
System.out.println(computer.getGraphicsCard());// RTX 4080

个人习惯:我一般把Builder作为静态内部类放在Product里面。这样调用方一眼就能看到“哦,这个类是用Builder创建的”,而且Builder可以直接访问Product的私有成员,代码更简洁。

8.5 避坑指南

我曾经踩过一个坑,在这里分享给大家:

坑1:Builder里不要做太复杂的业务逻辑。 Builder的职责就是构建对象,校验参数可以,但别在里面查数据库、调远程服务。否则你调用build()的时候,可能卡住好几秒,调用方会一脸懵。

坑2:注意线程安全。 Builder本身不是线程安全的。如果你在多线程环境下构建对象,记得加锁,或者每次new一个新的Builder。我个人习惯是每次构建都new,反正Builder对象很轻量。

坑3:不要滥用建造者模式。 如果一个类只有两三个参数,用构造方法或者setter就够了。强行用Builder反而显得过度设计。我一般遵循一个原则:参数超过4个,或者参数之间有复杂的依赖关系,才考虑用建造者模式

8.6 建造者模式 vs 工厂模式

很多初学者会把这两个模式搞混。我简单说一下区别:

  • 工厂模式:关注的是“创建哪个产品”,比如根据参数决定返回A还是B。
  • 建造者模式:关注的是“怎么一步步创建产品”,比如先装CPU再装内存。

说白了,工厂模式是一步到位,建造者模式是分步组装。如果产品构造很简单,用工厂模式就够了。如果产品构造很复杂,需要分步骤、有顺序、有校验,那就用建造者模式。

注意:建造者模式会增加类的数量。每个产品类都要配一个Builder类,代码量会翻倍。所以不要为了用模式而用模式,要真正有复杂构建的需求时再上。

8.7 实际项目中的应用

我记得在做一个支付网关的时候,需要构建一个支付请求对象。这个对象有几十个字段,包括商户号、订单号、金额、回调地址、商品描述、附加参数……而且不同支付渠道(微信、支付宝、银联)要求的字段还不一样。

用建造者模式之后,代码变得非常清晰:

PaymentRequest request = new PaymentRequest.Builder("MERCHANT_001", "ORDER_2024001", new BigDecimal("99.99"))
        .channel(PaymentChannel.WECHAT)
        .notifyUrl("https://api.myshop.com/pay/notify")
        .description("购买《Java设计模式精讲》课程")
        .extraParam("openId", "oABC123xyz")
        .build();

你看,调用方只需要关心他要配置哪些参数,完全不用管内部怎么组装。而且因为Builder方法返回的是this,IDE的自动补全用起来特别顺手,基本不会漏掉必填项。

嗯,这就是建造者模式的核心价值——让复杂对象的构建变得简单、清晰、可靠


公众号:蓝海资料掘金营,微信 deep3321