建造者(Builder)模式的实际应用与思考

896 阅读7分钟

背景

最近难得有空,将项目中的glide库进行了二次封装,虽然任务是挺简单的,但过程中也引起了我对建造者(Builder)模式的一些思考,故写出来与大家分享一下。 这篇文章将围绕以下几个问题展开(PS:我打算之后的文章都尝试以这样的方式进行):

  1. 什么是建造者模式?
  2. 为什么要用建造者模式?
  3. 建造者模式一般是怎样使用的?
  4. 为什么Builder通常作为Product(即最终构建的对象)的静态内部类?

探索

什么是建造者模式?

这里先引入维基百科的解释:建造模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

具体点来说,我所认为的建造者模式,首先肯定是用于创建一个对象的。那它与平时创建对象有什么区别呢?网上通常把用建造者模式构建对象的过程形容为“将许许多多的零部件拼凑在一起,搭建成一个完整的产品”,我觉得非常贴切。但我觉得应该还要加一个主语:工人,即“工人将许许多多的零部件拼凑在一起,搭建成一个完整的产品”。 no code, no bb。下面用代码来说明:

image.png

上图代码中的Product就是产品。price、color、size、countProduct的几个属性,也就是零部件。

image.png 工人即ProductBuilderProductBuilder跟客户说,他可以buildProductA构建产品A,也可以buildProductB构建产品B,甚至提供自定义套餐buildCustomProductProductBuilder帮我们屏蔽了构建产品的细节,让我们不需要知道这个产品是怎么生产的,拿去用就完事了。

随着工人专业水平的提高,后来甚至衍生出了专门生产A产品的工人以及专门生产B产品的工人。客户需要哪一类工人,就雇佣哪一类工人去干活。

image.png

image.png 那为什么要找工人去构建呢?明明我们自己也能构建啊,为什么还要花这个冤枉钱?

为什么要用建造者模式?

正如上面提到的,明明我们自己也能构造,为什么还非要多此一举呢?当然,并不是所有构造对象的场景都必须得用建造者模式。那为什么,或者说,什么时候用建造者模式比较合理呢?

多处代码创建相同属性的对象

正如上文ProductBuilder提供的buildProductAbuildProductB方法。若代码中有很多个地方都在用相同的重复的代码去构建同种属性的对象,这时候就可以用一个Builder去抽一下。

多处代码创建特定的,不同属性的对象

也拿ProductBuilder举例。ProductBuilder中的三个方法都可以为调用方构建三个类不同属性的Product。若代码中其它地方需要构建A产品、构建B产品,或是其它属性的产品,都可以直接调用。

可以与链式构造法结合,更优雅、更方便地去构建一个对象(重要)

上面的两个原因,其实有些勉强,因为随便写一个工具类都能实现。而与链式构造法结合去构建对象这一特性,才是我认为的,真正能回答“为什么,或者说,什么时候用建造者模式”。至于何为“链式构造法”,这里就不再展开了,感兴趣的同学可以自行百度一下(或者直接看下文应该也能懂了)。

建造者模式一般是怎样使用的?

看了上文,如果没有接触过建造者模式的同学可能就要开喷了:这就是建造者模式?不就是抽个方法包装一下吗?还搞这么麻烦。我随便写个类抽几个方法,不也照样能实现?

是的...没错,如果仅仅只有上面的特性,我们压根就想不起来会去用建造者模式,因为实在跟抽方法没啥区别。那么使用建造者模式的最佳姿势是怎样的呢? 答:与链式构造法结合使用

下面我将以我这次二次封装glide库的过程举例,给大家看一下建造者模式给我们带来的巨大好处。

先简单做个科普:glide库是Android开发者们经常使用的一个图片库,包含了非常实用的功能,例如加载图片、下载图片、图片缓存等,详见github.com/bumptech/gl…

那么既然要封装glide库,最基本的,就是需要提供丰富的加载图片的接口,例如加载普通图片、加载圆角矩形图片、加载圆形图片。这个时候我就想了,这还不简单,不就是三个方法么?于是就有了以下代码:

image.png

再换个场景,图片不仅可以是url的形式,还可以是本地文件,还可以是个Drawable对象。那这个时候,我又需要加代码了...加完代码后是这个样子:

image.png

再加个场景,我需要设置图片的拉伸效果(即scaleType),我就崩溃了...这才是三种属性(形状、图片来源、拉伸效果)的交叉构建,我就已经写不下去了,调用方也是一看一个头大。这个时候,链式构造法+建造者模式的优势终于显示出来了。可以看一下最终我封装完成后,是怎样进行调用的:

image.png

相比之下,是不是既简单又优雅。这就是建造者模式最常见的使用方式了。我们看一下具体是怎样构建的一个对象。

首先第一步,创建builder。ImageLoader.with(context)方法返回的就是一个builder。

image.png

builder有了之后,就需要提出需求了,我们需要构建什么属性的对象。针对图片来说,有形状、图片来源、拉伸效果、尺寸、优先级、缓存配置、滤镜效果等。centerCrop()rectRoundCorner()load()这些方法都是设置好对象的属性。

image.png image.png

设置完属性之后,就可以创建对象了,这里对应的最后一步就是into方法,通用点来说也就是build()方法。从表面上看,这个方法明明没创建对象呀,怎么又说是创建对象呢?因为这里的应用场景只是加载图片,那么就不需要感知到对象的创建。但如果场景是创建一个弹窗,后续还需要控制弹窗的行为,就需要显式返回一个弹窗对象了。

image.png

可以看到,在into方法里,完成了ImageConfig的创建,Builder的任务也就完成了。这就是我所认为的,最实用,也是最常见的,建造者模式的使用方式了。

PS: 对glide二次封装感兴趣的同学可以参考下这个库哈,我也是依葫芦画瓢,哈哈github.com/libin7278/I…

为什么Builder通常作为Product(即最终构建的对象)的静态内部类?

最后在这里引出一个问题,也是我个人比较探究的一个点:为什么Builder通常作为Product(即最终构建的对象)的静态内部类?

我个人的理解:

静态保证了Builder在类加载的时候就进行创建,保证了饿汉式的单例。那只有内部类才能是静态的,所以只能是静态内部类了... 从另一角度看,Builder其实从某种意义上来说就是个Product的构造函数,那么构造函数。当然是跟Product放在一起了。

如果大家知道有其它原因,望不吝指教~

这里顺便提一嘴,因为Builder模式需要保证Builder中的属性字段要跟Product中的属性字段一毛一样,每次增删的时候都特别麻烦,这点我也深有体会。据说lombok插件能有效拯救我们这一痛点,感兴趣的同学可以试一下哈~

文章不足之处,还望大家多多海涵,多多指点,先行谢过~