这个怪物生成器是一个很好的生成器/工厂模式,用于抽象与设置器混合的长构造函数吗?

2022-09-02 10:56:15

这是一个关于将步骤生成器模式增强型或向导生成器模式组合到创建型 DSL 中的人机界面问题。它使用流畅的类似接口,尽管它使用方法链接,而不是级联。也就是说,这些方法返回不同的类型。

我面对的是一个怪物类,它有两个构造函数,它们混合了int,Strings和String数组。每个构造函数的长度为 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
    ;
}   

Person可以在addBeersToday()之后构建,因为此时所有构造函数信息都是已知的,但在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);

这些参数设置的字段绝不能保留默认值。啤酒今天和Drunk如何以不同的方式设置同一领域。favoriteBeer 和 jobTitle 是不同的字段,但会导致与类的使用方式冲突,因此只应设置一个字段。它们由 setter 而不是构造函数处理。

该方法返回一个对象。它是唯一一个这样做的类型,也是它将返回的唯一类型。当它完全初始化时。doBuild()PersonPersonPerson

在接口的每个步骤中,返回的类型并不总是相同的。更改类型是指导开发人员完成这些步骤的方式。它只提供有效的方法。在完成所有必需的步骤之前,该方法不可用。doBuild()

do/add 前缀是使写作更容易的笨拙,因为更改的返回类型与赋值不匹配,并使智能化建议在 eclipse 中变为按字母顺序排列。我已经确认intellij没有这个问题。谢谢NimChimpsky。

这个问题是关于接口的,所以我将接受不提供实现的答案。但是,如果您知道一个,请分享。

如果您建议使用替代模式,请显示正在使用的界面。使用示例中的所有输入。

如果您建议使用此处提供的界面,或者一些轻微的变化,请为它辩护,免受这样的批评。

我真正想知道的是,大多数人是否更喜欢使用此界面进行构建或其他一些界面。这是人机界面问题。这是否违反了 PoLA?不要担心实施起来有多难。

但是,如果您对实现感到好奇:

失败的尝试(没有足够的状态或了解有效与未默认)

步进生成器实现(对于多个构造函数或替代函数来说不够灵活)

增强的构建器(仍然是衬垫,但具有灵活的状态)

向导生成器(处理分叉但不记得选择构造函数的路径)

要求:

  • 怪物(人)类已经关闭修改和扩展;不敏感

目标:

  • 隐藏长构造函数,因为怪物类有 10 个必需参数
  • 根据使用的备选方案确定要调用的构造函数
  • 禁止冲突的 setter
  • 在编译时强制实施规则

意图:

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

答案 1

一个静态的内部构建器,由josh bloch在有效的java中成名。

必需参数是构造函数参数,可选参数是方法。

举个例子。只需要用户名的调用:

RegisterUserDto myDto = RegisterUserDto.Builder(myUsername).password(mypassword).email(myemail).Build();

和底层代码(省略明显的实例 vars):

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);
        }
    }

答案 2

我建议创建一个新的构造函数来引入不同的参数对象,而不是生成器模式,这些构造函数对不同的参数进行分组。然后,您可以从该新构造函数中委托给原始构造函数。还要将原始构造函数标记为已弃用,并指向新构造函数。

使用参数对象重构构造函数也可以使用 IDE 支持完成,因此工作量不大。这样,您还可以重构现有代码。如果仍然需要参数对象和有问题的类,您仍然可以创建构建器。

您需要关注的问题是,不同的参数相互依赖。这种依赖关系应该反映在它们自己的对象中。

链接构建器的问题在于,您需要太多的类,并且您无法更改要使用它们的顺序,即使该顺序仍然正确。


推荐