多参构造器与Builder模式及链式调用的实际运用
Posted 小羊子说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多参构造器与Builder模式及链式调用的实际运用相关的知识,希望对你有一定的参考价值。
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://api.xxx.com/")
.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(); //最后一行不要也能创建成功,好多文章都是说要添加,不要似乎也没有什么影响 不知你怎么看,求解惑。
小结:这种用法,不太主流,团队中使用时需要根据大家的代码使用习惯,好好评估一下功能的正确性和代码的可读性,不同的设计角度解决不同的问题,总之,没有最好,只有适合。
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的使用是怎样的)
以上是关于多参构造器与Builder模式及链式调用的实际运用的主要内容,如果未能解决你的问题,请参考以下文章
建造者模式遇到大数据SparkSession,使链式函数编程再次流行