effective java 第三版 条目2:思考使用builder当面对大量的构造器参数的时候

    静态工厂和构造器都有这样一个限制:他们当面对一个大量的操作参数都不能表现很好。思考这样一个问题,如果有一个类,它表示一包食物的营养价值的标签。这个标签有少量的必须字段——(serving size, servings per container, and calories per serving)——而这里有可能超过20个可选择的字段(——(total fat, saturated fat, trans fat, cholesterol, sodium, and so on)

    这样一个类的构造器或者静态工厂方法的顺序将会是怎么样的?传统之上,程序员已经使用过重叠构造模式(telescoping constructor pattern),这种模式你可需要提供一个所有必须参数的构造器,另一个是只有一个的可选参数的构造器,第三个是两个可选参数的构造器,等等等等等....到最后在一个构造器中拥有所有的可选参数。那么这看起来是怎么样?为了简单,仅展示四个可选字段的构造器:

// Telescoping constructor pattern - does not scale well!

public class NutritionFacts {

        private final int servingSize; // (mL) required

        private final int servings; // (per container) required

        private final int calories; // (per serving) optional

        private final int fat;   // (g/serving)    optional

        private final int sodium; // (mg/serving)  optional

        private final int carbohydrate; // (g/serving) optional

        public NutritionFacts(int servingSize, int servings) {

                 this(servingSize, servings, 0);

        }

        public NutritionFacts(int servingSize, int servings, int calories) {

                this(servingSize, servings, calories, 0);

        }

        public NutritionFacts(int servingSize, int servings, int calories, int fat) {

                this(servingSize, servings, calories, fat, 0);

        }

          public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {

                this(servingSize, servings, calories, fat, sodium, 0);

        }

        public NutritionFacts(int servingSize, int servings,

                int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize;

                this.servings = servings; this.calories = calories; this.fat = fat;

                this.sodium = sodium; this.carbohydrate = carbohydrate;

        }

}

      当你想要创建一个实例的时候,你可以使用包含所有你想设置的最短的参数列表。

  NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);

       这个典型的构造器的调用将可能需要很多你不想设置的参数。但是你不得不为他们赋上一个值,在这个问题上,我们将fat设置为0,在只有六个参数的时候这看起来可能并不坏,但是在参数不断增加的时候,这很快就会失控。

      简而言之,重叠构造模式有用,但是在有很多参数的时候他很难去写出一个客户端的代码,同时也很难去阅读它!阅读者很好奇那些参数值到底是什么意思,同时必须非常关注计算参数的数量。长期的后果将导致一邪微妙的bug。如果这个客户端意外的倒转类两个参数的位置,编译就可能不会完成(通常类型不同IDE就会检查出来,但是两个参数类型相同,IDE就无法检查,就会出现难以排查的错误)!而程序在运行时就会出现错误。

第二个替代的方案是,在面对大量的可选参数的时候,你可以使用JavaBeans模式。你可以调用一个无参的构造器来创建一个对象,然后通过setter方法来设置每一个必须的参数和每一个可选参数

// JavaBeans Pattern - allows inconsistency, mandates mutability

public class NutritionFacts {

      // Parameters initialized to default values (if any)

       private int servingSize = -1; // Required; no default value

       private int servings = -1; // Required; no default value private int calories = 0;

      private int fat = 0;

      private int sodium = 0; private int carbohydrate = 0;

      public NutritionFacts() { }

      // Setters

      public void setServingSize(int val) {    servingSize = val; }

       public void setServings(int val) { servings = val; }

      public void setCalories(int val) { calories = val; }

       public void setFat(int val) { fat = val; }

      public void setSodium(int val) { sodium = val; }

       public void setCarbohydrate(int val) { carbohydrate = val; }

}

这种模式没有重叠构造模式的缺点,他很简单,可能有一点冗余,但是很容易阅读。

NutritionFacts cocaCola = new NutritionFacts(); 

cocaCola.setServingSize(240); 

cocaCola.setServings(8); 

cocaCola.setCalories(100); 

cocaCola.setSodium(35); 

cocaCola.setCarbohydrate(27);

      不幸的是,这个JavaBeans模式有一个严重的缺点,因为构造行为是通过很多次调用set方法来完成,所以每一个JavaBean可能会在构造过程中出现一个不正确的状态(a JavaBean may be in an inconsistent state partway through its construction.)这个类没有连续地实施对构造器参数的检查。计划当这个对象还是一个不一致的状态去使用这个对象,可能会导致远超过代码能控制范围的bug的错误,所以很难去调试。另一个和这个相关的缺点就是JavaBeans模式不允许去制造一个不可变对象!同时,在使用JavaBeans模式中,程序员需要去确保线程安全的问题。 

      通过“冻结“一个对象去降低这个短板是可行的,当一个对象的构造过程完成之前,不允许使用这个对象!但是这样的变体很笨重也很少在实际中使用。更多的情况是,由于编译器不能在一个对象使用之前确保程序员已经调用对象中的这个冻结方法,所以冻结一个对象的方式将会导致一些运行时错误。

       幸运到是,这里有第三种可替代的方式,这种方式联合了 重叠构造器模式的安全性和JavaBeans模式的可读性,这就形成了建造者模式(Builder pattern)。在使用建造者模式,我们不是首先就来创建一个想要的对象!首先客户端调用一个有所有必须参数的构造器(或者静态工厂方法)来得到一个builder对象,然后客户端调用builder对象上的类似于setter的方法来设置每一个需要的可选参数。最后,客户端调用一个无参的build方法来形成我们想要的对象。(这个对象是一个典型的不可变对象)。这个builder是在将要构建的类中的一个标准的静态成员类(条目24)。

  这里有一个建造者模式的例子:

// Builder Pattern

public class NutritionFacts { 

       private final int servingSize;

       private final int servings; 

       private final int calories;

        private final int fat;

       private final int sodium; 

       private final int carbohydrate;


        public static class Builder {

             // Required parameters

             private final int servingSize;

              private final int servings;

             // Optional parameters - initialized to default values private int calories = 0;

             private int fat = 0;

             private int sodium = 0; 

             private int carbohydrate = 0;


             public Builder(int servingSize, int servings) { 

                    this.servingSize = servingSize;        

                    this.servings = servings;

             }


             public Builder calories(int val){

                     calories = val; return this; 
             }

              public Builder fat(int val){

                     fat = val; return this;

              }

              public Builder sodium(int val){

                     sodium = val; return this; 

             }        

             public Builder carbohydrate(int val){

                     carbohydrate = val; return this;

              }


             public NutritionFacts build() { 

                    return new NutritionFacts(this);

             }

       }


       private NutritionFacts(Builder builder) { 

              servingSize = builder.servingSize; 

              servings = builder.servings; 

              calories = builder.calories;

              fat = builder.fat;

              sodium = builder.sodium;

               carbohydrate = builder.carbohydrate;

           }

}


       这个NutritionFacts 类是不可变的,同时所有的参数默认值都在一个地方。这个builder的setter方法返回一个建造者以至于能够使用一个流畅的链式调用。这里可以看一看在客户端的代码是怎么样:

   NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();


      这个客户端的代码很容易去写,更重要的是,它很容易去阅读。这个建造者模式的可选参数可以在Python和Scala语言中直接发现(比如python中的关键参数和参数有默认值的特性可以就是建造者模式的体现)。

     为了简洁,在这里有合理的检查被省略了。为了尽可能的检查验证参数,检查参数应该出现在在builder的构造器和方法中!在构造器调用build方法的时候也要检查builder的不可变性(类似集合的modCount变量??)。为了确保风险的不可改变,在从builder上复制字段到对象上时必须做出做这个检查,如果检查失败,就要抛出IllegalArgumentException异常!异常的细节即是这个参数没有通过验证的信息!

     建造者模式非常适合类层次结构!(The Builder pattern is well suited to class hierarchies.),在每一个内嵌在对应的class,使用一个建造者的平行层次。抽象类有抽象的建造者,具体的类有具体的建造者!例如,思考,一个在根节点层次的抽象类将会有各种各样的表现!

// Builder pattern for class hierarchies

public abstract class Pizza {

      public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set toppings;

     abstract static class Builder> {

            EnumSet toppings = EnumSet.noneOf(Topping.class);

            public T addTopping(Topping topping) {

                  toppings.add(Objects.requireNonNull(topping));

                  return self();

            }

         abstract Pizza build();

         // Subclasses must override this method to return "this" protected abstract T self();

       }

       Pizza(Builder builder) {

              toppings = builder.toppings.clone(); // See Item 50

       }

     注意到,这个Pizza.Builder是一个有递归参数的类的属性(条目30),这个和抽象自身的方法一起,允许方法链式的在没有额外的花费就能良好的在子类运行。

     这个技术工作的的原因,事实上是由于Java缺少一个它自己已知的包装的语言特性。(This workaround for the fact that Java lacks a self type is known as the simulated self-type idiom.)

    这里有两个具体的Pizza的子类,一个提供一个基础的New-York-style pizza,另一个,构成需要一个size的参数,这个参数(size)将会导致后者对象的调味汁应该放在外面还是里面。

public class NyPizza extends Pizza {

       public enum Size { SMALL, MEDIUM, LARGE } private final Size size;

        public static class Builder extends Pizza.Builder { private final Size size;

       public Builder(Size size) {

              this.size = Objects.requireNonNull(size);

       }

       @Override public NyPizza build() {

               return new NyPizza(this);

       }

          @Override protected Builder self() { 

              return this;

        }

}

private NyPizza(Builder builder) {

              super(builder);

              size = builder.size;

       }

}

public class Calzone extends Pizza { private final boolean sauceInside;

              public static class Builder extends Pizza.Builder {

                      private boolean sauceInside = false; // Default

                     public Builder sauceInside() { sauceInside = true;

                     return this;

              }

              @Override public Calzone build() { return new Calzone(this);

              }


              @Override protected Builder self() { return this; }

              }

              private Calzone(Builder builder) { super(builder);

                     sauceInside = builder.sauceInside;

              }

       }

值得注意的是,这个build方法在每个子类的builder被声明将要返回正确的子类:NyPizza.Builder的build方法返回NyPizza,当一个在Calzone.Builder将返回一个Calzone。这个在子类中声明具体的返回类型,但是这个方法的顶级声明在父类中出现的技术被称作covariant return typing它运行客户端不需要多余耗费地使用这些builider。

   对于这个层次builder,客户端的代码本质上和NutritionFacts 的builder的代码是相同的。这个接下来的客户端的代码展示短暂地从枚举常量中静态导入的例程。

NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();

Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

        建造者模式在构造器上的一个主要的优点是builder可以有多重的可变化的参数,因为每一个参数被指定在他们独有的方法中。要不然,builder可以通过调用方法将这些参数合并到一个单一的字段,如在addTopping之前的builder构造方法。

       建造者模式是非常灵活的!一个简单的建造者可以重复使用地构建多个对象,builder的参数可以在build方法调用之前做合理的调整来改变他们将要创建的对象。一个建造者可以像创建的对象自动地填充一些字段。比如说某一个字段,是在每一次创建一个对象之后会连续增长的数字。

   建造者模式也有他的缺点,为了创建一个对象,你必须首先创建一个builder,然而在实际中,创建builder的花费不太可能被关注到。这可能会是一些问题在性能临界位置(it could be a problem in performance-critical situations.)。同时,建造者模式比重叠构造模式更加冗余,所以它可能只有在参数较多的情况下才值得被使用,一般而言,大于等于四个可选的参数。但是,总是要记住你未来可能会添加新的参数,而如果你开始时使用构造器过着静态工厂来创建对象,当类演化到一个参数的数量出现失控时才改变成一个builder,这些废弃的构造器或者静态工厂方法将让人非常难受。因此,通常而言最好首先就使用建造者作为开始。

   总结:建造者模式是在设计一个构造参数可能会失控的类的一个很好的选择。尤其是在很多参数是可选的或者是相同的类型,客户端代码会非常简单易读当使用建造者模式而不是重叠构造器,同时建造者模式也比JavaBeans模式更加安全

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 1、何时以及如何创建对象? 2、何时以及如何避免创建对象? 3、如何确保它们能够适时地销毁,以及如何管理对象销毁之...
    子玲2017阅读 203评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,103评论 18 139
  • 今天宝贝很听话,背了两首古诗,宝贝加油
    梦花如雪_7290阅读 94评论 0 0
  • 书名:《一只聪明的笨狼》 从前有一只有教养的小狼,他第一次一个人去捕食。第一次,他捉到一只小兔子,小兔子对他说:“...
    8f95326955fd阅读 217评论 0 2
  • 哪里杵着一个悲观主义 在默默的哀悼 在急火奔驰的世界中选择静谧 在欢喜气氛中徒添几分悲调 我们兴高采烈的远离这样的...
    lexluseyan阅读 134评论 0 0