Изменить: я хотел бы отметить, что этот вопрос описывает теоретическую проблему, и я знаю, что я могу использовать аргументы конструктора для обязательных параметров, или вызвать исключение времени выполнения, если 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 также будет приемлемым)
источник
this
?.engine(e)
дважды вызывать одного строителя?build()
если вы не звонилиengine(e)
иtransmission(t)
раньше.Engine
реализации по умолчанию , а затем перезаписать ее более конкретной реализацией. Но, скорее всего, в этом было бы больше смысла, если быengine(e)
не сеттер, а сумматорaddEngine(e)
. Это было бы полезно дляCar
строителя, который может производить гибридные автомобили с более чем одним двигателем / двигателем. Поскольку это надуманный пример, я не стал вдаваться в подробности того, почему вы можете захотеть сделать это - для краткости.Ответы:
Похоже, у вас есть два разных требования, основанных на предоставленных вами вызовах методов.
Я думаю, что первая проблема здесь в том, что вы не знаете, что хотите, чтобы класс делал. Частично это означает, что неизвестно, как вы хотите, чтобы выглядел построенный объект.
Автомобиль может иметь только один двигатель и одну коробку передач. Даже гибридные автомобили имеют только один двигатель (возможно
GasAndElectricEngine
)Я рассмотрю обе реализации:
а также
Если требуется двигатель и трансмиссия, то они должны быть в конструкторе.
Если вы не знаете, какой двигатель или трансмиссия требуются, то пока не устанавливайте их; это признак того, что вы создаете конструктор слишком далеко вверх по стеку.
источник
Почему бы не использовать шаблон нулевого объекта? Избавьтесь от этого компоновщика, самый элегантный код, который вы можете написать, - это тот, который вам на самом деле не нужно писать.
источник
Car
, это имело бы смысл, так как число аргументов c'or очень мало. Однако, как только вы имеете дело с чем-то достаточно сложным (> = 4 обязательных аргумента), все становится более трудным для обработки / менее читаемым («Двигатель или трансмиссия были первыми?»). Вот почему вы должны использовать конструктор: API заставляет вас быть более ясным в отношении того, что вы создаете.Во-первых, если у вас не намного больше времени, чем в любом магазине, в котором я работал, вероятно, не стоит разрешать какой-либо порядок операций или просто жить с тем фактом, что вы можете указать более одного радио. Обратите внимание, что вы говорите о коде, а не о пользовательском вводе, поэтому вы можете иметь утверждения, которые не пройдут во время вашего модульного тестирования, а не за секунду до этого во время компиляции.
Однако, если ваше ограничение, как указано в комментариях, требует, чтобы у вас был движок и передача, то примените это, указав все обязательные свойства конструктора компоновщика.
Если только стерео является необязательным, то выполнение последнего шага с использованием подклассов компоновщиков возможно, но помимо этого выигрыш от получения ошибки во время компиляции, а не в тестировании, вероятно, не стоит этих усилий.
источник
Вы уже угадали правильное направление для этого вопроса.
Если вы хотите получить проверку во время компиляции, вам понадобятся
(2^n)
типы. Если вы хотите получить проверку во время выполнения, вам понадобится переменная, которая может хранить(2^n)
состояния;n
-битовое целое будет делать.Поскольку C ++ поддерживает нетипизированный параметр шаблона (например, целочисленные значения) , существует возможность создания шаблона класса C ++ для
O(2^n)
различных типов, используя схему, подобную этой .Однако в языках, которые не поддерживают нетипизированные параметры шаблона, вы не можете полагаться на систему типов для создания экземпляров
O(2^n)
различных типов.Следующая возможность - с аннотациями Java (и атрибутами C #). Эти дополнительные метаданные можно использовать для запуска пользовательского поведения во время компиляции, когда используются процессоры аннотаций . Тем не менее, было бы слишком много работы для их реализации. Если вы используете фреймворки, которые предоставляют вам эту функциональность, используйте ее. В противном случае проверьте следующую возможность.
Наконец, обратите внимание, что хранить
O(2^n)
различные состояния как переменную во время выполнения (буквально, как целое число, имеющееn
ширину по крайней мере в битах) очень легко. Вот почему все ответы с наибольшим количеством голосов рекомендуют выполнять эту проверку во время выполнения, поскольку усилия, необходимые для реализации проверки во время компиляции, слишком велики по сравнению с потенциальным выигрышем.источник