如何改进Bloch的Builder模式,使其更适合在高度可扩展的类中使用


34

Joshua Bloch的《 Effective Java》(第二版)对我的影响很大,可能比我所读过的任何编程书都影响更大。特别是他的“建造者模式”(项目2)产生了最大的影响。

尽管Bloch的构建者在几个月内使我比过去十年更深入,但我仍然发现自己碰壁:使用自返回方法链扩展类充其量是令人沮丧的,更糟的是噩梦--especially当仿制药开始发挥作用,并特别自我参照仿制药(如Comparable<T extends Comparable<T>>)。

我有两个主要需求,在这个问题中,我只想关注其中的第二个需求:

  1. 第一个问题是“如何共享自返回方法链,而不必在每个...单个...类中重新实现它们?” 对于那些可能感到好奇的人,我已经在本答案的底部讨论了这一部分,但这不是我想在这里重点讨论的。

  2. 我要评论的第二个问题是“如何在自己打算由许多其他类扩展的类中实现构建器?” 与构建者一起扩展课程比没有构建者扩展难度更大。扩展具有构建器的类也很麻烦Needable因此也具有重要的泛型

所以这是我的问题:我如何改进(称为)Bloch Builder,这样我就可以随意将构建器附加到任何类上,即使该类是可能成为“基础类”的,扩展和扩展了许多次- 在不影响我的未来自我或我的图书馆用户的情况下,由于构建器(及其潜在的泛型)施加了额外的负担吗?


附录
我的问题集中在上面的第2部分,但是我想详细说明问题一,包括如何处理它:

第一个问题是“如何共享自返回方法链,而不必在每个...单个...类中重新实现它们?” 这并不是要防止扩展类不得不重新实现这些链,当然,它们必须-而是如何防止希望利用这些方法链的非子类重新实现。 -实现每个自返回功能,以便他们的用户能够利用它们?为此,我提出了一个需要需求的设计,我将在此处打印界面框架,并暂时保留该框架。它对我来说效果很好(此设计需要花费数年的时间……最难的部分是避免循环依赖):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}

Answers:


21

对我来说,我创造了比乔什·布洛赫(Josh Bloch)的建造者模式大的进步。更不用说以某种方式“更好”了,只是在非常特殊的情况下,它确实提供了一些优点-最大的优点是它使建造者与即将建造的阶级脱钩。

我已经在下面彻底记录了这种替代方法,称为“盲目构建器模式”。


设计模式:盲目的建造者

作为Joshua Bloch的Builder模式的替代品(Effective Java中的项目2,第2版),我创建了所谓的“ Blind Builder Pattern”,它具有Bloch Builder的许多优点,除了一个字符外,以完全相同的方式使用。盲人建造者具有以下优势

  • 将构建器与其封闭类分离,消除循环依赖,
  • 大大减少了封闭类的源代码的大小(不再是),并且
  • 允许ToBeBuilt扩展类而不必扩展其builder

在本文档中,我将构建的类称为“ ToBeBuilt”类。

用Bloch Builder实现的类

Bloch Builder public static class包含在它所构建的类中。一个例子:

公共类UserConfig {
   私有最终字符串sName;
   私人最终int iAge;
   私有最终字符串sFavColor;
   public UserConfig(UserConfig.Cfg uc_c){//构造器
      //传递
         尝试{
            sName = uc_c.sName;
         } catch(NullPointerException rx){
            抛出新的NullPointerException(“ uc_c”);
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      //在此处验证所有字段
   }
   公共字符串toString(){
      返回“ name =” + sName +“,age =” + iAge +“,sFavColor =” + sFavColor;
   }
   //builder...START
   公共静态类Cfg {
      私有字符串sName;
      私人内部年龄;
      私有字符串sFavColor;
      公共Cfg(String s_name){
         sName = s_name;
      }
      //自动返回的二传手... START
         公开Cfg年龄(int i_age){
            iAge = i_age;
            返回这个
         }
         公共Cfg favoriteColor(String s_color){
            sFavColor = s_color;
            返回这个
         }
      //自动返回的二传手... END
      公共UserConfig build(){
         返回(新的UserConfig(this));
      }
   }
   //builder...END
}

使用Bloch Builder实例化课程

UserConfig uc = new UserConfig.Cfg(“ Kermit”)。age(50).favoriteColor(“ green”)。build();

相同的类,实现为Blind Builder

Blind Builder分为三个部分,每个部分都在单独的源代码文件中:

  1. ToBeBuilt类(在本例中:UserConfig
  2. 其“ Fieldable”界面
  3. 建造者

1.待建班

待构建类将其Fieldable接口作为其唯一的构造函数参数。构造函数从中设置所有内部字段,并对其进行验证。最重要的是,此类ToBeBuilt不了解其构造器。

公共类UserConfig {
   私有最终字符串sName;
   私人最终int iAge;
   私有最终字符串sFavColor;
    public UserConfig(UserConfig_Fieldable uc_f){//构造器
      //传递
         尝试{
            sName = uc_f.getName();
         } catch(NullPointerException rx){
            抛出新的NullPointerException(“ uc_f”);
         }
         iAge = uc_f.getAge();
         sFavColor = uc_f.getFavoriteColor();
      //在此处验证所有字段
   }
   公共字符串toString(){
      返回“ name =” + sName +“,age =” + iAge +“,sFavColor =” + sFavColor;
   }
}

作为一个聪明的评论者(谁莫名其妙地删除自己的答案)指出,如果ToBeBuilt类也实现了它Fieldable,它的一个和唯一的构造可以作为它的两个主要拷贝构造函数(一个缺点是,字段始终有效,即使已知原始字段ToBeBuilt是有效的)。

2.“ Fieldable”界面

现场接口是ToBeBuilt类及其生成器之间的“桥梁” ,定义了构建对象所需的所有字段。该接口是ToBeBuilt类构造函数所必需的,并且是由构造函数实现的。由于此接口可以由构建器以外的其他类实现,因此任何类都可以轻松实例化ToBeBuilt该类,而不必强制使用其构建器。ToBeBuilt当不希望或不需要扩展其生成器时,这也使扩展该类变得更加容易。

如下一节所述,我完全不介绍此接口中的功能。

公共接口UserConfig_Fieldable {
   字符串getName();
   int getAge();
   字符串getFavoriteColor();
}

3.建造者

构建器实现Fieldable该类。它根本不做任何验证,并且要强调这一事实,它的所有领域都是公共的并且是可变的。尽管不需要这种公共可访问性,但我更喜欢并推荐它,因为它加强了以下事实:在ToBeBuilt调用的构造函数之前,不会进行验证。这很重要,因为另一个线程有可能在传递给ToBeBuilt的构造函数之前进一步操纵该构造函数。保证字段有效的唯一方法(假设构建器无法以某种方式“锁定”其状态)是由ToBeBuilt类进行最终检查。

最后,与Fieldable接口一样,我没有记录其任何获取器。

公共类UserConfig_Cfg实现UserConfig_Fieldable {
   公共字符串sName;
   公共情报;
    public String sFavColor;
    公共UserConfig_Cfg(String s_name){
       sName = s_name;
    }
    //自动返回的二传手... START
       public UserConfig_Cfg age(int i_age){
          iAge = i_age;
          返回这个
       }
       public UserConfig_Cfg favoriteColor(String s_color){
          sFavColor = s_color;
          返回这个
       }
    //自动返回的二传手... END
    //getters...START
       公共字符串getName(){
          返回sName;
       }
       public int getAge(){
          返回iAge;
       }
       公共字符串getFavoriteColor(){
          返回sFavColor;
       }
    //getters...END
    公共UserConfig build(){
       返回(新的UserConfig(this));
    }
}

使用Blind Builder实例化课程

UserConfig uc = new UserConfig_Cfg(“ Kermit”)。age(50).favoriteColor(“ green”)。build();

唯一的区别是“ UserConfig_Cfg”而不是“ UserConfig.Cfg

笔记

缺点:

  • Blind Builders无法访问此类的私人成员ToBeBuilt
  • 它们更加冗长,因为现在在构建器和界面中都需要使用吸气剂。
  • 一个班级的一切不再仅仅放在一个地方

编译Blind Builder很简单:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Fieldable接口是完全可选的

对于ToBeBuilt缺少必填字段的类(例如本UserConfig示例类),构造函数可以简单地是

public UserConfig(String s_name,int i_age,String s_favColor){

并用

公共UserConfig build(){
   返回(新的UserConfig(getName(),getAge(),getFavoriteColor()));
}

甚至完全消除(在构建器中)吸气剂:

   返回(新的UserConfig(sName,iAge,sFavoriteColor));

通过直接传递字段,ToBeBuilt该类与Fieldable接口一样“盲”(不知道其生成器)。但是,对于ToBeBuilt要“多次扩展和再扩展”的类(在本文的标题中),对任何字段的任何更改必须在每个子类,每个构建器和ToBeBuilt构造函数中进行更改。随着字段和子类的数量增加,进行维护变得不切实际。

(实际上,在几乎没有必填字段的情况下,完全使用构建器可能会过大。对于那些感兴趣的人,这里是我的个人库中一些较大的Fieldable接口的示例。)

子包中的二级类

Fieldable对于所有盲目的构建器,我选择将所有构建器和类包含在其ToBeBuilt类的子包中。子程序包始终命名为“ z”。这样可以防止这些辅助类弄乱JavaDoc软件包列表。例如

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

验证示例

如上所述,所有验证都在ToBeBuilt的构造函数中进行。这又是带有示例验证代码的构造函数:

公开的UserConfig(UserConfig_Fieldable uc_f){
   //传递
      尝试{
         sName = uc_f.getName();
      } catch(NullPointerException rx){
         抛出新的NullPointerException(“ uc_f”);
      }
      iAge = uc_f.getAge();
      sFavColor = uc_f.getFavoriteColor();
   //验证(应该真的预先编译模式...)
      尝试{
         if(!Pattern.compile(“ \\ w +”)。matcher(sName).matches()){
            抛出新的IllegalArgumentException(“ uc_f.getName()(\”“ + sName +” \“)可能不为空,并且只能包含字母数字和下划线。”);
         }
      } catch(NullPointerException rx){
         抛出新的NullPointerException(“ uc_f.getName()”);
      }
      if(iAge <0){
         抛出新的IllegalArgumentException(“ uc_f.getAge()(” + iAge +“)小于零。”);
      }
      尝试{
         if(!Pattern.compile(“(?:red | blue | green | hot pink)”)。matcher(sFavColor).matches()){
            抛出新的IllegalArgumentException(“ uc_f.getFavoriteColor()(\”“ + uc_f.getFavoriteColor()+” \“)不是红色,蓝色,绿色或粉红色。”);
         }
      } catch(NullPointerException rx){
         抛出新的NullPointerException(“ uc_f.getFavoriteColor()”);
      }
}

文档编制者

本部分适用于Bloch Builder和Blind Builders。它演示了我如何记录此设计中的类,如何使设置器(在构建器中)和它们的获取器(在ToBeBuilt类中)相互交叉引用-只需单击一下鼠标,而用户无需知道位置这些功能实际上存在-并且开发人员不必多余地记录任何内容。

吸气剂:ToBeBuilt仅在班级中

吸气剂仅记录在ToBeBuilt班级中。_Fieldable_Cfg类中的等效getter 被忽略。我完全没有记录它们。

/ **
   <P>用户的年龄。</ P>
   @return表示用户年龄的int。
   @see UserConfig_Cfg#age(int)
   @see getName()
 ** /
public int getAge(){
   返回iAge;
}

第一个@see是指向其setter的链接,该链接位于builder类中。

设置者:在构建器类中

设置器记录,如果它是在ToBeBuilt,也仿佛确实验证(这实际上是由做ToBeBuilt的构造函数)。星号(“ *”)是视觉提示,表明链接的目标在另一个类中。

/ **
   <P>设置用户的年龄。</ P>
   @param i_age不得小于零。使用{@code UserConfig#getName()getName()} *获取。
   @see #favoriteColor(String)
 ** /
public UserConfig_Cfg age(int i_age){
   iAge = i_age;
   返回这个
}

更多信息

放在一起:Blind Builder示例的完整源代码以及完整的文档

UserConfig.java

导入java.util.regex.Pattern;
/ **
   <P>有关用户的信息-<I> [构建器:UserConfig_Cfg] </ I> </ P>
   <P>所有字段的验证都在此类构造函数中进行。但是,每个验证要求仅在构建器的setter函数中是文档。</ P>
   <P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
公共类UserConfig {
   公共静态最终void main(String [] igno_red){
      UserConfig uc = new UserConfig_Cfg(“ Kermit”)。age(50).favoriteColor(“ green”)。build();
      System.out.println(uc);
   }
   私有最终字符串sName;
   私人最终int iAge;
   私有最终字符串sFavColor;
   / **
      <P>创建一个新实例。这将设置并验证所有字段。</ P>
      @param uc_f可能不是{@code null}。
    ** /
   公开的UserConfig(UserConfig_Fieldable uc_f){
      //传递
         尝试{
            sName = uc_f.getName();
         } catch(NullPointerException rx){
            抛出新的NullPointerException(“ uc_f”);
         }
         iAge = uc_f.getAge();
         sFavColor = uc_f.getFavoriteColor();
      //验证
         尝试{
            if(!Pattern.compile(“ \\ w +”)。matcher(sName).matches()){
               抛出新的IllegalArgumentException(“ uc_f.getName()(\”“ + sName +” \“)可能不为空,并且只能包含字母数字和下划线。”);
            }
         } catch(NullPointerException rx){
            抛出新的NullPointerException(“ uc_f.getName()”);
         }
         if(iAge <0){
            抛出新的IllegalArgumentException(“ uc_f.getAge()(” + iAge +“)小于零。”);
         }
         尝试{
            if(!Pattern.compile(“(?:red | blue | green | hot pink)”)。matcher(sFavColor).matches()){
               抛出新的IllegalArgumentException(“ uc_f.getFavoriteColor()(\”“ + uc_f.getFavoriteColor()+” \“)不是红色,蓝色,绿色或粉红色。”);
            }
         } catch(NullPointerException rx){
            抛出新的NullPointerException(“ uc_f.getFavoriteColor()”);
         }
   }
   //getters...START
      / **
         <P>用户名。</ P>
         @return非{@code null}非空字符串。
         @see UserConfig_Cfg#UserConfig_Cfg(String)
         @see #getAge()
         @see #getFavoriteColor()
       ** /
      公共字符串getName(){
         返回sName;
      }
      / **
         <P>用户的年龄。</ P>
         @return一个大于零或等于零的数字。
         @see UserConfig_Cfg#age(int)
         @see #getName()
       ** /
      public int getAge(){
         返回iAge;
      }
      / **
         <P>用户喜欢的颜色。</ P>
         @return非{@code null}非空字符串。
         @see UserConfig_Cfg#age(int)
         @see #getName()
       ** /
      公共字符串getFavoriteColor(){
         返回sFavColor;
      }
   //getters...END
   公共字符串toString(){
      返回“ getName()=” + getName()+“,getAge()=” + getAge()+“,getFavoriteColor()=” + getFavoriteColor();
   }
}

UserConfig_Fieldable.java

/ **
   <P> {@ link UserConfig} {@code UserConfig#UserConfig(UserConfig_Fieldable)构造函数}必需。</ P>
 ** /
公共接口UserConfig_Fieldable {
   字符串getName();
   int getAge();
   字符串getFavoriteColor();
}

UserConfig_Cfg.java

导入java.util.regex.Pattern;
/ **
   <P> {@ link UserConfig}的构建器。</ P>
   <P>所有字段的验证都在<CODE> UserConfig </ CODE>构造函数中进行。但是,每个验证要求仅在此类setter函数中是文档。</ P>
 ** /
公共类UserConfig_Cfg实现UserConfig_Fieldable {
   公共字符串sName;
   公共情报;
   public String sFavColor;
   / **
      <P>使用用户名创建一个新实例。</ P>
      @param s_name不能为{@code null}或为空,并且只能包含字母,数字和下划线。使用{@code UserConfig#getName()getName()} {@ code()}获取。
    ** /
   公共UserConfig_Cfg(String s_name){
      sName = s_name;
   }
   //自动返回的二传手... START
      / **
         <P>设置用户的年龄。</ P>
         @param i_age不得小于零。使用{@code UserConfig#getName()getName()} {@ code()}获取。
         @see #favoriteColor(String)
       ** /
      public UserConfig_Cfg age(int i_age){
         iAge = i_age;
         返回这个
      }
      / **
         <P>设置用户喜欢的颜色。</ P>
         @param s_color必须为{@code“ red”},{@ code“ blue”},{@ code green}或{@code“ hot pink”}。使用{@code UserConfig#getName()getName()} {@ code()} *获取。
         @see #age(int)
       ** /
      public UserConfig_Cfg favoriteColor(String s_color){
         sFavColor = s_color;
         返回这个
      }
   //自动返回的二传手... END
   //getters...START
      公共字符串getName(){
         返回sName;
      }
      public int getAge(){
         返回iAge;
      }
      公共字符串getFavoriteColor(){
         返回sFavColor;
      }
   //getters...END
   / **
      <P>按照配置构建UserConfig。</ P>
      @return <CODE>(新的{@link UserConfig#UserConfig(UserConfig_Fieldable)UserConfig}(this))</ CODE>
    ** /
   公共UserConfig build(){
      返回(新的UserConfig(this));
   }
}


1
绝对是一个进步。此处实现的Bloch的构建器将两个具体的类结合在一起,这两个是待构建的类及其构建器。这本身就是不好的设计。您所描述的Blind Builder通过将要构建的类将其构造依赖项定义为抽象打破这种耦合,其他类可以以解耦的方式实现该抽象。您已经极大地应用了基本的面向对象设计指南。
rucamzu 2014年

3
如果还没有的话,您应该在某个地方写博客,这是不错的算法设计!我现在不分享它了:-)。
Martijn Verburg 2014年

4
谢谢你的客气话。这是我的新博客上的第一篇帖子:aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind 2014年

如果构建器和构建对象都实现了Fieldable,则该模式开始类似于我称为ReadableFoo / MutableFoo / ImmutableFoo的模式,尽管该方法不是使可变的东西成为构建器的“ build”成员,调用它asImmutable并将其包含在ReadableFoo接口中(使用该哲学,调用build不可变对象将仅返回对同一对象的引用)。
supercat

1
@ThomasN您需要扩展*_Fieldable并为其添加新的getter,并扩展其 *_Cfg,并向其添加新的setter,但是我不明白为什么您需要重现现有的getter和setter。它们是继承的,除非它们需要其他功能,否则无需重新创建它们。
aliteralmind

13

我认为这里的问题从一开始就假设了某种东西,而没有试图证明这一点,即构建器模式本质上是好的。

tl; dr我认为构建器模式很少是一个好主意。


生成器模式用途

构建器模式的目的是维护两个规则,这些规则使使用类变得更加容易:

  1. 对象应该不能以不一致/不可用/无效的状态构造。

    • 这是指例如一个场景Person对象可以在不必它来构建Id填写,而所有部分的代码使用该对象可能需要Id只是正确地配合工作Person
  2. 对象构造函数不应要求太多参数

因此,构建器模式的目的毫无争议。我认为它的大部分需求和用法都是基于到目前为止已经进行的分析:我们想要这两个规则,这给出了这两个规则-尽管我认为值得研究实现这两个规则的其他方法。


为什么还要考虑其他方法呢?

我认为这个问题本身的事实已充分说明了原因。在将构建器模式应用于结构时,结构变得复杂且增加了很多仪式。这个问题问的是如何解决其中的一些复杂性,因为就像复杂性一样,它会导致行为异常(继承)。这种复杂性还会增加维护开销(添加,更改或删除属性要比其他复杂得多)。


其他方法

因此,对于上述第一条规则,有什么方法?该规则所指的关键是,在构造时,对象具有正常运行所需的所有信息-构造后,该信息无法在外部更改(因此,它是不可变的信息)。

一种在构造时将所有必要信息提供给对象的方法是简单地向构造函数添加参数。如果构造函数需要该信息,则无法在没有所有这些信息的情况下构造该对象,因此它将被构造为有效状态。但是,如果对象需要大量信息才有效呢?哦,是的,如果是这样的话,这种方法将违反上面的规则2

好的,还有什么?好吧,您可以简单地获取使对象处于一致状态所需的所有信息,然后将其捆绑到另一个在构造时获取的对象中。您上面的代码将没有:

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

尽管它稍微简单一些,但它与构建器模式没有太大不同,最重要的是,我们现在满足规则1和规则2

那么,为什么不多花些钱使它成为完整的构建器呢?根本没有必要。在这种方法中,我满足了构建器模式的两个目的,它们稍微更简单,更易于维护并且可重用。最后一点是关键,使用的示例是虚构的,并不适合现实世界中的语义目的,因此,让我们展示一下这种方法如何导致可重用的DTO而不是单一目的的类

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

因此,当您像这样构建具有凝聚力的 DTO时,它们既可以更轻松地满足构建器模式的目的,又可以具有更广泛的价值/实用性。此外,此方法解决了构建器模式导致的继承复杂性:

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

您可能会发现DTO并不总是具有内聚性,或者要使属性分组具有内聚性,就需要跨多个DTO破坏它们-这并不是真正的问题。如果您的对象需要18个属性,并且可以使用这些属性制作3个具有凝聚力的DTO,那么您将拥有一个满足构建者目的的简单构造,然后是一些。如果您无法提出内聚分组,则这可能表明您的对象具有完全不相关的属性而不具有内聚性-但即使如此,由于实现起来更简单,因此制作单个非内聚DTO还是可取的解决您的继承问题。


如何改善建造者模式

好吧,撇开所有麻烦,您遇到了一个问题,正在寻找一种解决方案。我的建议:继承类可以简单地具有从超类的builder类继承的嵌套类,因此继承类的结构与超类基本相同,并且具有应与其他功能完全相同的生成器模式有关子类的其他属性。


什么时候好主意

放任不管,建造者模式有一个利基。我们都知道这一点,因为我们都在某一点上学习了这个特定的构建器:StringBuilder-这里的目的不是简单的构建,因为字符串的构建和连接等并不容易。这是一个很棒的构建器,因为它具有性能优势。

因此,性能优势是:您有一堆对象,它们是不可变类型的,您需要将它们折叠成一个不可变类型的对象。如果逐步执行此操作,则将在此处创建许多中间对象,因此一次执行所有操作会更加高效和理想。

因此,我认为何时才是个好主意的关键是在以下问题的领域StringBuilder需要将多个不可变类型的实例转换为一个不可变类型的实例


我认为您给出的示例不符合任何一条规则。没有什么可以阻止我在无效状态下创建Cfg的功能,而参数已从ctor中移出时,它们只是被移到了一个不太惯用和更冗长的位置。fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()提供了用于构建foos的简洁API,并可以在构建器本身中提供实际的错误检查。没有构建器,对象本身就必须检查其输入,这意味着我们的处境比以前更好。
Phoshi 2014年

DTO可以在setter上用批注通过声明的方式以多种方式验证其属性,但是您想这样做-验证是一个单独的问题,在他的构造器方法中,他显示了构造器中发生的验证,同样的逻辑非常适合在我的方法中。但是,通常最好使用DTO对其进行验证,因为正如我所展示的-DTO可用于构造多种类型,因此对其进行验证将有助于验证多种类型。构建器仅验证为其制造的一种特定类型。
Jimmy Hoffa

也许最灵活的方法是在构建器中具有静态验证功能,该功能接受单个Fieldable参数。可以从ToBeBuilt构造函数中调用此验证函数,但可以由任何地方的任何事物调用它。这消除了冗余代码的可能性,而无需强制执行特定的实现。(并且,如果您不喜欢这个Fieldable概念,没有什么可以阻止您将各个字段传递给验证功能的,但是现在至少必须在三个地方维护字段列表。)
aliteralmind 2014年

+1并且一个在其构造函数中具有过多依赖关系的类显然不够内聚,应将其重构为较小的类。
Basilevs

@JimmyHoffa:啊,我明白了,您只是忽略了这一点。我不确定我是否看到了此构建器与构建器之间的区别,然后除了将配置实例传递给ctor而不是在某个构建器上调用.build之外,并且该构建器有一条更明显的路径来对所有构建器进行正确性检查数据。每个单独的变量可能在其有效范围内,但在该特定排列中无效。.build可以检查这一点,但是将项目传递到ctor中需要在对象本身内部进行错误检查-糟糕!
Phoshi 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.