建造者模式:把复杂对象的组装过程封装起来

说实话,建造者模式是我在实际项目中用得最多的创建型模式之一。为什么?因为我们在真实开发中,经常会遇到那种「参数多到令人发指」的对象。你想想看,一个订单对象可能有十几个字段,有些必填、有些可选、有些还有依赖关系——这时候如果直接用构造函数,那代码简直没法看。

建造者模式的核心思想,说白了就是:把对象的构建过程和它的最终表示分离开。同样的构建步骤,可以产出不同的产品形态。就像做汉堡,步骤永远是「选面包→加肉饼→加蔬菜→加酱料」,但你可以做出牛肉汉堡、鸡肉汉堡、素汉堡——流程一样,结果不同。

什么时候该用建造者模式?

我个人习惯,当出现以下信号时,就会考虑引入建造者模式:

  • 对象的构造函数参数超过4个,而且很多是可选参数
  • 参数之间存在依赖关系(比如A参数必须和B参数一起出现)
  • 对象的构建过程有固定的步骤顺序
  • 同样的构建过程需要产出不同的产品变体

我在项目中遇到过这样一个场景:一个报表配置对象,有数据源、过滤条件、排序规则、分页设置、图表类型、颜色主题、导出格式……总共十几个参数。最开始大家用重载构造函数,后来改用setter方法,但总有人忘记调用某个必要的setter。最后我们用建造者模式重构,代码清晰多了,bug也少了一大半。

建造者模式的结构

先来看一下建造者模式的经典结构。我画了一张图,帮你快速理解各个角色的关系:

Director(指挥者) Builder(抽象建造者) ConcreteBuilderA ConcreteBuilderB ProductA ProductB 建造者模式结构图:指挥者控制构建流程,具体建造者实现产品组装

这张图里,Director 负责控制构建流程,它不关心具体产品长什么样。Builder 是抽象接口,定义了构建步骤。ConcreteBuilder 实现具体的构建逻辑,最终产出 Product

代码示例:从糟糕到优雅

先看一个反面教材。假设我们要构建一个电脑配置对象:

// 糟糕的做法: telescoping constructor 模式
public class Computer {
    private String cpu;
    private String gpu;
    private int ram;
    private int storage;
    private boolean hasBluetooth;
    private boolean hasWifi;
    
    public Computer(String cpu, String gpu, int ram, int storage) {
        this(cpu, gpu, ram, storage, false, false);
    }
    
    public Computer(String cpu, String gpu, int ram, int storage, boolean hasBluetooth) {
        this(cpu, gpu, ram, storage, hasBluetooth, false);
    }
    
    public Computer(String cpu, String gpu, int ram, int storage, 
                    boolean hasBluetooth, boolean hasWifi) {
        this.cpu = cpu;
        this.gpu = gpu;
        this.ram = ram;
        this.storage = storage;
        this.hasBluetooth = hasBluetooth;
        this.hasWifi = hasWifi;
    }
}

// 调用时,你根本分不清参数的含义
Computer c = new Computer("i7", "RTX3060", 16, 512, true, false);

这种代码,我每次看到都想叹气。参数一多,调用方根本记不住顺序,而且可读性极差。用建造者模式重构一下:

public class Computer {
    // 所有字段都是 final,保证不可变性
    private final String cpu;
    private final String gpu;
    private final int ram;
    private final int storage;
    private final boolean hasBluetooth;
    private final boolean hasWifi;
    
    // 私有构造函数,只能通过 Builder 创建
    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.gpu = builder.gpu;
        this.ram = builder.ram;
        this.storage = builder.storage;
        this.hasBluetooth = builder.hasBluetooth;
        this.hasWifi = builder.hasWifi;
    }
    
    // 静态内部 Builder 类
    public static class Builder {
        // 必填参数
        private final String cpu;
        private final int ram;
        
        // 可选参数,设置默认值
        private String gpu = "集成显卡";
        private int storage = 256;
        private boolean hasBluetooth = false;
        private boolean hasWifi = true;
        
        public Builder(String cpu, int ram) {
            this.cpu = cpu;
            this.ram = ram;
        }
        
        public Builder gpu(String gpu) {
            this.gpu = gpu;
            return this;
        }
        
        public Builder storage(int storage) {
            this.storage = storage;
            return this;
        }
        
        public Builder bluetooth(boolean hasBluetooth) {
            this.hasBluetooth = hasBluetooth;
            return this;
        }
        
        public Builder wifi(boolean hasWifi) {
            this.hasWifi = hasWifi;
            return this;
        }
        
        public Computer build() {
            // 可以在这里做参数校验
            if (ram < 4) {
                throw new IllegalArgumentException("内存至少4GB");
            }
            return new Computer(this);
        }
    }
}

// 调用方式:链式调用,清晰明了
Computer computer = new Computer.Builder("i7", 16)
    .gpu("RTX3060")
    .storage(512)
    .bluetooth(true)
    .build();

关键点:建造者模式让对象的构建过程变得「可读」且「可控」。每个setter方法返回Builder自身,实现链式调用。最终通过build()方法产出不可变的对象。

避坑指南

我曾经踩过一个坑:在一个项目中,我们给Builder的每个setter方法都做了复杂的参数校验。结果build()方法反而没做校验。后来发现,有些调用方跳过了某些setter,直接调build(),导致生成了不合法的对象。

我的建议是:参数校验集中在build()方法里做。setter方法只做简单的类型转换和赋值,不要在里面写复杂的业务逻辑。这样逻辑更集中,也更容易维护。

建造者模式的变体

在实际开发中,建造者模式有几种常见的变体,我简单列一下:

变体名称 适用场景 特点
经典建造者 有固定的构建步骤,需要不同的产品表示 有Director角色,控制构建流程
简化建造者 没有复杂的步骤顺序,只是参数多 去掉Director,Builder直接返回产品
递归建造者 对象包含子对象,需要嵌套构建 Builder支持泛型继承,实现链式调用

我个人最常用的是简化建造者,也就是上面代码示例那种。大多数业务场景其实不需要Director,因为构建步骤没那么复杂。Director更适合那种「构建流程本身会变化」的场景,比如不同版本的报表生成器。

与工厂模式的区别

很多初学者会把建造者模式和工厂模式搞混。我简单说一下我的理解:

  • 工厂模式:关注的是「创建哪个产品」,把选择权交给工厂,调用方不关心具体类名
  • 建造者模式:关注的是「怎么创建产品」,把组装过程拆解成步骤,调用方可以精细控制

说白了,工厂模式是「我给你参数,你给我产品」。建造者模式是「我一步步告诉你我要什么,最后你组装好给我」。两者不冲突,甚至可以结合使用——工厂负责选择具体的建造者,建造者负责组装产品。

小技巧:如果你发现一个类的构造函数参数超过5个,或者有超过3个可选参数,那就应该考虑用建造者模式了。别等到代码写成一团乱麻再重构,那时候改起来成本高得多。

实际项目中的应用

我记得在一个微服务项目中,我们需要构建复杂的查询条件对象。这个对象有分页、排序、过滤、字段选择、聚合计算等等。最开始用Map传参,代码里到处都是字符串常量,维护起来简直噩梦。

后来我们用建造者模式重构了查询构建器:

Query query = new Query.Builder("users")
    .select("id", "name", "email")
    .where("age", ">", 18)
    .and("status", "=", "active")
    .orderBy("createTime", SortOrder.DESC)
    .page(1, 20)
    .build();

你看,代码读起来就像在写SQL语句一样自然。这就是建造者模式的魅力——它让代码的调用方式接近自然语言,可读性极强。

嗯,这里要注意一点:建造者模式虽然好用,但也不是万能的。如果你的对象只有两三个参数,用建造者模式反而显得过度设计。记住,模式是为了解决问题而存在的,不是为了炫技。


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