1. 前言:


builder 模式的运用第一反应是构造者设计模式的运用,其实也可以用于对象的构造以及优雅的链式调用。


首先,回顾标准的用法

在《Effective Java 第2版》中有提到,遇到多个构造器参数时要考虑使用构建器(Builder模式)。相比于重叠构造器(telescoping constructor)模式JavaBeans模式Builder模式实现的对象更利于使用。

/**
* <pre>
* author : June Yang
* time : 2021/02/06
* desc : Effective Java 中builder的使用
* version: 1.0
* </pre>
*/
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 {
//必须注入的参数,可不用初始化
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 setCalories(int calories) {
this.calories = calories;
return this;
}

public Builder calories(int val) {
this.calories = val;
return this;
}

public Builder carbohydrate(int val) {
this.carbohydrate = val;
return this;
}

public Builder sodium(int val) {
this.sodium = val;
return this;
}

public NutritionFacts build() {
return new NutritionFacts(this);
}
}

@Override
public String toString() {
return "NutritionFacts{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}

//私有构造方法
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}

public static void main(String[] args) {
NutritionFacts bean = new NutritionFacts.
Builder(8, 20)
.calories(30)
.build();
}
}

调用时:

public static void main(String[] args) {
NutritionFacts bean = new NutritionFacts.
Builder(10, 20)
.calories(30)
.build();
}

2. Android项目的中运用

Android开发中经常遇到这样的代码:

如网络请求

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https:///")
.build();

或者 实例化一个对话框

new AlertDialog.Builder(this)
.setTitle("TitleName")
.setMessage("Message")
.setCancelable(true)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
//...
}
})
.show();

builder实现的链式调用看上去如此优雅。于是项目开发中我们也想实际用起来。

3. 常见的两种构建方式

在日常开发中,我们经常需要给某个对象的变量赋值,这个赋值的过程称为对象的构建。

比如现在有个 Person 类,它有几个成员变量:

//固定不变的对象,一般变量需要声明为 final
private final String mName; //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation; //可选
private String mJob; //可选

3.1 常见的构建方式之一:定义多个重载的构造函数

public class PersonOne {
//固定不变的对象,一般变量需要声明为 final
private final String mName; //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation; //可选
private String mJob; //可选

public PersonOne(String name) {
mName = name;
}

public PersonOne(String location, String name) {
mLocation = location;
mName = name;
}

public PersonOne(String name, String location, String job) {
mName = name;
mLocation = location;
mJob = job;
}
}

优点:简单。

缺点

只适用于成员变量少的情况,太多了不容易理解、维护。不优雅。

3.2 常见的构建方式之二:使用 setter 方法挨个构造

public class PersonTwo {
//固定不变的对象,一般变量需要声明为 final
private final String mName; //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation; //可选
private String mJob; //可选

public PersonTwo(String s) {
this.mName = s;
}

public String getName() {
return mName;
}

public String getLocation() {
return mLocation;
}

public void setLocation(String location) {
mLocation = location;
}

public String getJob() {
return mJob;
}

public void setJob(String job) {
mJob = job;
}
}

这种方式也是常见的构造方式,它的好处是:易于阅读,并且可以只对有用的成员变量赋值;

缺点是:


  • 成员变量不可以是 final 类型,失去了不可变对象的很多好处;
  • 对象状态不连续,你必须调用 4 次 setter 方法才能得到一个具备 4 个属性值得变量,在这期间用户可能拿到不完整状态的对象。

而且使用起来也不好看:

PersonTwo personTwo = new PersonTwo("shixin");
personTwo.setJob("Android");
personTwo.setLocation("成都");

如果有 N 个属性岂不是要 personTwo.setXXX N 回?不优雅!

即使把 setXXX 方法返回值改成当前构造类,但还是不满足最重要的缺点的第二点:


用户可能拿到不完整状态的对象。


什么意思呢?

这种方式是 先创建对象、后赋值,用户不知道什么时候拿到的对象是完整的,构建完成的。很有可能你只 set 了一两个属性就返回了,一些必要的属性没有被赋值。

小结:实例化对象时尤其要注意对象的完整性处理,什么时候用builder,什么时候不用要注意区分。

3.3 优雅的构建方式:变种 Builder 模式(回到标准用法上)

为了解决上述两种构建方式,伟大的程序员们创造出了 变种 Builder 模式

经典的 Builder 模式定义为:


将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。


它的重点在于:抽象出对象创建的具体步骤到一个接口,通过调用不同的接口实现,从而得到不同的结果。

Builder 模式在 Android 开发中演变出了 变种 Builder 模式,它除了具备经典构建者模式的功能,还简化了构建的过程,使得创建过程更加简单、直观。

先来看看用 变种 Builder 模式怎么实现上述 Person 对象的构建吧:

public class PersonThree {
//固定不变的对象,一般变量需要声明为 final
private final String mName; //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation; //可选
private String mJob; //可选

/**
* 构造方法的参数是它的 静态内部类,使用静态内部类的变量一一赋值
* @param builder
*/
public PersonThree(Builder builder) {
this.mName = builder.mName;
this.mLocation = builder.mLocation;
this.mJob = builder.mJob;
}

/**
* PersonTree 的静态内部类,成员变量和 PersonTree 的一致
*/
public static class Builder{
private final String mName; //必选,final 类型需要在 构造器中初始化,不允许不初始化它的构造器存在
private String mLocation; //可选
private String mJob; //可选

/**
* 含必选参数的构造方法
* @param name
*/
public Builder(String name) {
mName = name;
}

public Builder setLocation(String location) {
mLocation = location;
return this;
}

public Builder setJob(String job) {
mJob = job;
return this;
}

/**
* 最终构建方法,返回一个 PersonTree 对象,参数是当前 Builder 对象
* @return
*/
public PersonThree build(){
return new PersonThree(this);
}
}
}

可以看到,变种 Builder 模式包括以下内容:


  • 在要构建的类内部创建一个静态内部类 Builder
  • 静态内部类的参数与构建类一致
  • 构建类的构造参数是 静态内部类,使用静态内部类的变量一一赋值给构建类
  • 静态内部类提供参数的 setter 方法,并且返回值是当前 Builder 对象
  • 最终提供一个 build 方法构建一个构建类的对象,参数是当前 Builder 对象

new PersonThree.Builder("xiaoyangzishuo")
.setLocation("ChengDu ")
.setJob("Android Develop")
.build();

变种 Builder 模式目的在于减少对象创建过程中引入的多个构造函数、可选参数以及多个 setter 过度使用导致的不必要的复杂性。

好处就是文章开头所说的:


  • 先赋值,后创建对象(注意)
  • 链式调用相对优雅

最终调用 build() 方法才创建了构建类的对象,保证了状态的完整性。

小结:这是种标准的写法,注意使用场景:有多个参数,需要区分必要参数 和可选参数,并在最后调用的方法上要保证对象的完整性。

tips:

Android Studio 中使用插件自动生成 变种 Builder 模式代码.解决了重复模板代码。

下载插件 Inner Builder 也可以快速实现Builder的构建,代码同上。略。可亲手尝试一下。

4. 链式调用(其他builder的一种变种方式)

class Calculator {
private int total;

public Calculator(int cardinality) {
this.total = cardinality;
}

public Calculator add(int addend) {
this.total += addend;
return this;
}

public Calculator minus(int minus) {
this.total -= minus;
return this;
}

public Calculator multiply(int multiplier) {
this.total *= multiplier;
return this;
}

public Calculator divideBy(int divisor) {
this.total /= divisor;
return this;
}

public Calculator calculate() {
return this;
}

public int getTotal() {
return total;
}
}

将方法的返回值类型指定为当前 Class ,那么,在方法体的最后可以直接 ​​return this​​ ,从而可以形成一个类似于 Builder Patern 的效果。

用法如下:

Calculator calculator = new Calculator(5)
.add(3)
.minus(4)
.multiply(2)
.divideBy(4)
.calculate(); //最后一行不要也能创建成功,好多文章都是说要添加,不要似乎也没有什么影响 不知你怎么看,求解惑。

小结:这种用法,不太主流,团队中使用时需要根据大家的代码使用习惯,好好评估一下功能的正确性和代码的可读性,不同的设计角度解决不同的问题,总之,没有最好,只有适合。

问题:如果需要在对象的赋值的完整性考虑,设计成sdk供业务使用方,是用构造函数来实例化对象保证完整性, 还是可以考虑用这种方式更优雅的赋值来实现,不知你会怎么选,欢迎勾兑~

5. 总结

《Effective Java》值得大家多看一看,有些经典的原则、思想可以作为开发指导原则。不同的人设计方式不同,但是总是遵循既定的经典原则。多角度思考,多运用,多反馈,最后回归再思考。

参考:

以下是从不同的使用角度对这些知识点的学习、补充。

1.《Effective Java》(经典指导原则学习)

​2.《Effective Java》读书笔记02-多参构造器与Builder模式​

​3.变种 Builder 模式:优雅的对象构建方式​

​4.《effective java》builder模式一些思考​

​5. EffectiveJava—Builder设计模式​

​6. 从 Java Builder Pattern 到 return this 链式调用​​(了解return this的使用是怎样的)