Шаблон построителя и ковариантные возвращаемые типы в Java

Java Шаблоны проектирования

Чтобы прочитать эту статью, потребуется от пяти до десяти минут.

Шаблон Builder — это творческий шаблон проектирования, который решает проблему создания объектов.

В таких языках, как Java и C++, если класс имеет необязательные параметры при его создании, его можно реализовать путем перегрузки функций, но если необязательных параметров слишком много, количество конструкторов также станет очень большим, и это может быть невозможно перегрузить, потому что разные необязательные параметры имеют один и тот же тип.Мы проиллюстрируем это следующим примером.

1. Проблемы, вызванные необязательными параметрами

неперезаряжаемый кейс

//学号、姓名是必须参数,身高、体重可选
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, в этом классе хранится такая информация, как имя, возраст, пол и т. д., где имя является необходимой информацией, а возраст и пол — дополнительной информацией, тогда вы можете написать следующий конструктор:

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и на ЯвеthisАналогично обратитесь к текущему объекту. Мы пишем обязательные параметры в начале, а необязательные параметры — в конце (присваивая параметру значение по умолчанию, чтобы указать, что параметр является необязательным).

Когда мы создаем объект Person, мы можем записать его следующими способами:

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

Python уже имеет очень элегантное решение на уровне языка, в то время как на уровне языка Java есть только метод перегрузки.В приведенном выше анализе мы увидели недостатки этого метода.

Почему бы не использовать метод set?

Как вы могли бы сказать, нам просто нужен конструктор, который запрашивает необходимые параметры.

Для остальных необязательных параметров мы предоставляем соответствующий метод set, чтобы пользователь мог выборочно установить его после создания объекта?

Действительно, в некоторых случаях это может быть достигнуто.

Однако этот метод эквивалентен разбиению процесса создания объекта на множество этапов.В процессе создания объект подвергается воздействию внешнего мира, но еще не создан, что приводит к прерывистому состоянию. Окружающая среда Существует риск ниже.

Кроме того, много раз нам нужно создавать неизменяемые объекты (неизменяемые объекты), потому что этот метод позволяет изменять свойства объектов в любое время, поэтому нет никакого способа гарантировать неизменность объектов.

Для решения этой проблемы существует шаблон Builder.

2. Используйте режим строителя

Мы можем вынести процесс настройки необязательных параметров объекта отдельно и передать его для завершения 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(String 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. После настройки вызовите метод сборки для создания объекта Person. Таким образом, можно получить как удобство метода set, так и неизменяемый объект Person.

3. Проблема возвращаемого типа наследования Builder

Мы обнаружили, что шаблон Builder является хорошим решением проблемы создания объектов с необязательными параметрами, которые могут не только обеспечить непрерывное создание объекта, но и гарантировать невозможность изменения созданного объекта.

Давайте продолжим предположить другой сценарий и спроектируем класс Customer для системы доставки еды.Поскольку у нас уже есть класс Person, мы можем напрямую наследовать этот класс и расширять его.

У Покупателя в системе на вынос есть три обязательные данные: имя, номер мобильного телефона и адрес.

Дополнительная информация: псевдоним, личное представление.

Таким образом, класс 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, поэтому также необходимо решить проблему возвращаемого типа метода после наследования.

Есть два решения.Прежде чем представить его, давайте разберемся, что такое ковариантный возвращаемый тип.

ковариантный возвращаемый тип

Ковариантный возвращаемый тип означает, что при наследовании класса возвращаемый тип метода в классе становится типом, соответствующим подклассу.Этот измененный возвращаемый тип называется ковариантным возвращаемым типом.

на ЯвеObject.clone()Возьмите метод в качестве примера, тип, возвращаемый методом в классе Object,Objectтип. Мы знаем, что все классы наследуются от класса Object, поэтому, когда мы определяем класс, мы можем переопределить его.clone()метод:

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

Мы изменили возвращаемый тип на тип текущего класса, а не родительского классаObjectТип, это ковариантный возвращаемый тип, и возвращаемый тип становится типом, соответствующим подклассу.

Ковариантный возвращаемый тип не ограничивается тем же типом, что и сам класс, до тех пор, пока существует соответствующая связь, он также может рассматриваться как ковариантный возвращаемый тип.

Вот пример из StackOverflow:

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

определить наследование отAnimalизDogсвоего рода:

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

Мы видим, что после того, как класс Dog наследует класс Animal, соответствующий метод поиска еды, возвращаемое значение также устанавливаетсяFoodстановится его подклассомDogFood,здесьDogFoodявляется ковариантным возвращаемым типом.

Переопределение + приведение

Мы знаем, что сигнатура метода включает только имя метода, имя параметра, порядок параметров и не включает тип возвращаемого значения.

Таким образом, мы можем переопределить метод родительского класса Builder, сделать сигнатуру метода такой же, но изменить тип возвращаемого значения на подкласс Customer.Builder и привести возвращаемое значение метода родительского класса к Customer.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 для имитации собственного типа подкласса.

Другими словами, возвращаемый тип, который мы хотим указать при определении родительского класса 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>.

Рекурсивный параметр представляет типT1даBuilder<T2>Подкласс , так какTможет представлять любой тип, поэтомуT2может представлятьT extends Builder<T>, так вотBuilder<T2>Эквивалент текущего универсального классаBuilder<T extends Builder<T>>,такT1Он может представлять подкласс текущего универсального класса Person.Builder.

Как только дженерики определены, мы можем вернуть T в качестве возвращаемого значения в методе Person.Builder:

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。
    }
}

4. Резюме

Если при создании объекта в Java есть необязательные параметры, перегрузку можно использовать для реализации методов построения с разными параметрами, но этот метод может не завершить перегрузку, если типы необязательных параметров совпадают. привести к резкому увеличению методов строительства.

Предоставляя метод set для необязательных параметров, пользователь может вручную установить интересующие параметры после создания объекта, но этот метод приведет к тому, что фактический процесс создания объекта будет разбросан на множество шагов в прерывистом состоянии, если он находится в параллельной среде, могут возникнуть проблемы. Кроме того, таким образом невозможно создать неизменяемые объекты, и во многих случаях нам нужны неизменяемые объекты.

Итак, мы используем шаблон Builder для создания объектов, предоставляем параметры для установки в Builder, а затем вызываемbuild()Метод получает целевой объект, что удобно для установки необязательных параметров и получает неизменяемый объект.

После этого мы обсуждали в статье проблему, что при наследовании Builder возвращается тип родительского класса. Поскольку в паттерне Builder мы используем цепные вызовы, чтобы упростить процесс установки параметров, мы должны вернуть тип подкласса.

После того, как подкласс наследует родительский класс, тип возвращаемого значения исходного метода изменяется на тип, соответствующий подклассу, который называется ковариантным возвращаемым типом.

Есть два способа вернуть тип подкласса: первый — переопределить все методы настройки параметров родительского класса при реализации подкласса, изменить возвращаемое значение на тип подкласса и принудительно преобразовать возвращаемое значение в тип подкласса.

Другой — имитировать собственный тип подкласса с помощью дженериков с параметрами рекурсивного типа. То есть мы объявляем родительский класс Builder как универсальный класс, а затем используем общий параметр для возвращаемого типа метода.Tвместо этого и вернетthisпривести к типуT. При реализации подкласса передайте тип подкласса в общий список параметров родительского класса, чтобы метод установки параметров в родительском классе автоматически возвращал тип подкласса. Мы также можем отлитьthisзаTТиповые операции извлекаются отдельно дляself()Таким образом, определение абстрактных классов может поддерживаться, а подклассы должны только переопределятьself()метод для возврата соответствующего экземпляра подкласса.

Статьи по Теме

как