这个Monster Builder是一个很好的构建器/工厂模式,用于抽象混合了setter的长构造函数吗?

时间:2014-04-07 10:25:24

标签: java design-patterns dsl builder fluent

这是一个关于将step builder patternenhancedwizard构建器模式合并到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
  •   
  • 在编译时强制执行规则
  •   
     

意图:

     
      
  • 当默认值不可接受时清楚地发出信号
  •   

4 个答案:

答案 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的事情最好通过在构建方法上抛出运行时异常来处理。