跳到正文
返回

【设计模式】建造者模式

发表于 更新于
浏览量: --

Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)

知乎专栏:秒懂设计模式之建造者模式(Builder pattern)

《Effective Java 中文版(原书第 3 版)》 —— 俞黎敏译

1. 模式的定义与特点

参考链接:建造者模式(Bulider模式)详解

建造者(Builder)模式(又称构建者模式):将复杂对象的构造过程与其表示形式分离,使同一构造过程创建出不同的表示形式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。

优点

  1. 封装性好,构建和表示分离
  2. 扩展性好,各个具体的建造者相互独立,有利于系统的解耦
  3. 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

缺点

  1. 产品的组成部分必须相同,这限制了其使用范围
  2. 如果产品的内部结构复杂,当产品内部发生变化时,建造者也要同步修改,后期维护成本较大
  3. 为了创建对象,必须先创建它的构建器,在十分注重性能的情况下,这是一笔性能开销

建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。

每个字都认识,但是合在一起就不理解了,这到底是个啥? 🤔

不明白没有关系,这段文字可记可不记,当然记住更好,以后和别人解释就有装逼的资格了。🤣

2. 适用场景

设计模式,最重要的是如何使用和适用场景。

在《Effective Java 中文版(原书第 3 版)》一书中的第 2 条就说到了:遇到多个构造器(Constructor)参数时要考虑使用构建器(Builder)。

说人话就是:如果一个类的构造函数参数有多个时,要考虑使用构造器(Builder)。

其实还是有点模糊,再说人话:当一个类的构造函数参数个数超过 4 个,而且某些参数是可选的参数,考虑使用建造者模式。

问题又来了:

  1. 建造者模式解决了哪些问题?
  2. 为什么要使用建造者模式?
  3. 怎么使用建造者模式?

3. 解决的问题

假设有这样一个 Person 类:

/**
 * @author mofan 2021/2/3
 */
public class Person {
    private String firstName;
    private String lastName;
    private Integer gender;
    private Integer age;
    private Double height;
    private Double weight;
    private String job;
}

在创建 Person 对象时,有些属性是必填的,比如 firstName、lastName 和 gender,其余属性是可选的。

那现在想要创建一个这样的 Person 对象:

firstName  	---> 默
lastName 	---> 烦
gender  	---> 1
age 		---> 19
height		---> 178.2

该怎么做呢?

3.1 重叠构造器模式

在《Effective Java 中文版(原书第 3 版)》一书中,首先提到了 重叠构造器模式,即创建一个包含需要设置的参数的构造函数。

public Person(String firstName, String lastName, Integer gender, Integer age, Double height) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.gender = gender;
    this.age = age;
    this.height = height;
}

但这种方式是很愚蠢的 😑

总之,采用 重叠构造器模式 来解决并不是一种好的方式。

3.2 JavaBeans 模式

在《Effective Java 中文版(原书第 3 版)》一书中,紧接着又提到了 JavaBeans 模式。

简单来说:先调用一个无参构造函数来创建对象,然后使用 Setter 方法来设置必须与可选参数。

public Person() {}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

public void setLastName(String lastName) {
    this.lastName = lastName;
}

public void setGender(Integer gender) {
    this.gender = gender;
}

public void setAge(Integer age) {
    this.age = age;
}

public void setHeight(Double height) {
    this.height = height;
}

public void setWeight(Double weight) {
    this.weight = weight;
}

public void setJob(String job) {
    this.job = job;
}

这种方式也存在问题 😔

因为对象的各个属性值是分步设置的,这个过程中对象的状态容易发生变化,进而产生错误。

《Effective Java 中文版(原书第 3 版)》中是这么说的:

遗憾的是, JavaBeans 模式自身有着很严重的缺点。 因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性来保证一致性。 试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。 与此相关的另一点不足在于, JavaBeans 模式使得把类做成不可变的可能性不复存在,这就需要程序员付出额外的努力来确保它的线程安全。

那有没有究极方案呢?

可以使用建造者模式! 💪

4. 建造者模式

4.1 如何使用

以 Person 为例:

  1. 在 Person 中创建一个静态嵌套类 Builder,并将 Person 中的参数都复制到 Builder 类中
  2. 在 Person 中提供一个非 public 的构造函数,参数类型为 Builder
  3. 在 Builder 中提供一个 public 的构造函数,参数为构造 Person 对象时的必填参数
  4. 在 Builder 中提供多个设置方法,用于对 Person 中可选参数进行赋值,返回值为 Builder 类型的实例(用于链式调用)
  5. 在 Builder 中提供一个 build() 方法(当然叫啥名都可以,作为构建方法),用于构建最终的 Person 实例并返回

4.2 代码展示

/**
 * @author mofan 2021/2/3
 */
public class Person {
    private String firstName;
    private String lastName;
    private Integer gender;
    private Integer age;
    private Double height;
    private Double weight;
    private String job;

    private Person(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.gender = builder.gender;
        this.age = builder.age;
        this.height = builder.height;
        this.weight = builder.weight;
        this.job = builder.job;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Integer getGender() {
        return gender;
    }

    public Integer getAge() {
        return age;
    }

    public Double getHeight() {
        return height;
    }

    public static class Builder {
        private String firstName;
        private String lastName;
        private Integer gender;
        private Integer age;
        private Double height;
        private Double weight;
        private String job;

        public Builder(String firstName, String lastName, Integer gender) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.gender = gender;
        }

        public Builder setAge(Integer age) {
            this.age = age;
            return this;
        }

        public Builder setHeight(Double height) {
            this.height = height;
            return this;
        }

        public Builder setWeight(Double weight) {
            this.weight = weight;
            return this;
        }

        public Builder setJob(String job) {
            this.job = job;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }
}
@Test
public void testBuilder() {
    Person person = new Person.Builder("默", "烦", 1)
            .setAge(19)
            .setHeight(178.2)
            .build();
    assertNotNull(person);
    assertEquals("默", person.getFirstName());
    assertEquals("烦", person.getLastName());
    assertEquals(1, person.getGender().intValue());
    assertEquals(19, person.getAge().intValue());
    assertEquals(178.2, person.getHeight(), 0.0);
}

上述这种方式是在 Java 中简化的使用方式,经典的建造者模式与其有所不同,不感兴趣可以跳过第 5 节。😜

5. 经典建造者模式

5.1 角色与结构

建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个角色构成。

建造者(Builder)模式的主要角色如下:

  1. 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
  2. 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
  3. 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  4. 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。

建造者模式的结构图:

建造者模式的结构图

5.2 代码展示

目标 Person2 类

/**
 * @author mofan 2021/2/3
 */
public class Person2 {
    private String firstName;
    private String lastName;
    private Integer gender;
    private Integer age;
    private Double height;
    private Double weight;
    private String job;

    /**
     * 含有必填属性的构造函数
     */
    public Person2(String firstName, String lastName,
                   Integer gender) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.gender = gender;
    }

    /**
     * 可选属性的 Setter 方法
     */
    public void setAge(Integer age) {
        this.age = age;
    }

    /**
     * 可选属性的 Setter 方法
     */
    public void setHeight(Double height) {
        this.height = height;
    }

    /**
     * 可选属性的 Setter 方法
     */
    public void setWeight(Double weight) {
        this.weight = weight;
    }

    /**
     * 可选属性的 Setter 方法
     */
    public void setJob(String job) {
        this.job = job;
    }
	
    // 省略所有属性的 Getter 方法
}

抽象建造者类

/**
 * @author mofan 2021/2/3
 */
public abstract class Person2Builder {
    public abstract void setAge();

    public abstract void setHeight();

    public abstract void  setWeight();

    public abstract void  setJob();

    public abstract Person2 getPerson2();
}

实体建造者类,可以根据需求构建出多个实体建造者类,在此只构建一个 Student 类:

/**
 * @author mofan 2021/2/3
 */
public class Student extends Person2Builder {

    private Person2 person2;

    public Student(String firstName, String lastName,
                   Integer gender) {
        person2 = new Person2(firstName, lastName, gender);
    }

    @Override
    public void setAge() {
        person2.setAge(19);
    }

    @Override
    public void setHeight() {
        person2.setHeight(178.2);
    }

    @Override
    public void setWeight() {
        person2.setWeight(125.6);
    }

    @Override
    public void setJob() {
        person2.setJob(null);
    }

    @Override
    public Person2 getPerson2() {
        return person2;
    }
}

指导者类

/**
 * @author mofan 2021/2/3
 */
public class Person2Director {
    public void makePerson2(Person2Builder builder) {
        /*
         * 要为目标实体设置那些可选参数就在此设置
         * 比如,我只想让我的目标实体具有 age 和 height 两个可选属性值
         */
        builder.setAge();
        builder.setHeight();
    }
}

使用与测试

@Test
public void testPerson2() {
    Person2Director director = new Person2Director();
    Student student = new Student("默", "烦", 1);
    director.makePerson2(student);
    Person2 person2 = student.getPerson2();
    assertEquals("默", person2.getFirstName());
    assertEquals("烦", person2.getLastName());
    assertEquals(1, person2.getGender().intValue());
    assertEquals(19, person2.getAge().intValue());
    assertEquals(178.2, person2.getHeight(), 0.0);
}

思考

相比于经典建造者模式,最初的建造者模式省略了 Director 这个角色,将构建算法交给了 Client 端,其次将 Builder 写到了要构建的产品类里面,最后采用了链式调用。

上述实现中的 Student 对象的属性值都是无法修改的,因此建造者模式也常用于创建 一个成员变量不可变的对象。所以在最初的建造者模式中,常把 Person 的属性设置成 final 的。如:

public class Person {
    private final String firstName;
    private final String lastName;
    private final Integer gender;
    private final Integer age;
    private final Double height;
    private final Double weight;
    private final String job;
    
    // --snip--
}

6. Lombok 中的 @Builder

6.1 基本使用

建造者模式的实现步骤都差不多,就像一个模板一样。

在 Lombok 中提供了 @Getter 和 @Setter 注解来解决冗杂 Getter 和 Setter 方法,它还提供了 @Builder 注解来一键生成建造者模式的代码。

创建一个新实体 Animal,并使用 @Builder 注解:

/**
 * @author mofan 2021/2/3
 */
@Builder
@Getter
public class Animal {
    private final String name;
    private final String type;
    private final Integer age;
    private final Integer gender;
}
/**
 * @author mofan 2021/2/3
 */
public class AnimalTest {
    @Test
    public void testAnimal() {
        Animal animal = Animal.builder()
                .name("小黑")
                .gender(1)
                .type("Dog")
                .build();
        assertEquals("小黑", animal.getName());
        assertEquals(1, animal.getGender().intValue());
        assertEquals("Dog", animal.getType());
        assertNull(animal.getAge());
    }
}

查看 IDEA 反编译 Animal.class 后的内容:

public class Animal {
    private final String name;
    private final String type;
    private final Integer age;
    private final Integer gender;

    Animal(String name, String type, Integer age, Integer gender) {
        this.name = name;
        this.type = type;
        this.age = age;
        this.gender = gender;
    }

    public static Animal.AnimalBuilder builder() {
        return new Animal.AnimalBuilder();
    }

    public String getName() {
        return this.name;
    }

    public String getType() {
        return this.type;
    }

    public Integer getAge() {
        return this.age;
    }

    public Integer getGender() {
        return this.gender;
    }

    public static class AnimalBuilder {
        private String name;
        private String type;
        private Integer age;
        private Integer gender;

        AnimalBuilder() {
        }

        public Animal.AnimalBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Animal.AnimalBuilder type(String type) {
            this.type = type;
            return this;
        }

        public Animal.AnimalBuilder age(Integer age) {
            this.age = age;
            return this;
        }

        public Animal.AnimalBuilder gender(Integer gender) {
            this.gender = gender;
            return this;
        }

        public Animal build() {
            return new Animal(this.name, this.type, this.age, this.gender);
        }

        public String toString() {
            return "Animal.AnimalBuilder(name=" + this.name + ", type=" + this.type + ", age=" + this.age + ", gender=" + this.gender + ")";
        }
    }
}

与前文的建造者模式的实现相比,Lombok 生成的代码添加了静态方法 builder(),并且 AnimalBuilder 的构造函数不再是 public,仅仅是包私有的。

这样在构建 Animal 对象时不再需要使用关键词 new,而是直接使用静态方法 builder()。

6.2 进一步改进

如果只用 @Builder 注解,可以看到 Lombok 为 Animal 类生成的构造函数是包私有的。

之所以用建造者模式,是希望用户 只用 建造者模式提供的方法去创建实例,包私有的构造函数,可以被同一包中的类调用,所以需要将这个构造函数设置为 private:

@AllArgsConstructor(access = AccessLevel.PRIVATE)

最终的 Animal 类:

@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
    private final String name;
    private final String type;
    private final Integer age;
    private final Integer gender;
}

重新编译后,生成的构造函数:

private Animal(String name, String type, Integer age, Integer gender) {
    this.name = name;
    this.type = type;
    this.age = age;
    this.gender = gender;
}

6.3 可以进行的拓展

使用 Lombok 的 @Builder 后虽然生成了建造者模式的结构,也可以实现属性值的选择性设置,但并没做到属性值的必须设置。要想实现也很简单只需要:

/**
 * @author mofan 2021/2/3
 */
@Builder(builderMethodName = "hiddenBuilder")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
    private final String name;
    private final String type;
    private final Integer age;
    private final Integer gender;

    public static AnimalBuilder builder(String type) {
        return hiddenBuilder().type(type);
    }
}

使用 @Builder 注解的 builderMethodName 属性为自动生成的 builder 方法设置一个新的名字。

然后自己编写一个方法,在这个方法中调用生成的方法,并设置必填值。

@Test
public void testAnimal() {
    Animal animal = Animal.builder("Dog")
            .name("小黑")
            .gender(1)
            .build();
    assertEquals("小黑", animal.getName());
    assertEquals(1, animal.getGender().intValue());
    assertEquals("Dog", animal.getType());
    assertNull(animal.getAge());
}

重新编译后,生产的部分内容为:

// 我们自己编写的代码
public static Animal.AnimalBuilder builder(String type) {
    return hiddenBuilder().type(type);
}

// 自动生成的代码
public static Animal.AnimalBuilder hiddenBuilder() {
    return new Animal.AnimalBuilder();
}

6.4 代码兼容

无参构造与全参构造

如果没有显式声明一个类的构造函数,默认会有一个无参构造函数,但如果声明了一个有参构造函数,这个默认的无参构造函数就不存在了,除非显式声明它。

假设一个类在最初设计时并没有使用 @Builder 注解,在后续编码中突然使用了这个注解,由于 @Builder 注解会产生全参构造函数,为了能够更好地兼容旧代码,需要再使用 @NoArgsConstructor 注解为其声明无参构造函数。

这样显式声明无参构造函数后,@Builder 默认产生的全参构造函数就会失效,导致编译报错,因此还需要添加 @AllArgsConstructor(access = AccessLevel.PRIVATE) 注解。

默认值的处理

如果类中的属性存在默认值,这时再使用了 @Builder 注解,设置的默认值会失效。

这是因为 Lombok 生成的静态嵌套类中的属性不包含默认值。

为了让这些默认值生效,且能够愉快地使用 @Builder 注解,只需要在设置了默认值的字段上使用 @Builder.Default 注解即可。

7. 在类层次结构中使用

建造者模式也适用于类层次结构。抽象类有抽象的 builder,具体类有具体的 builder。

以下代码来自《Effective Java 中文版(原书第 3 版)》 一书。

声明枚举 Topping,表示披萨的各种馅料:

/**
 * @author mofan
 * @date 2022/10/5 12:34
 */
public enum Topping {
    /**
     * 火腿
     */
    HAM,

    /**
     * 蘑菇
     */
    MUSHROOM,

    /**
     * 洋葱
     */
    ONION,

    /**
     * 胡椒
     */
    PEPPER,

    /**
     * 香肠
     */
    SAUSAGE
}

用一个抽象类表示各种各样的披萨:

/**
 * @author mofan
 * @date 2022/10/5 12:34
 */
@Getter
public abstract class Pizza {
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        public abstract Pizza build();

        protected abstract T self();
    }

    public Pizza(Builder<?> builder) {
        this.toppings = builder.toppings.clone();
    }
}

现有两种具体的披萨,分别是经典纽约风味的披萨和馅料内置的半月型披萨。声明枚举 Size,表示披萨的尺寸:

/**
 * @author mofan
 * @date 2022/10/5 13:21
 */
public enum Size {
    /**
     * 小
     */
    SMALL,

    /**
     * 中
     */
    MEDIUM,

    /**
     * 大
     */
    LARGE
}

经典纽约风味的披萨的实现:

/**
 * @author mofan
 * @date 2022/10/5 13:22
 * <p>
 * 经典纽约风味披萨
 */
@Getter
public class NyPizza extends Pizza {

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    public NyPizza(Builder builder) {
        super(builder);
        this.size = builder.size;
    }
}

馅料内置的半月型披萨的实现:

/**
 * @author mofan
 * @date 2022/10/5 14:06
 *
 * 馅料内置的半月型披萨
 */
@Getter
public class Calzone extends Pizza {
    /**
     * 是否有酱汁
     */
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false;

        public Builder sauceInside() {
            this.sauceInside = true;
            return this;
        }

        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    public Calzone(Builder builder) {
        super(builder);
        this.sauceInside = builder.sauceInside;
    }
}
@Test
public void testPizza() {
    NyPizza pizza = new NyPizza.Builder(Size.SMALL)
            .addTopping(Topping.SAUSAGE)
            .addTopping(Topping.ONION)
            .build();
    assertEquals(Size.SMALL, pizza.getSize());
    assertTrue(pizza.getToppings().containsAll(new HashSet<>(Arrays.asList(Topping.SAUSAGE, Topping.ONION))));

    Calzone calzone = new Calzone.Builder()
            .addTopping(Topping.HAM)
            .sauceInside()
            .build();
    assertTrue(calzone.isSauceInside());
    assertEquals(1, calzone.getToppings().size());
    assertTrue(calzone.getToppings().contains(Topping.HAM));
}

8. 总结

建造者模式适用的场景

  1. 当一个类的构造函数参数个数较多(超过 4 个),而且某些参数是可选的,或者某些参数又是必选的。

  2. 要创造的对象是一个成员变量不可变的对象。

理解 创建的对象是一个成员变量不可变的对象

在经典建造者模式中,在具体建造者(Concrete Builder)中将属性值写死,导致属性值不可变,这里可以体现成员变量不可变。

在最初的建造者模式中,没有采用 JavaBeans 模式的方式给对象的属性赋值,因此类中没有提供任何字段的 Setter 方法,创建出来的对象的字段值无法再被修改。

因此,在使用建造者模式时,常用 final 修饰字段。

如何实现

  1. 参考第 4 节
  2. 使用 Lombok 的 @Builder 和 @AllArgsConstructor 注解

如果一个类中有大量成员变量,但它们并不是不可变的对象,还可以建造者模式吗?

这没有标准答案,因为既可以把对字段的赋值过程改成链式的,也可以使用 Lombok 中的 @Accessors(chain = true) 实现链式调用,当然使用建造者模式也没问题。

说句题外话,有些第三方类库或框架不支持链式调用,因此 Lombok 的 @Accessors(chain = true) 注解不要随意使用。

简而言之:如果创建一个对象所用的构造器(Constructor)或静态工厂有多个参数时,可以考虑使用建造者模式;如果这些参数有部分必选的,还有部分是可选的,那就更应该考虑建造者模式。

9. 补充:链式调用新思路

为实现链式调用,常常需要使 Setter 返回 this,或者使用 Lombok 的 @Accessors(chain = true) 注解。两种方式实现的链式调用在编译后生成的 class 文件中的 Setter 都返回了 this,但这在某些情况会出现问题,比如使用 CGLib 的 BeanCopier 进行 Bean 拷贝时会因为 Setter 返回了 this 导致拷贝失败。

在这种情况下除了使用建造者模式外,还有其他的方法吗?

/**
 * 链式调用的新思路
 * 我并不认为这是建造者模式的通用写法
 *
 * @author mofan
 * @date 2022/11/24 22:08
 */
public class Builder<T> {
    private final Supplier<T> CONSTRUCTOR;
    private Consumer<T> injector = (t) -> { };

    private Builder(Supplier<T> constructor) {
        this.CONSTRUCTOR = constructor;
    }

    public static <T> Builder<T> builder(Supplier<T> constructor) {
        return new Builder<>(constructor);
    }

    public <F> Builder<T> with(BiConsumer<T, F> biConsumer, F f) {
        Consumer<T> c = instance -> biConsumer.accept(instance, f);
        injector = injector.andThen(c);
        return this;
    }

    public T build() {
        T t = CONSTRUCTOR.get();
        injector.accept(t);
        return t;
    }

}

测试一下:

@Test
public void test() {
    Student student = Builder.builder(Student::new)
            .with(Student::setName, "mofan")
            .with(Student::setAge, 20)
            .build();
    Assert.assertEquals("mofan", student.getName());
    Assert.assertEquals(20, student.getAge());
}

有人管这个 Builder 叫做通用 Builder,试图将其与建造者模式联系上,但我认为这个 Builder 与建造者模式没有什么关系。

首先没法做到在创建对象时让一些参数必填,一些参数可选。或许可以提供多个 Builder#builder() 方法的重载?

但这样需要目标类中提供公有的有参构造函数,除此之外,在后续的操作中还要求目标类提供共有的 Setter,让创建出的对象能被随意修改。无论是公有的有参构造函数,还是公用的 Setter,这些在建造者模式中都不应该向外部暴露。

并不是说这个 Builder 一无是处,正如标题写的那样,这为链式调用提供了一种新思路。

10. 补充:Step Builder

参考链接:Step builder pattern

可以认为 Step Builder 是普通 Builder 的升级版,它用于逐步构建复杂对象,提供流式接口来创建具有大量可配置参数的对象。

Step Builder 不仅具有建造者模式的优点,对象的构建过程也更加清晰, 用户只会看到下一步可用的方法, 直到构建对象的正确时间才能看到构建方法。

Step Builder 并不是完美的,实现其功能的代码可读性较低,并且没有类似 Lombok 的第三方库来帮助代码生成。

10.1 需求场景

现有一 Email 类:

@Getter
public class Email {
    private final EmailAddress from;
    private final List<EmailAddress> to;
    private final List<EmailAddress> cc;
    private final List<EmailAddress> bcc;
    private final String subject;
    private final String content;
}

在构造 Email 时,希望能按照一定的顺序完成,比如第一步只能设置 from 的值,第二步只能设置 to 的值。

构造 Email 的过程如下,紫色表示必填值,黄色表示可选值:

step-builder

10.2 代码实现

/**
 * @author mofan
 * @date 2024/1/12 17:58
 * @link <a href="http://www.svlada.com/step-builder-pattern/">Step builder pattern</a>
 */
@Getter
public class Email {
    private final EmailAddress from;
    private final List<EmailAddress> to;
    private final List<EmailAddress> cc;
    private final List<EmailAddress> bcc;
    private final String subject;
    private final String content;

    public static FromStep builder() {
        return new Builder();
    }

    public sealed interface FromStep permits Builder {
        ToStep from(EmailAddress from);
    }

    public sealed interface ToStep permits Builder {
        SubjectStep to(List<EmailAddress> to);
    }

    public sealed interface SubjectStep permits Builder {
        ContentStep subject(String subject);
    }

    public sealed interface ContentStep permits Builder {
        Build content(String content);
    }

    public sealed interface Build permits Builder {
        /**
         * 构造 Email 对象
         */
        Email build();

        /**
         * cc 是可选的
         */
        Build cc(List<EmailAddress> cc);

        /**
         * bcc 也是可选的
         */
        Build bcc(List<EmailAddress> bcc);
    }

    private Email(Builder builder) {
        this.from = builder.from;
        this.to = builder.to;
        this.cc = builder.cc;
        this.bcc = builder.bcc;
        this.subject = builder.subject;
        this.content = builder.content;
    }

    public final static class Builder implements FromStep, ToStep, SubjectStep, ContentStep, Build {
        private EmailAddress from;
        private List<EmailAddress> to;
        private String subject;
        private String content;
        private final List<EmailAddress> cc = new ArrayList<>();
        private final List<EmailAddress> bcc = new ArrayList<>();

        @Override
        public Email build() {
            return new Email(this);
        }

        @Override
        public ToStep from(EmailAddress from) {
            Objects.requireNonNull(from);
            this.from = from;
            return this;
        }

        @Override
        public SubjectStep to(List<EmailAddress> to) {
            if (CollectionUtils.isEmpty(to)) {
                throw new RuntimeException();
            }
            this.to = new ArrayList<>(to);
            return this;
        }

        @Override
        public ContentStep subject(String subject) {
            Objects.requireNonNull(subject);
            this.subject = subject;
            return this;
        }

        @Override
        public Build content(String content) {
            Objects.requireNonNull(content);
            this.content = content;
            return this;
        }

        @Override
        public Build cc(List<EmailAddress> cc) {
            this.cc.addAll(CollectionUtils.emptyIfNull(cc));
            return this;
        }

        @Override
        public Build bcc(List<EmailAddress> bcc) {
            this.bcc.addAll(CollectionUtils.emptyIfNull(bcc));
            return this;
        }
    }
}
@Test
public void testBuild() {
    Email email = Email.builder()
            .from(EmailAddress.from("A"))
            .to(List.of(EmailAddress.from("B"), EmailAddress.from("C")))
            .subject("subject")
            .content("content")
            .bcc(List.of(EmailAddress.from("D")))
            .cc(List.of(EmailAddress.from("E")))
            .build();

    assertThat(email.getFrom().getAddress()).isEqualTo("A");
    assertThat(email.getTo())
            .map(EmailAddress::getAddress)
            .containsExactly("B", "C");
    assertThat(email.getSubject()).isEqualTo("subject");
    assertThat(email.getContent()).isEqualTo("content");
    assertThat(email.getBcc()).map(EmailAddress::getAddress)
            .singleElement()
            .isEqualTo("D");
    assertThat(email.getCc()).map(EmailAddress::getAddress)
            .singleElement()
            .isEqualTo("E");
}

10.3 柯里化

参考链接:Currying

百度百科对 柯里化 的解释是:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

给定函数 f : (X×Y)→Zf \ : \ (X \times Y) \rightarrow Z,使用科里化得到新函数 h : X→(Y→Z)h \ : \ X \rightarrow (Y \rightarrow Z),hh 从 XX 中获取一个参数,然后返回一个将 YY 映射到 ZZ 的函数,因此有 h(x)(y)=f(x,y)h(x)(y)=f(x,y)。

以 JavaScript 为例,存在名为 add 的函数,接收三个参数,最终返回它们的和:

function add(a, b, c) {
    return a + b + c
}

add(1, 2, 3) // 6

如果对 add 函数进行柯里化,那么有:

function curriedAdd(a) {
    return function (b) {
        return function (c) {
            return a + b + c
        }
    }
}

curriedAdd(1)(2)(3) // 6

在 Java8 引入 Lambda 表达式后,Java 也能很轻松实现类似的功能。

比如现在需要构造一个 Book 对象,需要依次传入类型、作者、书名和出版时间等信息。一般来说,一个作者会出版许多同类型、不同名称的书籍,在构造这样的 Book 对象时,如果每次都传入相同的类型和作者信息会使得代码很冗余,为此可以参考柯里化编写出以下代码:

public enum Genre {
    /**
     * 奇幻
     */
    FANTASY,

    /**
     * 恐怖
     */
    HORROR,

    /**
     * 科幻
     */
    SCI_FI,
}
public record Book(Genre genre, String author, String title, LocalDate publicationDate) {
    public static AddGenre builder() {
        return genre
                -> author
                -> title
                -> publicationDate
                -> new Book(genre, author, title, publicationDate);
    }

    public interface AddGenre {
        Book.AddAuthor withGenre(Genre genre);
    }

    public interface AddAuthor {
        Book.AddTitle withAuthor(String author);
    }

    public interface AddTitle {
        Book.AddPublicationDate withTitle(String title);
    }

    public interface AddPublicationDate {
        Book withPublicationDate(LocalDate publicationDate);
    }

}

现在可以这样构造 Book 对象:

@Test
public void test() {
    // Defining genre book functions
    Book.AddAuthor fantasyBookFunc = Book.builder().withGenre(Genre.FANTASY);
    Book.AddAuthor horrorBookFunc = Book.builder().withGenre(Genre.HORROR);
    Book.AddAuthor scifiBookFunc = Book.builder().withGenre(Genre.SCI_FI);

    // Defining author book functions
    Book.AddTitle kingFantasyBooksFunc = fantasyBookFunc.withAuthor("Stephen King");
    Book.AddTitle kingHorrorBooksFunc = horrorBookFunc.withAuthor("Stephen King");
    Book.AddTitle rowlingFantasyBooksFunc = fantasyBookFunc.withAuthor("J.K. Rowling");

    // Creates books by Stephen King (horror and fantasy genres)
    Book shining = kingHorrorBooksFunc.withTitle("The Shining")
        .withPublicationDate(LocalDate.of(1977, 1, 28));
    Book darkTower = kingFantasyBooksFunc.withTitle("The Dark Tower: Gunslinger")
        .withPublicationDate(LocalDate.of(1982, 6, 10));

    // Creates fantasy books by J.K. Rowling
    Book chamberOfSecrets = rowlingFantasyBooksFunc.withTitle("Harry Potter and the Chamber of Secrets")
        .withPublicationDate(LocalDate.of(1998, 7, 2));

    // Create sci-fi books
    Book dune = scifiBookFunc.withAuthor("Frank Herbert")
        .withTitle("Dune")
        .withPublicationDate(LocalDate.of(1965, 8, 1));
    Book foundation = scifiBookFunc.withAuthor("Isaac Asimov")
        .withTitle("Foundation")
        .withPublicationDate(LocalDate.of(1942, 5, 1));

    // --snip--
}
如果这篇文章对你有帮助,可以通过 赞赏 请我喝杯 Coffee ☕


上一篇
MapStruct 使用手册(更新中)
下一篇
使用 Java 操作 Excel