Java: Як реалізувати побудову кроків, для яких порядок встановлення не має значення?


10

Редагувати: Я хотів би зазначити, що це питання описує теоретичну проблему, і я знаю, що я можу використовувати аргументи конструктора для обов’язкових параметрів або викинути виняток з виконання, якщо API використовується неправильно. Однак я шукаю рішення, яке не вимагає конструкторських аргументів або перевірки часу виконання.

Уявіть, у вас такий Carінтерфейс:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

Як сказано в коментарі хотіли б запропонувати, Carповинна мати Engineі Transmissionале НЕ Stereoє обов'язковим. Це означає, що Builder, який може build()мати Carпримірник, повинен коли-небудь мати build()метод лише тоді, коли Engineа та Transmissionобидва вже були надані екземпляру будівельника. Таким чином перевірка типу відмовиться компілювати будь-який код, який намагається створити Carекземпляр без Engineабо Transmission.

Це вимагає побудови кроків . Зазвичай ви реалізуєте щось подібне:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Там в ланцюзі різних класів будівельника ( Builder, BuilderWithEngine, CompleteBuilder), що додати один потрібно сетер методу після того, як інший, з останнім класом , що містить всі додаткові методами сетера , як добре.
Це означає, що користувачі цього розробника кроків обмежуються порядком, у якому автор надав обов'язкові сетери . Ось приклад можливих застосувань (зауважте, що всі вони суворо впорядковані: engine(e)спочатку, після них transmission(t), і нарешті, необов’язкові stereo(s))

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

Однак існує маса сценаріїв, в яких це не ідеально для користувача будівельника, особливо якщо в програмі не тільки сетери, але й добавники, або якщо користувач не може контролювати порядок, у якому певні властивості для будівельника стануть доступними.

Єдине рішення, про яке я можу придумати, є дуже суперечливим: для кожної комбінації обов'язкових властивостей, встановлених або ще не встановлених, я створив спеціальний клас будівельника, який знає, які потенційні інші обов'язкові сетери потрібно викликати перед тим, як прийти вкажіть, де build()метод повинен бути доступний, і кожен із цих сеттерів повертає більш повний тип конструктора, який знаходиться на крок ближче до вмісту build()методу.
Я додав код нижче, але ви можете сказати , що я використовую систему типів для створення ФШМ , що дозволяє створити Builder, який може бути перетворений в яких BuilderWithEngineабо BuilderWithTransmission, що і тоді , може бути перетворений в CompleteBuilder, який реалізуєbuild()метод. Необов’язкові налаштування можуть бути викликані в будь-якому з цих екземплярів програми. введіть тут опис зображення

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Як ви можете сказати, це не масштабується, оскільки кількість необхідних різних будівельних класів буде O (2 ^ n), де n - кількість обов'язкових установників.

Звідси моє запитання: чи можна це зробити більш елегантно?

(Я шукаю відповідь, яка працює з Java, хоча Scala також була б прийнятною)


1
Що заважає вам просто використовувати контейнер IoC, щоб протистояти всім цим залежностям? Крім того, мені не зрозуміло, чому, якщо замовлення не має значення, як ви стверджували, ви не можете просто використовувати звичайні методи встановлення, які повертаються this?
Роберт Харві

Що означає .engine(e)двічі викликати одного будівельника?
Ерік Ейдт

3
Якщо ви хочете перевірити це статично, не записуючи вручну клас для кожної комбінації, вам, ймовірно, доведеться використовувати елементи на рівні шиї, такі як макроси або метапрограмування шаблонів. Наскільки мені відомо, Java недостатньо виразна, і, мабуть, не варто цього докладати до динамічно перевірених рішень іншими мовами.
Карл Білефельдт

1
Роберт: Мета полягає в тому, щоб перевіряючий тип забезпечував факт того, що і двигун, і передача є обов'язковими; таким чином, ви навіть не можете телефонувати, build()якщо ви не дзвонили engine(e)і transmission(t)раніше.
derabbink

Ерік: Ви можете почати з типової Engineреалізації, а пізніше замінити її більш конкретною реалізацією. Але , швидше за все , це буде мати більше сенсу , якщо б engine(e)ні сетер, а акумулятор: addEngine(e). Це було б корисно для Carбудівельника, який може виробляти гібридні машини з більш ніж одним двигуном / мотором. Оскільки це надуманий приклад, я не вдавався в деталі щодо того, чому ви можете це зробити - для стислості.
derabbink

Відповіді:


3

Здається, у вас є дві різні вимоги, засновані на наданому вами методі викликів.

  1. Лише один (необхідний) двигун, лише одна (необхідна) трансмісія і лише одна (необов’язково) стерео.
  2. Один або більше (необхідних) двигунів, одна чи більше (необхідних) трансмісій і один або більше (необов’язково) стерео.

Я думаю, що тут перше питання полягає в тому, що ви не знаєте, що хочете робити в класі. Частина цього полягає в тому, що невідомо, як ви хочете виглядати побудований об’єкт.

Автомобіль може мати лише один двигун і одну коробку передач. Навіть у гібридних автомобілів є лише один двигун (можливо, а GasAndElectricEngine)

Я торкнуся обох реалізацій:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

і

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

Якщо потрібні двигун і трансмісія, то вони повинні бути в конструкторі.

Якщо ви не знаєте, який двигун або трансмісія потрібні, тоді ще не встановлюйте його; це знак того, що ви створюєте будівельника занадто далеко над стеком.


2

Чому б не використовувати нульовий шаблон об'єкта? Позбудьтеся цього конструктора, найелегантнішим кодом, який ви можете написати, є той, який вам насправді не потрібно писати.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

Це я відразу подумав. хоча у мене не було б трьох конструкторів параметрів, просто два конструктора параметрів, які мають обов'язкові елементи, а потім сеттер для стерео, як це є необов'язковим.
Encaitar

1
У такому простому (надуманому) прикладі Car, це мало б сенс, оскільки кількість аргументів c'tor дуже мало. Однак, як тільки ви маєте справу з чим-небудь помірно складним (> = 4 обов'язкові аргументи), вся справа стає важче обробляти / менш читатимою ("Двигун чи трансмісія прийшли першими?"). Ось чому ви використовуєте конструктор: API змушує вас бути більш чіткими щодо того, що ви створюєте.
derabbink

1
@derabbink Чому в цьому випадку не розбивати свій клас на більш дрібні? Використання будівельника просто приховає той факт, що клас робить занадто багато і став нездійсненним.
Знайдено

1
Кудо за припинення шаблону божевілля.
Роберт Харві

@Spotted деякі класи просто містять багато даних. Наприклад, якщо ви хочете створити клас журналу доступу, який містить всю пов'язану інформацію про HTTP-запит і виводить дані у формат CSV або JSON. Даних буде багато, і якщо ви хочете примусити деякі поля бути присутніми під час компіляції, вам знадобиться модель конструктора з дуже довгим конструктором списку аргументів, який не виглядає добре.
ssgao

1

По-перше, якщо у вас набагато більше часу, ніж у будь-якому магазині, в якому я працював, напевно, не варто допускати жодного порядку операцій або просто жити при тому, що ви можете вказати більше одного радіо. Зауважте, що ви говорите про код, а не про введення користувача, тому у вас можуть бути твердження, які під час тестування вашого пристрою будуть виходити з ладу, а не секунди раніше, ніж під час компілювання.

Однак, якщо ваше обмеження, як зазначено в коментарях, у тому, що вам потрібно мати двигун і трансмісію, то застосуйте це, додавши всі обов'язкові властивості конструктора конструктора.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

Якщо лише стерео не є обов'язковим, то виконати останній крок за допомогою підкласів будівельників можливо, але крім цього виграш від отримання помилки під час компіляції, а не при тестуванні, мабуть, не варто докладати зусиль.


0

кількість потрібних різних будівельних класів буде O (2 ^ n), де n - кількість обов'язкових задач.

Ви вже здогадалися в правильному напрямку цього питання.

Якщо ви хочете перевірити час компіляції, вам знадобляться (2^n)типи. Якщо ви хочете перевірити час виконання, вам знадобиться змінна, яка може зберігати (2^n)стани; n-бітовое ціле буде робити.


Оскільки C ++ підтримує параметр шаблону нетипового шаблону (наприклад, цілочисельні значення) , шаблон шаблону класу C ++ можна інстанціювати в O(2^n)різні типи, використовуючи схему, подібну до цієї .

Однак у мовах, які не підтримують параметри шаблону нетипових типів, ви не можете покластися на систему типів для інстанціфікації O(2^n)різних типів.


Наступна можливість - з анотаціями на Java (та атрибутами C #). Ці додаткові метадані можна використовувати для запуску визначеної користувачем поведінки під час компіляції, коли використовуються процесори анотацій . Однак для вас було б занадто багато роботи. Якщо ви використовуєте рамки, які надають вам цю функціональність, використовуйте її. В іншому випадку перевірте наступну можливість.


Нарешті, зауважте, що зберігати O(2^n)різні стани як змінну під час виконання (буквально як ціле число, яке має принаймні nбіт шириною) дуже просто. Ось чому всі найбільш обґрунтовані відповіді всі рекомендують виконати цю перевірку під час виконання, оскільки зусиль, необхідних для здійснення перевірки часу компіляції, занадто багато, порівняно з потенційним посиленням.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.