本文讲解的Builder模式并非设计模式中定义的经典的Builder模式(即包含指挥者类Director),因为在实际开发和android上的一些知名的开源项目中看到的都是非经典的模式。关于Builder模式的经典模式的定义可参看:https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/builder.html 。
应用场景
- 隔离复杂对象的创建和使用。
- 需要创建的类有复杂的内部结构:表现特征为:这个类包含多种不同签名的构造函数。或包含大量成员属性和对应的setter方法
使用到建造者模式的知名项目分析
上面2种场景是Builder模式出席最多的场景,尤其是第2个场景:当要创建的一个类对象包含复杂的内部结构的时候,那么此时我们可以考虑使用建造者模式来创建这个类。
还记得上次讲解单例模式时提到过android图片加载框架UIL的初始化代码吗?其实就使用了建造者模式。首先来回顾下UIL的官方demo的初始化代码:
public static void initImageLoader(Context context) {
// This configuration tuning is custom. You can tune every option, you may tune some of them,
// or you can create default configuration by
// ImageLoaderConfiguration.createDefault(this);
// method.
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
config.threadPriority(Thread.NORM_PRIORITY - 2);
config.denyCacheImageMultipleSizesInMemory();
config.diskCacheFileNameGenerator(new Md5FileNameGenerator());
config.diskCacheSize(50 * 1024 * 1024); // 50 MiB
config.tasksProcessingOrder(QueueProcessingType.LIFO);
config.writeDebugLogs(); // Remove for release app
// Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(config.build());
}
跟进ImageLoaderConfiguration类看下其具体代码:
public final class ImageLoaderConfiguration {
final Resources resources;
final int maxImageWidthForMemoryCache;
final int maxImageHeightForMemoryCache;
final int maxImageWidthForDiskCache;
final int maxImageHeightForDiskCache;
final BitmapProcessor processorForDiskCache;
final Executor taskExecutor;
final Executor taskExecutorForCachedImages;
final boolean customExecutor;
final boolean customExecutorForCachedImages;
final int threadPoolSize;
final int threadPriority;
final QueueProcessingType tasksProcessingType;
final MemoryCache memoryCache;
final DiskCache diskCache;
final ImageDownloader downloader;
final ImageDecoder decoder;
final DisplayImageOptions defaultDisplayImageOptions;
final ImageDownloader networkDeniedDownloader;
final ImageDownloader slowNetworkDownloader;
private ImageLoaderConfiguration(final Builder builder) {
resources = builder.context.getResources();
maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;
maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;
maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;
maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;
processorForDiskCache = builder.processorForDiskCache;
taskExecutor = builder.taskExecutor;
taskExecutorForCachedImages = builder.taskExecutorForCachedImages;
threadPoolSize = builder.threadPoolSize;
threadPriority = builder.threadPriority;
tasksProcessingType = builder.tasksProcessingType;
diskCache = builder.diskCache;
memoryCache = builder.memoryCache;
defaultDisplayImageOptions = builder.defaultDisplayImageOptions;
downloader = builder.downloader;
decoder = builder.decoder;
customExecutor = builder.customExecutor;
customExecutorForCachedImages = builder.customExecutorForCachedImages;
networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);
slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);
L.writeDebugLogs(builder.writeLogs);
}
public static ImageLoaderConfiguration createDefault(Context context) {
return new Builder(context).build();
}
ImageSize getMaxImageSize() {
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int width = maxImageWidthForMemoryCache;
if (width <= 0) {
width = displayMetrics.widthPixels;
}
int height = maxImageHeightForMemoryCache;
if (height <= 0) {
height = displayMetrics.heightPixels;
}
return new ImageSize(width, height);
}
/**
* Builder for {@link ImageLoaderConfiguration}
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
*/
public static class Builder {
private static final String WARNING_OVERLAP_DISK_CACHE_PARAMS = "diskCache(), diskCacheSize() and diskCacheFileCount calls overlap each other";
private static final String WARNING_OVERLAP_DISK_CACHE_NAME_GENERATOR = "diskCache() and diskCacheFileNameGenerator() calls overlap each other";
private static final String WARNING_OVERLAP_MEMORY_CACHE = "memoryCache() and memoryCacheSize() calls overlap each other";
private static final String WARNING_OVERLAP_EXECUTOR = "threadPoolSize(), threadPriority() and tasksProcessingOrder() calls "
+ "can overlap taskExecutor() and taskExecutorForCachedImages() calls.";
/** {@value} */
public static final int DEFAULT_THREAD_POOL_SIZE = 3;
/** {@value} */
public static final int DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 2;
/** {@value} */
public static final QueueProcessingType DEFAULT_TASK_PROCESSING_TYPE = QueueProcessingType.FIFO;
private Context context;
private int maxImageWidthForMemoryCache = 0;
private int maxImageHeightForMemoryCache = 0;
private int maxImageWidthForDiskCache = 0;
private int maxImageHeightForDiskCache = 0;
private BitmapProcessor processorForDiskCache = null;
private Executor taskExecutor = null;
private Executor taskExecutorForCachedImages = null;
private boolean customExecutor = false;
private boolean customExecutorForCachedImages = false;
private int threadPoolSize = DEFAULT_THREAD_POOL_SIZE;
private int threadPriority = DEFAULT_THREAD_PRIORITY;
private boolean denyCacheImageMultipleSizesInMemory = false;
private QueueProcessingType tasksProcessingType = DEFAULT_TASK_PROCESSING_TYPE;
private int memoryCacheSize = 0;
private long diskCacheSize = 0;
private int diskCacheFileCount = 0;
private MemoryCache memoryCache = null;
private DiskCache diskCache = null;
private FileNameGenerator diskCacheFileNameGenerator = null;
private ImageDownloader downloader = null;
private ImageDecoder decoder;
private DisplayImageOptions defaultDisplayImageOptions = null;
private boolean writeLogs = false;
public Builder(Context context) {
this.context = context.getApplicationContext();
}
public Builder memoryCacheExtraOptions(int maxImageWidthForMemoryCache, int maxImageHeightForMemoryCache) {
this.maxImageWidthForMemoryCache = maxImageWidthForMemoryCache;
this.maxImageHeightForMemoryCache = maxImageHeightForMemoryCache;
return this;
}
public Builder diskCacheExtraOptions(int maxImageWidthForDiskCache, int maxImageHeightForDiskCache,
BitmapProcessor processorForDiskCache) {
this.maxImageWidthForDiskCache = maxImageWidthForDiskCache;
this.maxImageHeightForDiskCache = maxImageHeightForDiskCache;
this.processorForDiskCache = processorForDiskCache;
return this;
}
public Builder taskExecutor(Executor executor) {
if (threadPoolSize != DEFAULT_THREAD_POOL_SIZE || threadPriority != DEFAULT_THREAD_PRIORITY || tasksProcessingType != DEFAULT_TASK_PROCESSING_TYPE) {
L.w(WARNING_OVERLAP_EXECUTOR);
}
this.taskExecutor = executor;
return this;
}
public Builder taskExecutorForCachedImages(Executor executorForCachedImages) {
if (threadPoolSize != DEFAULT_THREAD_POOL_SIZE || threadPriority != DEFAULT_THREAD_PRIORITY || tasksProcessingType != DEFAULT_TASK_PROCESSING_TYPE) {
L.w(WARNING_OVERLAP_EXECUTOR);
}
this.taskExecutorForCachedImages = executorForCachedImages;
return this;
}
public Builder threadPoolSize(int threadPoolSize) {
if (taskExecutor != null || taskExecutorForCachedImages != null) {
L.w(WARNING_OVERLAP_EXECUTOR);
}
this.threadPoolSize = threadPoolSize;
return this;
}
// 省略很多其它方法...
/** Builds configured {@link ImageLoaderConfiguration} object */
public ImageLoaderConfiguration build() {
initEmptyFieldsWithDefaultValues();
return new ImageLoaderConfiguration(this);
}
}
}
上面是简化版的,省略了很多方法定义,但是成员属性部分都保留下来了。完整的可以看官方代码:https://github.com/nostra13/Android-Universal-Image-Loader/blob/4c728792d73e964f6d8501f742cda57dd56731c6/library/src/main/java/com/nostra13/universalimageloader/core/ImageLoaderConfiguration.java
从上面的代码结构可以看出ImageLoaderConfiguration类具备如下特征:
- ImageLoaderConfiguration类的构造函数是私有的,意味着外部无法直接创建该类的实例
- 包含大量的成员属性,这些属性都定义为了final
- 包含一个内部类Builder,且被public statc修饰,该内部内几乎重复包含了其外部类的绝大多数成员属性
- Builder类的成员变量中与外部类成员一一对应的部分声明为private,且为其提供了setter方法(广义的setter,不一定是setXX这种形式,但其作用是用来设置成员属性的值)
- Builder内部类类中setter方法的返回值是Builder对象本身
- Builder内部类包含了一个build方法,在该方法中返回其外部类的实例对象。
上面这些特征就是Builder模式实现的模板。从上面特征中的第2条可以看到,ImageLoaderConfiguration类使用Builder模式满足的是应用场景中提到的第2条。即要创建的对象具备复杂的内部结构,也就是包含很多成员属性。在创建该对象的时候我们需要为其设置大量的属性。传统的做法就是直接在该类中提供大量的setter方法,先创建对象,然后调用对象的setter方法来完成这些属性的设置。类似下面的代码:
public class People {
private String name;
private String age;
private String height;
private String weight;
private String houseAddress;
private String company;
private String department;
public void setName(String name) {
this.name = name;
}
public void setAge(String age) {
this.age = age;
}
public void setHeight(String height) {
this.height = height;
}
public void setWeight(String weight) {
this.weight = weight;
}
public void setHouseAddress(String houseAddress) {
this.houseAddress = houseAddress;
}
public void setCompany(String company) {
this.company = company;
}
public void setDepartment(String department) {
this.department = department;
}
//省略setter对应的getter方法
public void speech(){
}
public void writeCode(){
}
//省略很多其他功能函数
}
传统的做法,当我们要描述一个People对象的时候,首先创建一个People对象实例,然后通过大量的setter方法来为其设置属性。比如name,age等,最后才是去真正使用People类的功能函数,比如演讲speech(),写代码writeCode函数等。这么做一个非常不好的地方就是将对象的构建和使用混到了一起。很明显name,age这些属性是在创建一个People对象的时候就应该具备的,我们真正希望调用的是People的功能函数speech和writeCode。但是这么做为了使用功能函数我们在创建了People对象之后还需要先调用大量的非功能函数setter函数来设置其属性。另外一个不好的点就是通过setter方法来设置属性,意味着在程序运行过程中People对象的name,age等信息可以被外部修改。而实际上一旦People对象产生其姓名和年龄理应该是不可更改的。也就是说无法保证运行期间某些成员的不变性。那么现在反过来看前面提到的UIL开源项目中对ImageLoaderConfiguration类的结构的定义就很清楚其为何要使用Builder模式以及使用其作用了。
首先ImageLoaderConfiguration类包含了大量的非功能属性(实际上这也是配置类的必然特性,比如安卓对话框也包含很多属性可以设置),如果使用传统的setter方法,那么会包含大量的非功能函数。所以我们可以考虑将这些属性的设置函数和功能函数分离开来,比如将这些属性设置函数放到其内部的一个内部类Builder类中,由Builder提供这些属性的设置和ImageLoaderConfiguration类的创建,而ImageLoaderConfiguration只需提供真正的功能函数和一些必要的getter函数。这样一个很大的好处就是将对象的构建和表示相分离,使得类本身对外的API接口将更加简洁,尤其是如果该类是直接暴露给用户操作的门面类时更加明显(在UIL中门面类是ImageLoader类),同时如果为了保证成员属性在对象创建之后的不可变性,可以将外部类中的成员都定义为final,只允许获取,不允许修改。修改必须通过Builder类在构建时修改。另外为了使得Builder类中的setter方法可以链式调用,setter方法都返回Builder对象本身,即return this。
使用Builder模式改写上面的People类
了解了Builder模式的规则之后,我们按照这些规则来将上面的People类改为使用Builder模式的代码:
public class People {
//为了保证属性只能在创建时设置,不能被外部修改,将其声明为final,且去除对应的setter方法
private final String name;
private final String age;
private final String height;
private final String weight;
private final String houseAddress;
private final String company;
private final String department;
public String getName() {
return name;
}
public String getAge() {
return age;
}
public String getHeight() {
return height;
}
public String getWeight() {
return weight;
}
public String getHouseAddress() {
return houseAddress;
}
public String getCompany() {
return company;
}
public String getDepartment() {
return department;
}
public void speech() {
}
public void writeCode() {
}
//构造函数依赖Builder对象
private People(final Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
this.houseAddress = builder.houseAddress;
this.company = builder.company;
this.department = builder.department;
}
//省略很多其他功能函数
//Builder内部类必须声明为public static的
public static class Builder {
private String name;
private String age;
private String height;
private String weight;
private String houseAddress;
private String company;
private String department;
//为了可链式调用,setter方法返回Builder对象本身
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setAge(String age) {
this.age = age;
return this;
}
public Builder setHeight(String height) {
this.height = height;
return this;
}
public Builder setWeight(String weight) {
this.weight = weight;
return this;
}
public Builder setHouseAddress(String houseAddress) {
this.houseAddress = houseAddress;
return this;
}
public Builder setCompany(String company) {
this.company = company;
return this;
}
public Builder setDepartment(String department) {
this.department = department;
return this;
}
People build() { //在build方法中创建外部类People实例
return new People(this);
}
}
}
改写需要遵循的Builder模式的规则在上面注释中解释的很详细,总体需要遵循以下规则
- 外部类People构造函数私有,且其构造函数依赖Builder对象
- 外部类People中不希望在运行时被修改的属性设置为final,且对这些属性只提供getter方法,不提供setter方法
- Builder内部类必须声明为public static的
- Builder内部类只提供setter方法,如果为了链式调用,setter方法返回Builder对象本身,即return this;
- 在Builder类的build方法中创建外部类People实例,然后将其返回
这样使用上就非常清晰明了了,如下所示:
People people = new People.Builder()
.setName("小明")
.setAge("23")
.setHeight("170")
.setHeight("57")
.build();
//对象创建之后就不需要调用setter方法了。
people.speech();
使用建造者模式的好处
- 通过前面的示例很容易看出来,使用建造者模式的一个优点就是对象的构建和表示相分离,尤其是当构建的对象包含大量的非功能属性的时候,比如安卓对话框,将对象的创建单独放到一个类中去维护,能够使得对象对外的功能API更加清晰,简洁。
- 将对象的成员属性设置为 final的,由于不对外提供setter方法,那么外部只能在使用Builder创建该对象的时候设置该属性,能够确保程序在运行期间属性状态不会被修改。
文章评论