您的位置:首页 > 产品设计 > UI/UE

设计模式—建造者模式及实例(BuilderPattern)

2017-01-23 17:29 726 查看

一、介绍

      建造者模式(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. 建造者模式的优点

封装性好

      通过建造者模式可以让调用方不必知道产品内部组成的细节,将对象的配置与实现隔离。

容易扩展

      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 built
class 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 builder
abstract 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 tea
class 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 builder
class 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();
}
}


客户端调用:

//customer
public 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();

}
}


参考文献:

1.Java Design Pattern: Builder

2.http://www.jianshu.com/p/f3cf42416dff



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: