这是一个关于将step builder pattern与enhanced或wizard构建器模式合并到creational DSL的人机界面问题。它使用流畅的界面,虽然它使用方法链接,而不是级联。也就是说,这些方法返回不同的类型。
我正在面对一个怪物类,它有两个构造函数,它们混合了整数,字符串和一系列字符串。每个构造函数都是10个参数。它还有大约40个可选的制定者;如果一起使用,其中一些相互冲突。它的构造代码看起来像这样:
Person person = Person("Homer","Jay", "Simpson","Homie", null, "black", "brown",
new Date(1), 3, "Homer Thompson", "Pie Man", "Max Power", "El Homo",
"Thad Supersperm", "Bald Mommy", "Rock Strongo", "Lance Uppercut", "Mr. Plow");
person.setClothing("Pants!!");
person.setFavoriteBeer("Duff");
person.setJobTitle("Safety Inspector");
这最终失败了,因为事实证明设置了最喜欢的啤酒和职称是不兼容的。叹息。
重新设计怪物类不是一种选择。它被广泛使用。有用。我只是不想再看它直接构建了。我想写一些干净的东西来喂它。在不让开发人员记住它们的情况下遵循其规则的东西。
与我一直在研究的美妙的建造者模式相反,这个东西没有口味或类别。它在需要时始终需要一些字段和其他字段,有些仅取决于之前设置的字段。施工人员不是伸缩式的。它们提供了两种使类进入相同状态的替代方法。它们漫长而丑陋。他们想要给他们的东西各不相同。
一个流畅的构建器肯定会让长构造器更容易看到。然而,大量可选的设置器使所需的设置器变得混乱。并且要求级联的流利构建器不满足:编译时执行。
构造函数强制开发人员显式添加必需的字段,即使将其归零也是如此。使用级联流畅的构建器时,这会丢失。它与安装者一样失去了同样的方式。我想要一种方法来阻止开发人员构建,直到添加了每个必填字段。
与许多构建器模式不同,我所追求的不是不变性。我发现它,我正在离开课堂。我想通过查看构建它的代码来了解构造的对象是否处于良好状态。无需参考文档。这意味着它需要通过有条件的必要步骤来接受程序员。
Person makeHomer(PersonBuilder personBuilder){ //Injection avoids hardcoding implementation
return personBuilder
// -- These have good default values, may be skipped, and don't conflict -- //
.doOptional()
.addClothing("Pants!!") //Could also call addTattoo() and 36 others
// -- All fields that always must be set. @NotNull might be handy. -- //
.doRequired() //Forced to call the following in order
.addFirstName("Homer")
.addMiddleName("Jay")
.addLastName("Simpson")
.addNickName("Homie")
.addMaidenName(null) //Forced to explicitly set null, a good thing
.addEyeColor("black")
.addHairColor("brown")
.addDateOfBirth(new Date(1))
.addAliases(
"Homer Thompson",
"Pie Man",
"Max Power",
"El Homo",
"Thad Supersperm",
"Bald Mommy",
"Rock Strongo",
"Lance Uppercut",
"Mr. Plow")
// -- Controls alternatives for setters and the choice of constructors -- //
.doAlternatives() //Either x or y. a, b, or c. etc.
.addBeersToday(3) //Now can't call addHowDrunk("Hammered");
.addFavoriteBeer("Duff")//Now can’t call addJobTitle("Safety Inspector");
.doBuild() //Not available until now
;
}
可以在addBeersToday()之后构建Person,因为此时所有构造函数信息都已知,但在doBuild()之前不会返回。
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, int beersToday,
String[] aliases);
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, String howDrunk,
String[] aliases);
这些参数设置的字段绝不能保留默认值。啤酒今天和howDrunk以不同的方式设置相同的字段。 favoriteBeer和jobTitle是不同的字段,但会导致与类的使用方式发生冲突,因此只应设置一个。它们由setter而非构造函数处理。
doBuild()
方法返回Person
个对象。它是唯一一个,而Person
是它将返回的唯一类型。当Person
完全初始化时。
在界面的每一步中,返回的类型并不总是相同。更改类型是指通过步骤引导开发人员的方式。它只提供有效的方法。在完成所有必需步骤之前,doBuild()
方法不可用。
执行/添加前缀是一种使写入更容易的因素,因为更改返回类型 与作业不匹配,使得知识产权建议成为按字母顺序排列的 在日食。我确认intellij没有这个问题。谢谢NimChimpsky。
这个问题与界面有关,因此我接受不提供实施的答案。但如果你知道一个,请分享。
如果您建议使用其他模式,请显示正在使用的界面。使用示例中的所有输入。
如果您建议使用此处提供的界面或稍微有些变化,请将其保护免受this等批评。
我真正想知道的是,如果大多数人更愿意使用此界面来构建或其他。这是人机界面问题。这会违反PoLA吗?不要担心实施起来有多难。
但是,如果您对实施感到好奇:
A failed attempt(没有足够的状态或理解有效而非违约)
A step builder implementation(对多个构造函数或替代方案不够灵活)
An enhanced builder(仍有班轮但有灵活状态)
Wizard builder(处理分叉但不记住选择构造函数的路径)
要求:
- 怪物(人)类已经关闭修改和扩展;没有敏感
目标:
- 隐藏长构造函数,因为怪物类有10个必需参数
- 根据使用的替代方法确定要调用的构造函数
- 禁止有冲突的setters
- 在编译时强制执行规则
意图:
- 当默认值不可接受时清楚地发出信号
答案 0 :(得分:3)
一个静态的内部构建者,在有效的java中以josh bloch着称。
必需参数是构造函数args,可选参数是方法。
一个例子。只需要用户名的调用:
RegisterUserDto myDto = RegisterUserDto.Builder(myUsername).password(mypassword).email(myemail).Build();
底层代码(省略明显的实例变量):
private RegisterUserDTO(final Builder builder) {
super();
this.username = builder.username;
this.firstName = builder.firstName;
this.surname = builder.surname;
this.password = builder.password;
this.confirmPassword = builder.confirmPassword;
}
public static class Builder {
private final String username;
private String firstName;
private String surname;
private String password;
private String confirmPassword;
public Builder(final String username) {
super();
this.username = username;
}
public Builder firstname(final String firstName) {
this.firstName = firstName;
return this;
}
public Builder surname(final String surname) {
this.surname = surname;
return this;
}
public Builder password(final String password) {
this.password = password;
return this;
}
public Builder confirmPassword(final String confirmPassword) {
this.confirmPassword = confirmPassword;
return this;
}
public RegisterUserDTO build() {
return new RegisterUserDTO(this);
}
}
答案 1 :(得分:1)
因此静态内部构建器与工厂函数结合可以完成您想要的一些操作。 (1)如果设置了A,它也可以强制执行类型的依赖关系。 (2)它可以返回不同的类。 (3)它可以对条目进行逻辑检查。
但是,如果程序员输入了错误的字段,它仍然会失败。
一个可能的优势是"多个建设者"图案。如果客户提前了解他为什么要构建特定元素的目的,那么他可以获得不同的构建器。您可以为每个组合制作一个构建器。
根据类中逻辑依赖项的类型,您可以将这些多个构建器与一个常规构建器组合在一起。例如,你可以拥有一个常规构建器,当你在常规构建器上调用setOption(A)时,它会返回一个不同类的构建器,你只能将那些继续相关的方法链接起来。这样你就会流利,但是你可以排除一些路径。当你这样做时,你必须小心取消已设置但已变得无关紧要的字段 - 你不能使构建者彼此的子类。
这可以强制客户端在编译时选择如何构造对象,那是你所追求的吗?
更新 - 试图回答评论:
首先,第一件事 - 工厂函数是Joshua Blocks Effective Java中的第一项,它只是意味着对于一个对象,你将构造函数设为私有,而是创建一个静态工厂函数。这比构造函数更灵活,因为它可以返回不同的对象类型。当您将工厂功能与多个构建器结合使用时,您确实可以获得非常强大的组合。以下是模式说明:http://en.wikipedia.org/wiki/Factory_method_pattern http://en.wikipedia.org/wiki/Abstract_factory_pattern
所以想象一下,你想要创建描述一个人及其工作的对象,但是当你指定他们的工作时,你想要一个特定的子选项列表。
public class outer{
Map <Map<Class<?>, Object> jobsMap - holds a class object of the new builder class, and a prototypical isntance which can be cloned.
outer(){
jobsMap.add(Mechanic.class, new Mechanic());
//add other classes to the map
}
public class GeneralBuilder{
String name;
int age;
//constructor enforces mandatory parameters.
GeneralBuilder(String name, int age, \\other mandatory paramerters){
//set params
}
public T setJob(Class<T extends AbstractJob> job){
AbstractJob result = super.jobsMap.getValue(job).clone();
//set General Builder parameters name, age, etc
return (T) result;
}
}
public MechanicBuilder extends AbstractJobs{
//specific properties
MechanicBuilder(GeneralBuilder){
// set age, name, other General Builder properties
}
//setters for specific properties return this
public Person build(){
//check mechanic mandatory parameters filled, else throw exception.
return Person.factory(this);
}
}
public abstract class AbstractJob{
String name;
String age;
//setters;
}
public class Person {
//a big class with lots of options
//private constructor
public static Person Factory(Mechanic mechanic){
//set relevant person options
}
}
所以现在这很流利。我创建了一个外部实例,并用所有特定的作业类型填充地图。然后,我可以创建尽可能多的新构建器作为内部类的实例。我为调用.setJobs(Mechanic.class)的常规构建器设置了参数,它返回了一个具有一堆特定属性的mechanic实例,我现在可以使用.setOptionA()等进行流畅调用。最后我调用build,这将调用Person类中的静态工厂方法并传递自身。你找回了一个人。
它有很多实现,因为你必须为每个&#34;类型&#34;创建一个特定的构建器类。可能由Person类表示的对象,但它确实构成了一个客户端非常容易使用的API。实际上,虽然这些课程有很多选择,但实际上可能只有少数人打算创作,其余的只是偶然出现。
答案 2 :(得分:1)
我建议创建一个引入不同参数对象的新构造函数,而不是构建器模式。然后,您可以从该新构造函数中委托原始构造函数。同时将原始构造函数标记为已弃用并指向新构造函数。
使用参数对象重构到构造函数也可以通过IDE支持完成,因此工作量不大。这样您也可以重构现有代码。如果仍然需要参数对象和相关类,你仍然可以创建构建器。
您需要关注的问题是不同的参数相互依赖。这种依赖关系应该反映在他们自己的对象中。
链式构建器的问题在于,您需要太多的类,并且您无法更改要使用它们的顺序,即使该顺序仍然正确。
答案 3 :(得分:0)
所以我有几年的时间来考虑这一点,我想我现在知道这样做的正确方法。
首先:将每个强制设置实现为单个方法接口,返回下一个单一方法接口。这会强制客户端填写所有必需的参数,并且还有一个额外的好处,即它们必须在代码中的所有位置以相同的顺序填写,以便更容易发现错误。
其次:在一个接口中实现所有独立的可选参数,这是最终参数的返回类型。
第三:对于任何复杂的可选参数子组,制作更多接口,强制选择路由。
interface FirstName {
public LastName setFirstName(String name)
}
interface LastName {
public OptionalInterface setLastName(String name)
}
interface OptionalParams {
public OptionalParams setOptionalParam(String numberOfBananas)
public OptionalParams setOptionalParam(int numberOfApples)
public AlcoholLevel setAlcoholLevel() // go down to a new interface
public MaritalStatus setMaritalStatus()
public Person build()
}
interface AlcoholLevel {
//mutually exclusive options hoist you back to options
public OptionalParams setNumberOfBeers(int bottles)
public OptionalParams setBottlesOfWine(int wine)
public OptionalParams setShots(int shots)
public OptionalParams isHammered()
public OptionalParams isTeeTotal()
public OptionalParams isTipsy()
}
interface MaritalStatus {
public OptionalParams isUnmarried()
//force you into a required params loop
public HusbandFirstName hasHusband()
public WifeFirstName hasWife()
}
通过一系列方法接口,您可以在很大程度上强制客户端上的良好行为。该模式适用于例如在需要某些认证的网络中形成格式良好的HTTP请求。标准httml库顶部的接口覆盖使客户端朝着正确的方向发展。
某些条件逻辑基本上太难以值得。坚持参数1和参数2的总和小于参数3的事情最好通过在构建方法上抛出运行时异常来处理。