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оба уже были переданы экземпляру Builder. Таким образом, средство проверки типов откажется компилировать любой код, который пытается создать Carэкземпляр без Engineили Transmission.

Это требует Step Builder . Обычно вы реализуете что-то вроде этого:

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()метода.
Я добавил приведенный ниже код, но вы можете сказать, что я использую систему типов для создания FSM, который позволяет вам создать a Builder, который можно превратить в a BuilderWithEngineили or BuilderWithTransmission, который затем можно превратить в a 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 также будет приемлемым)

derabbink
источник
1
Что мешает вам просто использовать контейнер IoC для поддержки всех этих зависимостей? Кроме того, мне не ясно, почему, если порядок не имеет значения, как вы утверждали, вы не могли бы просто использовать обычные методы установки, которые возвращают this?
Роберт Харви
Что значит .engine(e)дважды вызывать одного строителя?
Эрик Эйдт
3
Если вы хотите проверить это статически, не написав вручную класс для каждой комбинации, вам, вероятно, придется использовать такие вещи, как макросы или шаблонные мета-программы. Насколько мне известно, Java не достаточно выразительна для этого, и усилия, скорее всего, не стоили бы того, чтобы использовать динамически проверенные решения на других языках.
Карл Билефельдт
1
Роберт: Цель состоит в том, чтобы средство проверки типов приводило в действие тот факт, что и двигатель, и трансмиссия являются обязательными; таким образом, вы даже не можете позвонить, build()если вы не звонили engine(e)и transmission(t)раньше.
Дераббинк
Эрик: Возможно, вы захотите начать с 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;
    }
}

Если требуется двигатель и трансмиссия, то они должны быть в конструкторе.

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

Zymus
источник
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'or очень мало. Однако, как только вы имеете дело с чем-то достаточно сложным (> = 4 обязательных аргумента), все становится более трудным для обработки / менее читаемым («Двигатель или трансмиссия были первыми?»). Вот почему вы должны использовать конструктор: API заставляет вас быть более ясным в отношении того, что вы создаете.
derabbink
1
@derabbink Почему бы не разбить свой класс на меньшие в этом случае? Использование компоновщика просто скроет тот факт, что класс делает слишком много и стал неуправляемым.
Пятница,
1
Престижность за прекращение модели безумия.
Роберт Харви
@ Пятна некоторых классов просто содержат много данных. Например, если вы хотите создать класс журнала доступа, который содержит всю связанную информацию о 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ширину по крайней мере в битах) очень легко. Вот почему все ответы с наибольшим количеством голосов рекомендуют выполнять эту проверку во время выполнения, поскольку усилия, необходимые для реализации проверки во время компиляции, слишком велики по сравнению с потенциальным выигрышем.

rwong
источник