天问

  • Blog
  • 热修复与动态加载
  • python爬虫
  • 专栏
    • 热修复与动态加载
    • NDK开发
    • 设计模式与架构
    • React Native
  • 好文转载
    • 计算机技术
    • 投资理财
    • 摄影与拍照技巧
  • 投稿
  • 关于本站及隐私政策
  • Github
StevenQ
为今天工作,为明天投资,为未来孵化一些东西!
  1. 首页
  2. 计算机技术
  3. 设计模式
  4. 正文

【设计模式之】建造者模式

2018年11月24日 395点热度 1人点赞 0条评论

本文讲解的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();

使用建造者模式的好处

  1. 通过前面的示例很容易看出来,使用建造者模式的一个优点就是对象的构建和表示相分离,尤其是当构建的对象包含大量的非功能属性的时候,比如安卓对话框,将对象的创建单独放到一个类中去维护,能够使得对象对外的功能API更加清晰,简洁。
  2. 将对象的成员属性设置为 final的,由于不对外提供setter方法,那么外部只能在使用Builder创建该对象的时候设置该属性,能够确保程序在运行期间属性状态不会被修改。
本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 设计模式
最后更新:2018年11月24日

天问

追梦追自由,穷则独善其身,达则兼济天下

打赏 点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2022 天问 ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang