From 3e9b73708fe7a237b96bab43b9c498fc3f2af46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=89=E5=8B=87=E5=BC=BA2?= Date: Wed, 11 Sep 2019 20:00:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=93=E6=9E=84=E9=80=A0=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=BF=87=E5=A4=9A=E6=97=B6=E4=BD=BF=E7=94=A8?= =?UTF-8?q?builder=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../02.当构造方法参数过多时使用builder模式.md | 51 ++++++++ .../CreatingAndDestroyingObjects/pom.xml | 44 +++---- .../main/java/com/jueee/item02/Item02Example01.java | 48 ++++++++ .../main/java/com/jueee/item02/Item02Example02.java | 30 +++++ .../main/java/com/jueee/item02/Item02Example03.java | 74 ++++++++++++ .../main/java/com/jueee/item02/Item02Example04.java | 113 ++++++++++++++++++ 6 files changed, 340 insertions(+), 20 deletions(-) create mode 100644 ch02创建和销毁对象/02.当构造方法参数过多时使用builder模式.md create mode 100644 ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example01.java create mode 100644 ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example02.java create mode 100644 ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example03.java create mode 100644 ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example04.java diff --git a/ch02创建和销毁对象/02.当构造方法参数过多时使用builder模式.md b/ch02创建和销毁对象/02.当构造方法参数过多时使用builder模式.md new file mode 100644 index 0000000..c17946c --- /dev/null +++ b/ch02创建和销毁对象/02.当构造方法参数过多时使用builder模式.md @@ -0,0 +1,51 @@ +## 当构造方法参数过多时使用builder模式 + +静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。 + +考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过20个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。 + +### 可伸缩构造方法模式 + +**代码示例**:[Item02Example01.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example01.java) + +在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。 + +可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的bug。如果客户端意外地反转了两个这样的参数,编译器并不会抱怨,但是程序在运行时会出现错误行为(条目51)。 + +### JavaBeans 模式 + +**代码示例**:[Item02Example02.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example02.java) + +在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置每个必需的参数和可选参数。 + +这种模式没有伸缩构造方法模式的缺点。有点冗长,但创建实例很容易,并且易于阅读所生成的代码。 + +但 JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中JavaBean可能处于不一致的状态。该类没有通过检查构造参数的有效性来执行一致性的选项。在不一致的状态下尝试使用对象可能会导致与包含bug的代码大相径庭的错误,因此很难调试。一个相关的缺点是,JavaBeans模式排除了让类不可变的可能性(条目17),并且需要在程序员的部分增加工作以确保线程安全。 + +### Builder 模式 + +Builder 模式结合了可伸缩构造方法模式的安全性和 javabean 模式的可读性。 + +客户端不直接调用所需的对象,而是调用构造方法(或静态工厂),并使用所有必需的参数,并获得一个builder对象。然后,客户端调用builder对象的`setter`相似方法来设置每个可选参数。最后,客户端调用一个无参的`build`方法来生成对象,该对象通常是不可变的。Builder通常是它所构建的类的一个静态成员类(条目24)。 + +为了简洁起见,省略了有效性检查。 要尽快检测无效参数,检查builder的构造方法和方法中的参数有效性。 在`build`方法调用的构造方法中检查包含多个参数的不变性。为了确保这些不变性不受攻击,在从builder复制参数后对对象属性进行检查(条目 50)。 如果检查失败,则抛出`IllegalArgumentException`异常(条目 72),其详细消息指示哪些参数无效(条目 75)。 + +### Builder模式在类层次结构中的应用 + +Builder模式非常适合类层次结构。 使用平行层次的builder,每个嵌套在相应的类中。 抽象类有抽象的builder; 具体的类有具体的builder。 + +**代码示例**:[Item02Example04.java](CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example04.java) + +每个子类builder中的`build`方法被声明为返回正确的子类:`NyPizza.Builder`的`build`方法返回`NyPizza`,而`Calzone.Builder`中的`build`方法返回`Calzone`。 + +这种技术,其一个子类的方法被声明为返回在超类中声明的返回类型的子类型,称为**协变返回类型**( covariant return typing)。 它允许客户端使用这些builder,而不需要强制转换。 + +### Builder模式总结 + +Builder对构造方法的一个微小的优势是,builder可以有多个可变参数,因为每个参数都是在它自己的方法中指定的。或者,builder可以将传递给多个调用的参数聚合到单个属性中。 + +Builder模式非常灵活。 单个builder可以重复使用来构建多个对象。 builder的参数可以在构建方法的调用之间进行调整,以改变创建的对象。 builder可以在创建对象时自动填充一些属性,例如每次创建对象时增加的序列号。 + +Builder模式也有缺点。为了创建对象,首先必须创建它的builder。虽然创建这个builder的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,如果希望在将来添加更多的参数。但是,如果从构造方法或静态工厂开始,并切换到builder,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个builder。 + +总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且builder比JavaBeans更安全。 \ No newline at end of file diff --git a/ch02创建和销毁对象/CreatingAndDestroyingObjects/pom.xml b/ch02创建和销毁对象/CreatingAndDestroyingObjects/pom.xml index 9764cb4..87cec83 100644 --- a/ch02创建和销毁对象/CreatingAndDestroyingObjects/pom.xml +++ b/ch02创建和销毁对象/CreatingAndDestroyingObjects/pom.xml @@ -1,25 +1,29 @@ - - 4.0.0 + + 4.0.0 - com.github.jueee - CreatingAndDestroyingObjects - 0.0.1-SNAPSHOT - jar + com.github.jueee + CreatingAndDestroyingObjects + 0.0.1-SNAPSHOT + jar - CreatingAndDestroyingObjects - http://maven.apache.org + CreatingAndDestroyingObjects + http://maven.apache.org - - UTF-8 - + + UTF-8 + - - - junit - junit - 3.8.1 - test - - + + + junit + junit + 3.8.1 + test + + + org.projectlombok + lombok + 1.16.6 + + diff --git a/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example01.java b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example01.java new file mode 100644 index 0000000..237c182 --- /dev/null +++ b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example01.java @@ -0,0 +1,48 @@ +package com.jueee.item02; + +import lombok.Data; + +// 可伸缩构造方法模式 +public class Item02Example01 { + + public static void main(String[] args) { + NutritionFacts1 cocaCola = new NutritionFacts1(240, 8, 100, 0, 35, 27); + System.out.println(cocaCola.getServings()); + } + +} + +@Data +class NutritionFacts1 { + 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 NutritionFacts1(int servingSize, int servings) { + this(servingSize, servings, 0); + } + + public NutritionFacts1(int servingSize, int servings, int calories) { + this(servingSize, servings, calories, 0); + } + + public NutritionFacts1(int servingSize, int servings, int calories, int fat) { + this(servingSize, servings, calories, fat, 0); + } + + public NutritionFacts1(int servingSize, int servings, int calories, int fat, int sodium) { + this(servingSize, servings, calories, fat, sodium, 0); + } + + public NutritionFacts1(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; + } +} \ No newline at end of file diff --git a/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example02.java b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example02.java new file mode 100644 index 0000000..4e8d506 --- /dev/null +++ b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example02.java @@ -0,0 +1,30 @@ +package com.jueee.item02; + +import lombok.Data; + +// JavaBeans 模式:调用一个无参数的构造函数来创建对象,然后调用setter方法来设置每个必需的参数和可选参数 +public class Item02Example02 { + + public static void main(String[] args) { + NutritionFacts2 cocaCola = new NutritionFacts2(); + cocaCola.setServingSize(240); + cocaCola.setServings(8); + cocaCola.setCalories(100); + cocaCola.setSodium(35); + cocaCola.setCarbohydrate(27); + System.out.println(cocaCola.getServings()); + } + +} + +@Data +class NutritionFacts2 { + private int servingSize; // (mL) required + private int servings; // (per container) required + private int calories; // (per serving) optional + private int fat; // (g/serving) optional + private int sodium; // (mg/serving) optional + private int carbohydrate; // (g/serving) optional + + public NutritionFacts2() {} +} \ No newline at end of file diff --git a/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example03.java b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example03.java new file mode 100644 index 0000000..1153f1f --- /dev/null +++ b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example03.java @@ -0,0 +1,74 @@ +package com.jueee.item02; + +import lombok.Data; + +// Builder 模式:客户端不直接调用所需的对象,而是调用构造方法(或静态工厂),并使用所有必需的参数,并获得一个builder对象。 +public class Item02Example03 { + + public static void main(String[] args) { + NutritionFacts3 cocaCola = new NutritionFacts3.Builder(240, 8) + .calories(100).sodium(35).carbohydrate(27).build(); + System.out.println(cocaCola.getServings()); + } + +} + +@Data +class NutritionFacts3 { + 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 { + // 必选参数 + private final int servingSize; + private final int servings; + + // 可选参数 - 设置默认值 + 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 NutritionFacts3 build() { + return new NutritionFacts3(this); + } + } + + private NutritionFacts3(Builder builder) { + servingSize = builder.servingSize; + servings = builder.servings; + calories = builder.calories; + fat = builder.fat; + sodium = builder.sodium; + carbohydrate = builder.carbohydrate; + } +} \ No newline at end of file diff --git a/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example04.java b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example04.java new file mode 100644 index 0000000..dd524f0 --- /dev/null +++ b/ch02创建和销毁对象/CreatingAndDestroyingObjects/src/main/java/com/jueee/item02/Item02Example04.java @@ -0,0 +1,113 @@ +package com.jueee.item02; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +import lombok.Data; + +public class Item02Example04 { + + public static void main(String[] args) { + NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL).addTopping(NyPizza.Topping.SAUSAGE).addTopping(NyPizza.Topping.ONION).build(); + System.out.println(pizza.getSize()); + System.out.println(pizza.toppings); + + System.out.println(); + + Calzone calzone = new Calzone.Builder().addTopping(Calzone.Topping.HAM).sauceInside().build(); + System.out.println(calzone.isSauceInside()); + System.out.println(calzone.toppings); + } +} + +// 各种比萨饼的根层次结构的抽象类 +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(); + + // 子类必选重写该方法并且返回 "this" + protected abstract T self(); + } + + Pizza(Builder builder) { + toppings = builder.toppings.clone(); + } +} + +// 代表标准的纽约风格的披萨:有一个所需的尺寸参数 +@Data +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; + } +} + +// 半圆形烤乳酪馅饼:允许指定酱汁是否应该在里面或在外面 +@Data +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; + } +} \ No newline at end of file