概念: 上一篇记录的是设计模式中的工厂模式(https://blog.csdn.net/ysq_chris/article/details/88350990),和工厂模式一样,建造者模式也是对象创建型模式,可以创建一些复杂的对象。看过一些介绍后很多时候会有一个疑问,建造者模式和工厂模式有什么不一样,适用的场景有不同吗?
开发过Android的同学都应该知道,Android中的对话框(eg:AlertDialog)的封装就是用的建造者模式。经过对其源码的分析我有了一些自己的理解。
下面的文章包括以下内容:
- 沿用之前在工厂模式介绍中用到的车票的例子进行建造者模式的应用场景的分析。
- 通过AlertDialog源码的解析去分析其中的建造者模式的应用和变化。
- 总结一下和工厂模式的个人理解的异同。
1. 常规建造者模式:
常规的建造者模式包含的四个主要部分:
1.产品 Product:也就是我们要建造的对象,这个对象一般是不太方便直接new出来的。为什么呢? 例如建造的这个对象有很多属性参数,这些属性需要进行初始化,需要设置这些参数。
直接想到的方式有两种:
1.直接通过构造函数传入.(缺点:如果参数很多或者并不是所有的参数都是必须的时候就会看到一些很长的构造函数,裹脚布一样,常常发现传错了参数,或者是一大堆的构造函数,看半天不知道用哪个)
2.通过set函数一个一个设置。(直接暴露了这个产品对象的很多细节)
遇到需要构建复杂的对象的时最好时进行一些用户不用关注的细节,并且提供足够清晰的建造方式和路径。这是为什么要使用建造者模式构建复杂对象的原因。
2.抽象建造者或者接口 Builder:定义需要构建的产品的参数设置方法,并且最终构建产品对象的方法。
3.具体建造者 ConcreteBuilder:实现上面的构建类。
4.指挥者 Director : 所谓的指挥者,就是一个管理者的角色,在有些对象中属性之间时有依赖的,所以会有建造的先后次序的问题,这个类就对具体的组装过程进行了管理和限定(在某些建造者模式中时没有这个类的,或者说是这个过程在建造者 ConcreteBuilder 进行最后构建的时候可以提供方法完成 AlertDialog就是这样实现的)
建造者模式需要的角色已经梳理清楚了,现在就来用代码编程实例来进行阐述,还是用我上篇文章的车票的例子,只是这里不再是构建汽车、火车、飞机等不同类型的票,而是如何去构建具体的一种票,如果用户想要买火车票,那么要知道的火车票的信息还是蛮多的。比如:站点,出发到达的时间、票价、站台号,车次信息… 这就是一个相对蛮复杂的对象。
下面首先定义这个火车票吧(产品角色) 这个类还是继承自Ticket这个票类型:
public class TrainTicket implements Ticket{ //火车票
private int price; //票价
private String startTime;//出发到达的时间
private String arriveTime;
private String platformInfo;//车次信息
private int trainNumber;//站台号
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getArriveTime() {
return arriveTime;
}
public void setArriveTime(String arriveTime) {
this.arriveTime = arriveTime;
}
public String getPlatformInfo() {
return platformInfo;
}
public void setPlatformInfo(String platformInfo) {
this.platformInfo = platformInfo;
}
public int getTrainNumber() {
return trainNumber;
}
public void setTrainNumber(int trainNumber) {
this.trainNumber = trainNumber;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
接下来定义一个构建接口,需要提供设置这些产品参数的方法
public interface TicketBuilder {
public void setStartTime(String startTime);
public void setArriveTime(String arriveTime) ;
public void setPlatformInfo(String platformInfo);
public void setTrainNumber(int trainNumber) ;
public void setPrice(int price) ;
}
接下来就是直接实现构建类,构建出包含具体火车票信息的对象。
public class TrainTicketBuilder implements TicketBuilder{
private int price;
private String startTime;
private String arriveTime;
private String platformInfo;
private int trainNumber;
@Override
public void setStartTime(String startTime) {
this.startTime = startTime;
}
@Override
public void setArriveTime(String arriveTime) {
this.arriveTime = arriveTime;
}
@Override
public void setPlatformInfo(String platformInfo) {
this.platformInfo = platformInfo;
}
@Override
public void setTrainNumber(int trainNumber) {
this.trainNumber = trainNumber;
}
@Override
public void setPrice(int price) {
this.price = price;
}
}
public TrainTicket build(){
TrainTicket trainTicket = new TrainTicket();
trainTicket.setPlatformInfo(platformInfo);
trainTicket.setArriveTime(arriveTime);
trainTicket.setStartTime(startTime);
trainTicket.setTrainNumber(trainNumber);
trainTicket.setPrice(price);
return trainTicket;
}
最后是创建一个指挥者,管理最后如何创建出这个车票对象
public class TrainTicketDirector {
public TrainTicket build(Builder builder){
builder.setPlatformInfo("2");
builder.setArriveTime("10:00");
builder.setStartTime("8:00");
builder.setTrainNumber(131);
builder.setPrice(88);
return builder.builder(); // 返回一个具体的票类
}
}
通过这样的封装之后,在调用的地方直接调用TrainTicketDirector.build(),方法就能创建一个票类。上面的代码有一个问题,就是最后的TrainTicketDirector 类把信息写死了,这样的话,如果换了票的信息就要重新改一遍,不合理。对于这些变化的参数怎么办? 在build()方法里面作为参数传进去吗?如果还是一样的有几十个参数不就又回到最初“裹脚布”的问题了吗?这个时候我们就可以采用链式构建的方法。我们不再使用到指挥者的角色。我们修改一下TrainTicketBuilder 的代码:
public class TrainTicketBuilder implements TicketBuilder{
private int price;
private String startTime;
private String arriveTime;
private String platformInfo;
private int trainNumber;
@Override
public TrainTicketBuilder setStartTime(String startTime) {
this.startTime = startTime;
return this;
}
@Override
public TrainTicketBuilder setArriveTime(String arriveTime) {
this.arriveTime = arriveTime;
return this;
}
@Override
public TrainTicketBuilder setPlatformInfo(String platformInfo) {
this.platformInfo = platformInfo;
return this;
}
@Override
public TrainTicketBuilder setTrainNumber(int trainNumber) {
this.trainNumber = trainNumber;
return this;
}
@Override
public TrainTicketBuilder setPrice(int price) {
this.price = price;
return this;
}
public TrainTicket build(){
TrainTicket trainTicket = new TrainTicket();
trainTicket.setPlatformInfo(platformInfo);
trainTicket.setArriveTime(arriveTime);
trainTicket.setStartTime(startTime);
trainTicket.setTrainNumber(trainNumber);
trainTicket.setPrice(price);
return trainTicket;
}
}
客户端调用的方式:
TrainTicketBuilder ticketBuilder = new TrainTicketBuilder().
setArriveTime("10:00").
setPlatformInfo("1").
setTrainNumber(100);
TrainTicket trainTicket = ticketBuilder.build();
这样的调用就很清晰明了,并且代码也会简洁很多。
以上例子就基本用一个买票的例子把建造者常规模式说清楚了。其实在实际使用的时候更多的也确实是这种链式的调用构建。
下面就进入源码看看 源码里面使用建造者模式的经典案例
2.AlertDialog源码的解析
首先直接看实例化AlertDialog的代码:
new AlertDialog.Builder(self)
.setTitle("你好")
.setMessage("确定要离开吗?")
.setPositiveButton("是", null)
.setNegativeButton("否", null)
.show();
和我们上面我们写的链式调用差不多,简洁明了。
进入到AlertDialog看这个类的代码
// 继承Dialog 基类 和 DialogInterface接口 ,这个接口主要是一些监听
public class AlertDialog extends Dialog implements DialogInterface {
private AlertController mAlert; // 其他的属性先不管,记住这个,后面会解释
new AlertDialog.Builder 的 Builder是AlertDialog的一个静态内部类,其他的忽略先
可以看到Builder中有一个对象是AlertController.AlertParams
public static class Builder {
private final AlertController.AlertParams P;
Builder中最多的就是下面这样的set代码,把属性设置到对象P上,并且返回this本身。
public Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
那我们就继续看看 AlertController.AlertParams 这个。
public static class AlertParams {
public final Context mContext;
public final LayoutInflater mInflater;
public int mIconId = 0;
public Drawable mIcon;
public int mIconAttrId = 0;
public CharSequence mTitle;
public View mCustomTitleView;
public CharSequence mMessage;
public CharSequence mPositiveButtonText;
...
}
可以看到这个类也是一个静态类,其中定义了很多属性,这些属性都是Dialog需要的。相当于这个是一个缓存类,把这些设置到builder的属性统一缓存起来。这个AlertParams 和Builder合起来就像是我们上面写的TrainTicketBuilder ,只是那里把设置的方法和属性写在一个类里,这里为了更好的扩展(其他的弹窗也可以用到这个类进行属性保存)和让代码更清晰,就分开两个类写了。
回到Builder,看看create()这个方法:
/**
* Creates an {@link AlertDialog} with the arguments supplied to this
* builder.
* <p>
* Calling this method does not display the dialog. If no additional
* processing is needed, {@link #show()} may be called instead to both
* create and display the dialog.
*/
public AlertDialog create() {
// Context has already been wrapped with the appropriate theme.
final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
P.apply(dialog.mAlert); // 注意这行代码
dialog.setCancelable(P.mCancelable);
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
这样就构建了一个dialog,那这个dialog的属性怎么传进去的呢, P.apply(dialog.mAlert); 就是把p保存的属性一一设置到dialog上了的。把简化的代码贴一下吧,下面这个代码就是apply的代码,结果就是这样进行了属性的赋值。
public void apply(AlertController dialog) {
if (mCustomTitleView != null) {
dialog.setCustomTitle(mCustomTitleView);
} else {
if (mTitle != null) {
dialog.setTitle(mTitle);
}
if (mIcon != null) {
dialog.setIcon(mIcon);
}
if (mIconId >= 0) {
dialog.setIcon(mIconId);
}
if (mIconAttrId > 0) {
dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
}
}
if (mMessage != null) {
dialog.setMessage(mMessage);
}
...
}
至此,AlertDialog的构建就清晰了,其中内部的构建再复杂也和外部调用者没有关系,外部调用者只要根据自己的需要把一个一个值或者对象,传到builder进行构建就好了,这样链式的调用简单清晰。至此建造者模式就讲完了。其实回过头去看,好像并没有什么特别新奇的地方,确实是这样的,有了编程语言的基础之后回过头看源码还是蛮清晰的,但是为什么很多时候看其他的代码就很痛苦呢,其实就是这些设计模式的使用,很多时候我们觉得使用设计模式优点大材小用,随便new个对象简单快捷,根本没必要用这些所谓的设计模式,搞得似乎是在装逼。其实还真不是,看了源码之后的体会更深了。
至于和工厂方式的区别,是不是可以从一个是构建不同的票类,一个是构建具体的票信息就有所体悟呢。
欢迎吐槽指正,一起武装前进。