设计模式—建造者模式及实例(BuilderPattern)
一、介绍
建造者模式(Builder Pattern)也称生成器模式,它属于创建型模式。它的名词解释如下:
separate the construction of a complex object from its representation so that the same construction process can create different representations.
即:将复杂对象的构造与它的表示分开,这样可以在相同的构造过程中创建不同的表示形式。
UML图:
如上图所示,经典的Builder模式包含了四个角色,分别是:
-
产品(Product)角色
由一系列部件组成,一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量。在本类图中,产品类是一个具体的类,而非抽象类。实际编程中,产品类可以是由一个抽象类与它的不同实现组成,也可以是由多个抽象类与他们的实现组成。 -
抽象建造者(Builder)角色
给出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口独立于应用程序的商业逻辑。模式中直接创建产品对象的是具体建造者 (ConcreteBuilder)角色。具体建造者类必须实现这个接口所要求的两种方法:一种是建造方法(buildPart),另一种是返还结构方法(getResult)。一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少零件,就有多少相应的建造方法。
引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回产品。 -
具体建造者(ConcreteBuilder)角色
实现抽象类的所有未实现的方法,具体来说一般是两项任务:组建产品;返回组建好的产品。 -
导演者(Director)角色
负责调用适当的建造者来组建产品,导演类一般不与产品类发生依赖关系,与导演类直接交互的是建造者类。一般来说,导演类被用来封装程序中易变、及其复杂的部分。导演者角色并没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者角色。
通俗地来讲,假如想构建一个对象,通过告诉构建者,自己对这个对象的各种要求,然后建造者根据这些要求进行处理,生成所需要的一个对象。打个比方,我们现在需要一个用户信息对象,里面包含姓名,年龄,性别,身高,体重,职业...等等信息。但我们可能只需要其中的部分信息,如果对象包含的参数很多时,则根据排列组合起来,构造方法将多得不敢想象。而利用建造者模式,则具体化各对象部分的建造,最后一次性返回建造好的用户信息对象。
1. 建造者模式的优点
- 封装性
使用建造者模式可以使客户端不必知道产品内部组成的细节,如例子中我们就不需要关心每一个具体的模型内部是如何实现的,产生的对象类型就是CarModel。
- 建造者独立,容易扩展
Builder之间是相互独立的,与其它的Builder无关,耦合度低,对系统的扩展非常有利。
- 便于控制细节风险
模式所建造的最终产品更易于控制:由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
2. 建造者模式的缺点
- 可能会产生多余的Builder对象以及Director对象;
- 对象的构建过程暴露。
3. 建造者模式的适用场景
- 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式,需要生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序。
- 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
- 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式是非常合适。
- 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程。该种场景,只能是一个补偿方法,因为一个对象不容易获得,而在设计阶段竟然没有发觉,而要通过创建者模式柔化创建过程,本身已经违反设计最初目标。
二、在Android中的应用案例
在Android开发中,常见的有系统对话框AlertDialog、Notification、网络请求开源库Okhttp、图片加载缓存库Glide、Picasso 等都利用了建造者模式。它们的具体源码就不贴了,有兴趣的可自行阅读研究源码。刚好最近在研究Gilde的源码,这里给出Glide 中的Gildebuilder类用以展示:
package com.bumptech.glide;import android.content.Context;import android.os.Build;import com.bumptech.glide.load.DecodeFormat;import com.bumptech.glide.load.engine.Engine;import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter;import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool;import com.bumptech.glide.load.engine.cache.DiskCache;import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;import com.bumptech.glide.load.engine.cache.LruResourceCache;import com.bumptech.glide.load.engine.cache.MemoryCache;import com.bumptech.glide.load.engine.cache.MemorySizeCalculator;import com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor;import java.util.concurrent.ExecutorService;/** * A builder class for setting default structural classes for Glide to use. */public class GlideBuilder { private final Context context; private Engine engine; private BitmapPool bitmapPool; private MemoryCache memoryCache; private ExecutorService sourceService; private ExecutorService diskCacheService; private DecodeFormat decodeFormat; private DiskCache.Factory diskCacheFactory; public GlideBuilder(Context context) { this.context = context.getApplicationContext(); } /** * Sets the {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation to use to store and * retrieve reused {@link android.graphics.Bitmap}s. * * @param bitmapPool The pool to use. * @return This builder. */ public GlideBuilder setBitmapPool(BitmapPool bitmapPool) { this.bitmapPool = bitmapPool; return this; } /** * Sets the {@link com.bumptech.glide.load.engine.cache.MemoryCache} implementation to store * {@link com.bumptech.glide.load.engine.Resource}s that are not currently in use. * * @param memoryCache The cache to use. * @return This builder. */ public GlideBuilder setMemoryCache(MemoryCache memoryCache) { this.memoryCache = memoryCache; return this; } /** * Sets the {@link com.bumptech.glide.load.engine.cache.DiskCache} implementation to use to store * {@link com.bumptech.glide.load.engine.Resource} data and thumbnails. * * @deprecated Creating a disk cache directory on the main thread causes strict mode violations, use * {@link #setDiskCache(com.bumptech.glide.load.engine.cache.DiskCache.Factory)} instead. Scheduled to be removed * in Glide 4.0. * @param diskCache The disk cache to use. * @return This builder. */ @Deprecated public GlideBuilder setDiskCache(final DiskCache diskCache) { return setDiskCache(new DiskCache.Factory() { @Override public DiskCache build() { return diskCache; } }); } /** * Sets the {@link com.bumptech.glide.load.engine.cache.DiskCache.Factory} implementation to use to construct * the {@link com.bumptech.glide.load.engine.cache.DiskCache} to use to store * {@link com.bumptech.glide.load.engine.Resource} data on disk. * * @param diskCacheFactory The disk cche factory to use. * @return This builder. */ public GlideBuilder setDiskCache(DiskCache.Factory diskCacheFactory) { this.diskCacheFactory = diskCacheFactory; return this; } /** * Sets the {@link java.util.concurrent.ExecutorService} implementation to use when retrieving * {@link com.bumptech.glide.load.engine.Resource}s that are not already in the cache. * * <p> * Any implementation must order requests based on their {@link com.bumptech.glide.Priority} for thumbnail * requests to work properly. * </p> * * @see #setDiskCacheService(java.util.concurrent.ExecutorService) * @see com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor * * @param service The ExecutorService to use. * @return This builder. */ public GlideBuilder setResizeService(ExecutorService service) { this.sourceService = service; return this; } /** * Sets the {@link java.util.concurrent.ExecutorService} implementation to use when retrieving * {@link com.bumptech.glide.load.engine.Resource}s that are currently in cache. * * <p> * Any implementation must order requests based on their {@link com.bumptech.glide.Priority} for thumbnail * requests to work properly. * </p> * * @see #setResizeService(java.util.concurrent.ExecutorService) * @see com.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor * * @param service The ExecutorService to use. * @return This builder. */ public GlideBuilder setDiskCacheService(ExecutorService service) { this.diskCacheService = service; return this; } /** * Sets the {@link com.bumptech.glide.load.DecodeFormat} that will be the default format for all the default * decoders that can change the {@link android.graphics.Bitmap.Config} of the {@link android.graphics.Bitmap}s they * decode. * * <p> * Decode format is always a suggestion, not a requirement. See {@link com.bumptech.glide.load.DecodeFormat} for * more details. * </p> * * <p> * If you instantiate and use a custom decoder, it will use * {@link com.bumptech.glide.load.DecodeFormat#DEFAULT} as its default. * </p> * * <p> * Calls to this method are ignored on KitKat and Lollipop. See #301. * </p> * * @param decodeFormat The format to use. * @return This builder. */ public GlideBuilder setDecodeFormat(DecodeFormat decodeFormat) { this.decodeFormat = decodeFormat; return this; } // For testing. GlideBuilder setEngine(Engine engine) { this.engine = engine; return this; } Glide createGlide() { if (sourceService == null) { final int cores = Math.max(1, Runtime.getRuntime().availableProcessors()); sourceService = new FifoPriorityThreadPoolExecutor(cores); } if (diskCacheService == null) { diskCacheService = new FifoPriorityThreadPoolExecutor(1); } MemorySizeCalculator calculator = new MemorySizeCalculator(context); if (bitmapPool == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { int size = calculator.getBitmapPoolSize(); bitmapPool = new LruBitmapPool(size); } else { bitmapPool = new BitmapPoolAdapter(); } } if (memoryCache == null) { memoryCache = new LruResourceCache(calculator.getMemoryCacheSize()); } if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) { engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService); } if (decodeFormat == null) { decodeFormat = DecodeFormat.DEFAULT; } return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat); }}
三、Builder模式变种(链式调用)
平时使用的场景和频率也非常多,我们可能会是这样:
1.通过构造函数的参数形式去写一个实现类
UserInfo(String name);
UserInfo(String name ,String sex);
UserInfo(String name ,String sex, int age);
UserInfo(String name ,String sex, int age, double height);
UserInfo(String name ,String sex, int age, double height, double weight);
2.设置setter和getter方法的形式:
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
在参数不多的情况下,利用构造方法的方式是比较方便快捷的,一旦参数多了,代码可读性大大降低,并且难以维护,对调用者来说也造成一定困惑;
而设置setter和getter可读性不错,也易于维护,但是这样子做对象会产生不一致的状态,当你想要传入全部参数的时候,你必需将所有的setXX方法调用完成之后才行。然而一部分的调用者看到了这个对象后,以为这个对象已经创建完毕,就直接使用了,其实User对象并没有创建完成。因此,利用Bulider模式来创建复杂的对象,就会合适很多。这里我就自己写了个demo,以方便大家更为直观地理解它的思路,GitHub项目地址:Builder模式demo项目
代码如下:
1.定义UserBuilder类:
package com.builderdemo;import android.widget.TextView;/** * TODO<用户信息的建造者> * * @author: 小嵩 * @date: 2017/2/7 16:05 * @version: V1.0 */public class UserInfoBuilder { private String name; //姓名 (必填,在初始化时,传入参数) private String sex; //性别 private int age; //年龄 private double height; //身高CM private double weight; //体重KG public UserInfoBuilder(String name){ this.name = name; } public UserInfoBuilder setSex(String sex){ this.sex = sex; return this; } public UserInfoBuilder setAge(int age){ this.age = age; return this; } public UserInfoBuilder setHeight(double height){ this.height = height; return this; } public UserInfoBuilder setWeight(double weight){ this.weight = weight; return this; } public UserInfoBuilder into(TextView textView){ textView.setText("姓名:"+name+"\n性别:"+sex+"\n年龄:"+age+"\n身高:"+height+"\n体重:"+weight); return this; } public UserInfo create(){ return new UserInfo(name, sex, age, height, weight); }}
2.UserInfo 类:
package com.builderdemo;/** * TODO<用户信息> * * @author: 小嵩 * @date: 2017/2/7 15:32 * @version: V1.0 */public class UserInfo { private String name; //姓名(必填) private String sex; //性别 private int age; //年龄 private double height; //身高CM private double weight; //体重KG public UserInfo(String name, String sex, int age, double height, double weight) { this.name = name; this.sex = sex; this.age = age; this.height = height; this.weight = weight; } @Override public String toString() { return "UserInfo{" + "name='" + name + '\'' + ", sex='" + sex + '\'' + ", age=" + age + ", height=" + height + ", weight=" + weight + '}'; }}
需要注意的是,如果某些参数必须设定,那么我们则可定义一个Builder的构造方法,在初始化Builder的时候,就传入参数进去,像这样:
public class UserInfoBuilder { private String name; //姓名 (必填,在初始化时,传入参数) private String sex; //性别 private int age; //年龄 private double height; //身高CM private double weight; //体重KG public UserInfoBuilder(String name){ this.name = name; } ...
关于链式调用,它的关键其实是在set方法中,return builder对象本身,这样在调用方法时就能返回对象本身,不用打分号继续调用其他方法,像这样:
new UserInfoBuilder("小嵩") .setAge(23) .setSex("男") .setHeight(174) .setWeight(62.5) .into(tv_content);//显示到TextView中而不用每次都这样:
UserInfoBuilder builder = new UserInfoBuilder("小嵩");builder.setAge(23);builder.setSex("男");
四、经典 Builder模式 (简单实例代码)
具体产品类:
package designpatterns.builder;// produce to be builtclass Starbucks { private String size; private String drink; public void setSize(String size) { this.size = size; } public void setDrink(String drink) { this.drink = drink; }}
抽象Builder类:
//abstract builderabstract class StarbucksBuilder { protected Starbucks starbucks; public Starbucks getStarbucks() { return starbucks; } public void createStarbucks() { starbucks = new Starbucks(); System.out.println("a drink is created"); } public abstract void buildSize(); public abstract void buildDrink();}
具体Builder类:
// Concrete Builder to build teaclass TeaBuilder extends StarbucksBuilder { public void buildSize() { starbucks.setSize("large"); System.out.println("build large size"); } public void buildDrink() { starbucks.setDrink("tea"); System.out.println("build tea"); }}
导演者(Director)类:
//director to encapsulate the builderclass Waiter { private StarbucksBuilder starbucksBuilder; public void setStarbucksBuilder(StarbucksBuilder builder) { starbucksBuilder = builder; } public Starbucks getstarbucksDrink() { return starbucksBuilder.getStarbucks(); } public void constructStarbucks() { starbucksBuilder.createStarbucks(); starbucksBuilder.buildDrink(); starbucksBuilder.buildSize(); }}
客户端调用:
//customerpublic class Customer { public static void main(String[] args) { Waiter waiter = new Waiter(); StarbucksBuilder coffeeBuilder = new CoffeeBuilder(); //Alternatively you can use tea builder to build a tea //StarbucksBuilder teaBuilder = new TeaBuilder(); waiter.setStarbucksBuilder(coffeeBuilder); waiter.constructStarbucks(); //get the drink built Starbucks drink = waiter.getstarbucksDrink(); }}
2.http://www.jianshu.com/p/f3cf42416dff
更多相关文章
- Android中Task任务栈的四种模式
- Android全屏模式,沉浸模式。粘性沉浸模式
- android打开文件方法
- ArcGIS for Android 中MapView截图实现方法
- Android:指定分辨率和清晰度的图片压缩方法源码
- Android简单获取经纬度的方法
- Android拨打电话的两种实现方法
- android 基本的画图方法
- Handler 内部类导致的内存泄露修改方法