创建者模式, 建造者模式, Builder pattern

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

使用场景

  1. 多个部件或零件,都可以装配到一个对象中,但产生的结果又不相同时。
  2. 当初始化一个对象特别复杂的时候,比如参数多,而且很多参数都有默认值。

GoF Builder 模式包含四个角色:

  • 抽象建造者 (Builder):给出一个抽象接口,以规范产品对象各个组成成分的建造。
  • 具体建造者 (ConcreteBuilder):实现抽象建造者声明的接口,一步一步完成创建产品实例的操作;建造完成后提供产品实例。
  • 指挥者 (Director):调用具体建造者以创建产品对象。
  • 产品 (Product):建造中的复杂对象。

GoF Builder 示例:RTF 文档转换器

RTF 背景

RTF(Rich Text Format,富文本格式)是微软 1987 年推出的一种文档格式,用纯文本加控制命令描述带格式的文档。一段 RTF 内容长这样:

{\rtf1 Hello {\b World}\par}
  • {\b ... } 表示加粗
  • \par 表示段落结束

RTFReader(Director)的工作是逐个扫描这些 token:遇到 \b 就调 convertFontBold(true),遇到普通字符就调 convertCharacter(c),遇到 \par 就调 convertParagraph()。它自己不生产任何输出,只负责解析结构、驱动 Builder。虽然 RTF 现在已经很少用了,但"解析器解析 → 转换器输出不同格式"这个模型在现代依然普遍(如 Markdown 解析器输出 HTML / PDF / DOCX)。

这是 GoF 原书使用的经典案例。同一份 RTF 文档,Director(RTFReader)负责解析 token 流,每遇到字符、加粗标记、段落标记就调用 Builder 对应的方法。Director 只管读懂结构,Builder 只管决定输出,两者通过接口解耦:parse() 接收的是 TextConverter 接口,不需要知道背后的具体类型,也不需要知道最终输出格式——它只是把 RTF 语义事件翻译成接口调用,由各 ConcreteBuilder 自己决定如何响应:ASCIIConverter 忽略格式标记只保留字符,HTMLConverter 将加粗转为 <b> 标签,TeXConverter 转为 \textbf{} 命令。

// Product - 转换结果
class Document {
    private final StringBuilder content = new StringBuilder();

    public void append(String text) { content.append(text); }

    @Override
    public String toString() { return content.toString(); }
}

// Builder 接口
interface TextConverter {
    void convertCharacter(char c);
    void convertFontBold(boolean on);
    void convertParagraph();
    Document getResult();
}

// ConcreteBuilder - 纯文本(忽略所有格式标记)
class ASCIIConverter implements TextConverter {
    private Document doc = new Document();

    public void convertCharacter(char c)    { doc.append(String.valueOf(c)); }
    public void convertFontBold(boolean on) { /* 纯文本忽略粗体 */ }
    public void convertParagraph()          { doc.append("\n"); }
    public Document getResult()             { return doc; }
}

// ConcreteBuilder - HTML(格式标记 → HTML 标签)
class HTMLConverter implements TextConverter {
    private Document doc = new Document();

    public void convertCharacter(char c)    { doc.append(String.valueOf(c)); }
    public void convertFontBold(boolean on) { doc.append(on ? "<b>" : "</b>"); }
    public void convertParagraph()          { doc.append("</p>\n<p>"); }
    public Document getResult()             { return doc; }
}

// ConcreteBuilder - TeX(格式标记 → TeX 命令)
class TeXConverter implements TextConverter {
    private Document doc = new Document();

    public void convertCharacter(char c)    { doc.append(String.valueOf(c)); }
    public void convertFontBold(boolean on) { doc.append(on ? "\\textbf{" : "}"); }
    public void convertParagraph()          { doc.append("\n\n"); }
    public Document getResult()             { return doc; }
}

// Director - RTF 解析器,逐字符扫描 token 流,驱动 converter
// 它只负责识别控制字和普通字符,不决定最终输出格式
class RTFReader {
    private final String rtf;

    public RTFReader(String rtf) { this.rtf = rtf; }

    public void parse(TextConverter converter) {
        boolean inBold = false;
        int i = 0;
        while (i < rtf.length()) {
            char c = rtf.charAt(i);
            if (c == '{') {
                i++;
            } else if (c == '}') {
                if (inBold) { converter.convertFontBold(false); inBold = false; }
                i++;
            } else if (c == '\\') {
                // 解析控制字:\word[digit][ ]
                i++;
                StringBuilder word = new StringBuilder();
                while (i < rtf.length() && Character.isLetter(rtf.charAt(i)))
                    word.append(rtf.charAt(i++));
                while (i < rtf.length() && Character.isDigit(rtf.charAt(i)))
                    i++; // 跳过数字参数(如 \rtf1 中的 1)
                if (i < rtf.length() && rtf.charAt(i) == ' ')
                    i++; // 控制字后的空格是 RTF 分隔符,消耗掉
                switch (word.toString()) {
                    case "b":   converter.convertFontBold(true); inBold = true; break;
                    case "par": converter.convertParagraph(); break;
                    // 其他控制字(如 rtf1 文件头)忽略
                }
            } else {
                converter.convertCharacter(c); // 普通字符(含内容里的空格)
                i++;
            }
        }
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        // 真实 RTF 文档片段:"Hello " 普通文本,"World" 加粗,段落结束
        RTFReader reader = new RTFReader("{\\rtf1 Hello {\\b World}\\par}");

        TextConverter ascii = new ASCIIConverter();
        reader.parse(ascii);
        System.out.println(ascii.getResult());
        // → Hello World

        TextConverter html = new HTMLConverter();
        reader.parse(html);
        System.out.println(html.getResult());
        // → Hello <b>World</b></p>

        TextConverter tex = new TeXConverter();
        reader.parse(tex);
        System.out.println(tex.getResult());
        // → Hello \textbf{World}
    }
}

GoF Builder 和 AbstractFactory 都用于创建复杂对象:Builder 强调一步步构建,相同过程可得到不同结果,对象不直接返回;AbstractFactory 强调直接返回一组相互依赖的对象。

Builder vs 工厂方法

工厂方法注重整体对象的创建,Builder 注重部件构建的过程。如要制造一个超人:

  • 工厂方法:直接产生出一个力大无穷、能够飞翔、内裤外穿的超人
  • Builder:先组装手、头、脚、躯干,再把内裤外穿,一步一步构建

工厂方法创建的产品粒度较粗,Builder 的产品粒度较细、可控制每个构建步骤。

两者的关键区别不是"能创建几种产品",而是能否控制构建过程的细节。工厂方法的可变点是"类型"(创建哪种产品),Builder 的可变点是"过程"(按什么步骤、用什么方式组装)。


Java Builder 模式:可选参数与链式调用

Joshua Bloch 在《Effective Java》中以 Pizza 为例,推荐用 Builder 解决可选参数过多的问题。Pizza 有必选的尺寸,以及若干可选配料——用构造方法重载会导致组合爆炸,用 set 方法又无法保证对象不可变。Builder 通过链式调用优雅地解决了这个问题:

public class Pizza {
    private final int size;           // 必选:尺寸(英寸)
    private final boolean cheese;     // 可选:加芝士
    private final boolean pepperoni;  // 可选:加培根
    private final boolean mushrooms;  // 可选:加蘑菇

    private Pizza(Builder builder) {
        this.size      = builder.size;
        this.cheese    = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.mushrooms = builder.mushrooms;
    }

    public static class Builder {
        private final int size;
        private boolean cheese    = false;
        private boolean pepperoni = false;
        private boolean mushrooms = false;

        public Builder(int size) {
            this.size = size;
        }

        public Builder cheese(boolean val)    { cheese    = val; return this; }
        public Builder pepperoni(boolean val) { pepperoni = val; return this; }
        public Builder mushrooms(boolean val) { mushrooms = val; return this; }

        public Pizza build() { return new Pizza(this); }
    }
}

使用时:

Pizza pizza = new Pizza.Builder(12)
        .cheese(true)
        .pepperoni(true)
        .build();

Builder 继承与协变返回类型

子类继承父类 Builder 时,父类中返回 Person.Builder 的方法在子类中仍返回父类类型,链式调用会中断:

Customer customer = new Customer.Builder("Tom", 13999999999L, "北京市")
        .age(20)        // 此处返回 Person.Builder
        .alias("昵称")  // 错误:Person.Builder 上没有 alias 方法
        .build();

协变返回类型(Covariant Return Type)指子类覆写方法时,可以将返回类型改为父类返回类型的子类型。解决此问题有两种方案:

方案一:Override + 强制类型转换

在子类 Builder 中覆写父类每个设置方法,将返回值强转为子类类型:

public static class Builder extends Person.Builder {
    @Override
    public Builder age(int age) {
        return (Builder) super.age(age);
    }

    @Override
    public Builder sex(String sex) {
        return (Builder) super.sex(sex);
    }
}

缺点:需要覆写父类所有设置方法,代码冗长。

方案二:泛型递归类型参数(模拟 self-type)

将父类 Builder 声明为泛型类,用 T extends Builder<T> 表示"子类自身类型":

public class Person {
    public static class Builder<T extends Builder<T>> {
        public T age(int age) {
            this.age = age;
            return (T) this;
        }

        public T sex(String sex) {
            this.sex = sex;
            return (T) this;
        }
    }
}

子类将自身类型传入泛型参数,无需覆写各设置方法:

public class Customer {
    public static class Builder extends Person.Builder<Builder> {
        @Override
        public Customer build() {
            return new Customer(this);
        }
    }
}

如果父类是抽象类,可将 self() 声明为抽象方法,由子类负责返回正确的 this:

public abstract class Person {
    public abstract static class Builder<T extends Builder<T>> {
        public T age(int age) {
            this.age = age;
            return self();
        }

        public T sex(String sex) {
            this.sex = sex;
            return self();
        }

        abstract protected T self();
    }
}