Java 中的 Builder 模式和协变返回类型

阅读这篇文章大约需要十分钟时间。

Builder 模式是一种创建型的设计模式,即解决对象的创建问题。

在 Java、C++ 这类语言中,如果一个类在创建时存在可选参数,那么可以通过函数重载来实现,但是如果可选参数非常多的话,构造函数的数量也会变得非常多,并且可能因为不同可选参数类型相同而没法重载,我们接下来通过例子来说明。

一、可选参数带来的问题

不可重载的情况

//学号、姓名是必须参数,身高、体重可选
public Student(int id, String name) {}
public Student(int id, String name, float height, float weight) {}
public Student(int id, String name, float height) {} //只填身高
public Student(int id, String name, float weight) {} //只填体重(签名重复,无法重载)

虽然最后两个构造方法参数名不同,但是它们类型相同,方法签名也就相同,因此没办法重载,只能保留一个。

构造方法数量过多

接着考虑这么一个场景,你正在设计一个 Person 类,这个类存放了 name、age、sex 等信息,其中 name 是必要信息,而 age 和 sex 是可选信息,那么你可能会编写如下的构造方法:

public class Person {
    private String name;
    private int age;
    private String sex;

    public Person(String name) {}
    public Person(String name, int age) {}
    public Person(String name, String sex) {}
    public Person(String name, int age, int sex) {}
}

我们利用 Java 方法的重载,来实现参数的“可选”,但我们也因此不得不设计很多的构造方法,来应对不同的对象创建需求。而且,上面的例子中只有两个可选参数,当我们需要更多的可选参数时,这种实现方式几乎不可行。

在某些语言中,可以通过“命名可选参数”来解决这个问题,例如 Python 中可以这么实现:

class Person:
    def __init__(self, name, age = 0, sex = "unknown"):
        self.name = name
        self.age = age
        self.sex = sex

其中 self 和 Java 中的 this 类似,指代当前对象。我们将必须的参数写在前面,将可选参数写在后面(通过给参数赋默认值的方式来表示该参数是可选参数)。

当我们创建 Person 对象时,可以有以下几种写法:

Tom = Person("Tom", age=18)
John = Person("John", sex="male")
Lily = Person("Lily", age=20, sex="female")

Python 在语言层面已经有了很优雅的解决方法,而 Java 语言层面只有重载的方式,在上面的分析中,我们已经看到了这种方式的弊端。

为什么不用 set 方法?

可能你会说,我们只需要一个构造函数,要求提供必要的参数。

剩下的可选参数,我们提供对应的 set 方法,让使用者在创建对象后选择性地设置不就可以了吗?

确实,有些情况可以这么实现。

但是,这种方式相当于将对象的创建过程拆分成了很多步骤,对象在这个创建过程中暴露给了外界,却又尚未创建完毕,导致其处于一种不连续的状态,在多线程环境下存在风险。

此外,很多时候我们需要创建不可变的对象(immutable object),这种方法由于允许随时改变对象的属性,因此没办法保证对象的不可变。

为了解决这一问题,就有了 Builder 模式。

二、使用 Builder 模式

我们可以将对象可选参数的设置过程单独拿出来,交给 Builder 来完成,等参数设置好了之后,再根据这些参数创建 Person 对象,得到不可变的 Person 对象。

Person 类及嵌套的 Builder 类:

public class Person {
    private String name;
    private int age;
    private String sex;

    protected Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.sex = builder.sex;
    }

    public static class Builder {
        private String name;
        private int age;
        private String sex;

        public Builder(String name) {
            this.name = name;
        }

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

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

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

创建 Person 对象:

Person person = new Person.Builder("John")
        .age(20)
        .sex("male")
        .build();

我们通过 Builder 的链式调用来模拟“命名可选参数”,设置完成后,调用 build 方法创建一个 Person 对象,如此一来,既有了 set 方法的简便,又能得到不可变的 Person 对象。

三、Builder 继承时的返回类型问题

我们发现 Builder 模式很好地解决了带有可选参数的对象创建问题,既能保证对象创建时是连续的,又能保证创建的对象不可被改变。

我们继续假设另一个场景,给外卖系统设计一个 Customer 类,由于我们已经有了 Person 类,所以可以直接继承该类,进行扩展。

外卖系统中的 Customer 有三个信息是必须的:姓名、手机号、地址。

可选信息:昵称、个人介绍。

所以 Customer 类设计如下:

public class Customer extends Person {
    private long phone;
    private String address;
    private String alias;
    private String intro;

    private Customer(Builder builder) {
        super(builder);
        this.phone = builder.phone;
        this.alias = builder.alias;
        this.intro = builder.intro;
    }

    public static class Builder extends Person.Builder {
        private long phone;
        private String address;
        private String alias;
        private String intro;

        public Builder(String name, long phone, String address) {
            super(name);
            this.phone = phone;
            this.address = address;
        }

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

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

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

我们给 Customer 类增加了四个成员变量,也在 Customer.Builder 当中进行了相应的扩展,但是,当我们尝试调用参数设置方法时就会发现问题:

Customer customer = new Customer.Builder("Tom", 13999999999L, "北京市XXX")
        .age(20) //此处返回类型为 Person.Builder
        .alias("用户昵称")  //错误,不存在该方法
        .intro("用户自我介绍");

我们发现,这么继承父类 Builder 是有问题的。Java 不存在“自身类型”这个概念,也就是说,当一个子类继承了父类之后,原先父类中返回值为父类类型的方法,仍旧返回父类类型,并不会变成子类类型。

所以之前定义的方法返回类型仍旧是父类 Person.Builder,而不是当前的 Customer.Builder,因此还需要解决继承后,方法返回类型的问题。

解决方法有两种,在介绍之前,我们先理解什么是协变返回类型。

协变返回类型

协变返回类型(Covariant Return Type),指的是当一个类被继承之后,该类中方法的返回类型变成子类对应的类型,这个改变后的返回类型就叫协变返回类型。

以 Java 中的 Object.clone() 方法为例,该方法在 Object 类中返回的类型是 Object 类型。我们知道,所有类都继承自 Object 类,所以我们在定义类时可以覆写类中的 clone() 方法:

public class MyClass {
    @Override
    public MyClass clone() {
        //...
    }
}

我们将返回类型改为了当前类的类型,而不是父类中的 Object 类型,这就是协变返回类型,返回的类型变成了子类对应的类型。

协变返回类型并不局限于和类本身相同的类型,只要是存在对应关系,也可以认为是协变返回类型。

下面是 StackOverflow 上的一个例子:

public class Animal {
    protected Food seekFood() {
        return new Food();
    }
}

定义一个继承自 AnimalDog 类:

public class Dog extends Animal {
    @Override
    protected DogFood seekFood() {
        return new DogFood();
    }
}

我们看到,在 Dog 类继承 Animal 类之后,对应的寻找食物的方法,返回值也由 Food 变成了其子类 DogFood,这里的 DogFood 就是协变返回类型。

Override + 强制类型转换

我们知道,方法的签名只包括方法名、参数名、参数顺序,不包含返回类型。

所以我们可以覆写父类 Builder 的方法,让方法签名相同,但是返回类型改为子类 Customer.Builder,并将父类方法的返回值强制转换为 Customer.Builder,这样就能让所有方法都返回子类 Builder。

public class Customer extends Person {
    //...
    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);
        }
        //...
    }
    //...
}

通过覆写父类方法,并对返回值进行强制类型转换,现在 Customer.Builder 类已经可以正常使用了。

这种方式的缺点也很明显,你需要覆写父类 Builder 的所有方法,并对返回值进行强制类型转换,这无疑会使代码变得很冗长。

接下来,我们看看另一种解决方法。

使用泛型模拟子类的自身类型

我们可以利用 Java 中的泛型,来模拟子类的自身类型(self-type)。

也就是说,我们想要在定义父类 Builder 时所指定的返回类型,可以在该类被继承时,自动变成子类的自身类型。

不过,这里使用的泛型参数列表不是简单的 <T>,而是递归的 <T extends Builder<T>>

首先,将 Person.Builder 定义为泛型类:

public class Person {
    public static class Builder<T extends Builder<T>> {
        //...
    }
}

解释一下上述泛型中的递归类型参数,通常,简单的泛型只有一个类型参数 T,而这里的类型参数变成了递归的 T extends Builder<T>,为了方便解释,我们不妨写成 T1 extends Builder<T2>

该递归参数表示类型 T1Builder<T2> 的子类,由于 T 可以表示任意类型,所以 T2 可以表示 T extends Builder<T>,因此此处的 Builder<T2> 等价于当前泛型类 Builder<T extends Builder<T>>,所以 T1 就可以表示当前泛型类 Person.Builder 的子类。

定义完泛型之后,我们就可以在 Person.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;
        }
        //...
    }
}

定义子类 Customer.Builder 时,将当前 Builder 类型传入泛型参数中:

public class Customer {
    public static class Builder extends Person.Builder<Builder> {
        //...
    }
}

这样就能让父类中的类型参数 T 对应到当前子类 Customer.Builder,让方法返回当前的子类类型,也就不需要再覆写父类的设置参数方法了。

当然,由于最后的 build()方法要返回 Customer 类型,所以还需要覆写 build()

我们注意到,父类 Builder 在返回子类类型时,需要将当前的 this 强制转换成子类类型。

我们也可以编写一个 self() 方法,来获得子类类型的实例:

public class Person {
    public 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();
        }

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

这样就无需在每一个方法中进行类型转换了。

假设我们不会直接用到 Person 类,使用的都是它的子类,于是我们决定将 Person 声明为一个抽象类,那么可以将 self() 方法声明为抽象方法,让子类去实现它,返回对应的子类实例:

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(); //子类需要覆写该方法,返回对应的 this。
    }
}

四、总结

Java 中创建对象时如果存在可选参数,可以使用重载来实现不同参数的构造方法,但是这种方式会有可能在可选参数类型相同的情况下,无法完成重载,此外,在可选参数很多的时候,还会导致构造方法急剧增加的情况。

通过为可选参数提供 set 方法,可以让使用者在创建完对象后,手动设置感兴趣的参数,但这种方式会导致对象的实际创建过程被分散成很多步骤,处于一种不连续的状态,如果是在并发环境下,可能会出现问题。此外,这种方式没办法创建不可变对象,而很多情况下,我们希望得到的是不可变对象。

于是,我们使用 Builder 模式来创建对象,将要设置的参数先提供给 Builder,然后再调用 build() 方法获得一个目标对象,既方便设置可选参数,又能得到不可变对象。

之后,我们在文章中讨论了继承 Builder 时,返回的是父类类型的问题。因为在 Builder 模式中,我们是使用链式调用让设置参数的过程更简便,因此必须得返回子类的类型。

子类继承父类之后,将原先方法的返回类型变成该子类对应的类型,这个类型就叫做协变返回类型。

返回子类类型有两种方法,一种是在实现子类时,覆写父类的所有参数设置方法,将返回值改成子类类型,并强制将返回值转换成子类类型。

另一种是通过带递归类型参数的泛型,来模拟子类的自身类型。即我们将父类 Builder 声明为泛型类,然后将方法的返回类型用泛型参数 T 来代替,并将返回的 this 强制转换成类型 T。在实现子类时,将子类类型传入到父类的泛型参数列表中,这样父类中的参数设置方法就会自动返回子类类型。我们还可以将强制转换 thisT 类型的操作单独提取到 self() 方法中,通过这种方式,可以支持抽象类的定义,子类只需要覆写 self() 方法来返回对应的子类实例即可。

相关文章

Loading Likes...

发表评论

电子邮件地址不会被公开。 必填项已用*标注