Builder Pattern and Inheritance – 生成器模式和继承

最后修改: 2024年 1月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn about the challenges in implementing the Builder Design Pattern while dealing with hierarchal inheritance. An example of a hierarchical inheritance could be the inheritance between an electric car, a car, and a vehicle.

在本教程中,我们将了解在处理分层继承时实现 Builder 设计模式所面临的挑战。分层继承的一个例子是电动汽车、汽车和车辆之间的继承。

Builder Pattern is a creational design pattern that helps simplify building complex objects having many attributes in a step-by-step process with the help of method chaining. While inheritance helps simplify design, it also leads to complexity in implementing method chaining to create objects in the Builder Pattern.

构建器模式是一种娱乐设计模式,它有助于在方法链的帮助下一步步简化构建具有许多属性的复杂对象的过程。虽然继承有助于简化设计,但它也会导致在构建器模式中实现方法链以创建对象的复杂性。

Further, we’ll come up with an efficient implementation with the help of Java Generics API.

此外,我们还将借助 Java Generics API 来实现高效的实现。

2. Problem Description

2.问题描述

Let’s take an example of applying the Builder Pattern in creating objects of type Vehicle, Car and ElectricCar:

让我们以创建 车辆汽车电动汽车类型的对象为例,说明如何应用构建器模式:

 

latest builder pattern

最新建筑商模式

At the top of the object hierarchy, there is the Vehicle class. The class Car extends the Vehicle and then the ElectricCar extends Car. Similar to these objects their builders also have a hierarchical relationship between them.

在对象层次结构的顶层,有一个 Vehicle 类。Car 扩展了 Vehicle ,然后 ElectricCar 扩展了 Car 。与这些对象类似,它们的构建器之间也有层次关系。

Let’s instantiate the CarBuilder class, set its attributes with method chaining, and finally call the build() method to get the car object:

让我们实例化 CarBuilder 类,使用方法链设置其属性,最后调用 build() 方法获取 car 对象:

CarBuilder carBuilder = new CarBuilder();
Car car = carBuilder.make("Ford")
  .model("F")
  .fuelType("Petrol")
  .colour("red")
  .build();

Let’s try to change the order of the method calls:

让我们尝试改变方法调用的顺序:

CarBuilder carBuilder = new CarBuilder();
Car car = carBuilder.make("Ford")
  .colour("red")
  .fuelType("Petrol")
  .model("F")
  .build();

The methods colour() and fuelType() return VehicleBuilder class. Hence, the subsequent call to model() would cause a compilation error because it’s absent in the VehicleBuilder class. This is inconvenient and a drawback. Similar behaviour is seen when we try to build the ElectricVehicle object with the ElectricVehicleBuilder class.

方法 colour()fuelType() 返回 VehicleBuilder 类。因此,随后调用 model() 将导致编译错误,因为 VehicleBuilder 类中没有该方法。这既不方便,也是一个缺点。当我们尝试使用 ElectricVehicleBuilder 类构建 ElectricVehicle 对象时,也会看到类似的行为。

3. Solution Without Generics

3.无通用名解决方案

This is a very straightforward implementation where the child builder classes override the chaining methods of all the base builder classes in the hierarchy. Hence, there is no compilation error during method chaining to set the attribute values.

这是一种非常直接的实现方式,子构建器类覆盖了层次结构中所有基构建器类的连锁方法。因此,在设置属性值的方法链过程中不会出现编译错误。

Let’s understand this by first taking a look at the Vehicle class:

让我们先来了解一下 Vehicle 类:

public class Vehicle {

    private String fuelType;
    private String colour;

    // Standard Getter methods..
    public Vehicle(VehicleBuilder builder) {
        this.colour = builder.colour;
        this.fuelType = builder.fuelType;
    }

    public static class VehicleBuilder {

        protected String fuelType;
        protected String colour;

        public VehicleBuilder fuelType(String fuelType) {
            this.fuelType = fuelType;
            return this;
        }

        public VehicleBuilder colour(String colour) {
            this.colour = colour;
            return this;
        }

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

The Vehicle class has two attributes fuelType and colour. It also has an inner class VehicleBuilder with methods with a name similar to the attributes in the Vehicle class. They return the builder class so that it can support method chaining.

Vehicle 类有两个属性 fuelTypecolour 。它还有一个内层类 VehicleBuilder,其中的方法名称与 Vehicle 类中的属性相似。它们返回构建器类,使其支持方法链。

Now, let’s take a look at the Car class:

现在,让我们来看看 Car 类:

public class Car extends Vehicle {

    private String make;
    private String model;

    // Standard Getter methods..

    public Car(CarBuilder builder) {
        super(builder);
        this.make = builder.make;
        this.model = builder.model;
    }

    public static class CarBuilder extends VehicleBuilder {

        protected String make;
        protected String model;

        @Override
        public CarBuilder colour(String colour) {
            this.colour = colour;
            return this;
        }

        @Override
        public CarBuilder fuelType(String fuelType) {
            this.fuelType = fuelType;
            return this;
        }

        public CarBuilder make(String make) {
            this.make = make;
            return this;
        }

        public CarBuilder model(String model) {
            this.model = model;
            return this;
        }

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

The class Car has inherited from Vehicle and similarly, the CarBuilder class has inherited from VehicleBuilder. Additionally, the CarBuilder class had to override the methods colour() and fuelType().

Car 类继承自 Vehicle 类,同样,CarBuilder 类也继承自 VehicleBuilder 类。此外,CarBuilder 类必须覆盖 colour()fuelType() 方法。

Let’s build a Car object now:

现在让我们构建一个 Car 对象:

@Test
void givenNoGenericImpl_whenBuild_thenReturnObject() {
    Car car = new Car.CarBuilder().colour("red")
      .fuelType("Petrol")
      .make("Ford")
      .model("F")
      .build();
    assertEquals("red", car.getColour());
    assertEquals("Ford", car.getMake());
}

We can set the car’s attributes in any order before calling the build() method.

在调用 build() 方法之前,我们可以按任意顺序设置汽车的属性。

However, for a child class of Car such as ElectricCar, we must override all the methods of CarBuilder and VehicleBuilder in ElectricCarBuilder. Hence, it’s not a very efficient implementation.

但是,对于 Car 的子类(如 ElectricCar ),我们必须在 ElectricCarBuilder 中覆盖 CarBuilderVehicleBuilder 的所有方法。因此,这不是一个非常有效的实现

4. Solution With Generics

4.使用非专利药的解决方案

Generics can help overcome the challenges faced in the implementation discussed earlier.

仿制药可以帮助克服前面讨论过的实施过程中面临的挑战。

For this let’s modify the inner Builder class in the Vehicle class:

为此,让我们修改 Vehicle 类中的内部 Builder 类:

public class Vehicle {

    private String colour;
    private String fuelType;

    public Vehicle(Builder builder) {
        this.colour = builder.colour;
        this.fuelType = builder.fuelType;
    }

    //Standard getter methods..
    public static class Builder<T extends Builder> {

        protected String colour;
        protected String fuelType;

        T self() {
            return (T) this;
        }

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

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

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

Noticeably, the methods fuelType() and colour() in the inner Builder class are returning a generic type. This kind of implementation facilitates fluent style coding or method chaining. This is a design pattern well known by the name Curiously Recurring Template Pattern(CRTP).

值得注意的是,内部 Builder 类中的 fuelType()colour() 方法返回的是通用类型。这种实现有助于流畅的编码风格或方法链。这是一种众所周知的设计模式,其名称为Curiously Recurring Template Pattern(CRTP)

Let’s now implement the class Car:

现在让我们来实现 Car 类:

public class Car extends Vehicle {

    private String make;
    private String model;

    //Standard Getters..
    public Car(Builder builder) {
        super(builder);
        this.make = builder.make;
        this.model = builder.model;
    }

    public static class Builder<T extends Builder<T>> extends Vehicle.Builder<T> {

        protected String make;
        protected String model;

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

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

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

We’ve applied CRTP in the signature of the inner Builder class and made the methods in the inner class return a generic type to support method chaining.

我们在内部 Builder 类的签名中应用了 CRTP,并使内部类中的方法返回一个通用类型,以支持方法链。

Similarly, let’s implement the child class ElectricCar of Car:

同样,让我们实现 Car 的子类 ElectricCar

public class ElectricCar extends Car {
    private String batteryType;

    public String getBatteryType() {
        return batteryType;
    }

    public ElectricCar(Builder builder) {
        super(builder);
        this.batteryType = builder.batteryType;
    }

    public static class Builder<T extends Builder<T>> extends Car.Builder<T> {
        protected String batteryType;

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

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

The implementation remains almost the same except for the inner Builder class extending its parent Builder.Car<T>. The same technique must be applied to the subsequent child classes of ElectricCar and so on.

除了内部 Builder 类扩展了其父类 Builder.Car<T> 之外,其他实现几乎相同。同样的技术必须应用于 ElectricCar 的后续子类,依此类推。

Let’s see the implementation in action:

让我们来看看具体的实施过程:

@Test
void givenGenericImpl_whenBuild_thenReturnObject() {
    Car.Builder<?> carBuilder = new Car.Builder();
    Car car = carBuilder.colour("red")
      .fuelType("Petrol")
      .make("Ford")
      .model("F")
      .build();

    ElectricCar.Builder<?> ElectricCarBuilder = new ElectricCar.Builder();
    ElectricCar eCar = ElectricCarBuilder.make("Mercedes")
      .colour("White")
      .model("G")
      .fuelType("Electric")
      .batteryType("Lithium")
      .build();

    assertEquals("red", car.getColour());
    assertEquals("Ford", car.getMake());

    assertEquals("Electric", eCar.getFuelType());
    assertEquals("Lithium", eCar.getBatteryType());
}

The method successfully builds the objects of type Car and ElectricCar.

该方法成功创建了 CarElectricCar 类型的对象。

Interestingly, we’ve used the raw generic type ? for declaring the inner classes Car.Builder<?> and ElectricCar.Builder<?>. This is because we need to ensure that method calls such as carBuilder.colour() and carBuilder.fuelType() return Car.Builder instead of its parent Vehicle.Builder.

有趣的是,我们在声明内部类 Car.Builder<?>ElectricCar.Builder<?> 时使用了原始 通用类型 ?。这是因为我们需要确保方法调用(如 carBuilder.color()carBuilder.fuelType() )返回 Car.Builder 而不是其父类 Vehicle.Builder

Similarly, the method calls ElectricCarBuilder.make() and ElectricCarBuilder.model() should return ElectricCarBuilder and not CarBuilder class. Without this method chaining won’t be possible.

同样,方法调用 ElectricCarBuilder.make()ElectricCarBuilder.model() 应返回 ElectricCarBuilder 而不是 CarBuilder 类。没有这个方法链将无法实现。

5. Conclusion

5.结论

In this article, we discussed the challenges in the Builder Design Pattern while handling inheritance. Java Generics and Curiously Recurring Template Pattern helped us implement a solution. With this, we can use method chaining without worrying about the order of method calls to set the attribute values in the builder class.

在本文中,我们讨论了构建器设计模式在处理继承时遇到的挑战。Java 泛型和奇妙重复模板模式帮助我们实现了一种解决方案。有了它,我们就可以使用方法链,而不必担心在构建器类中设置属性值的方法调用顺序。

As usual, the code used can be found over on GitHub.

像往常一样,使用的代码可以在 GitHub 上找到