设计模式系列文章目录导读:
设计模式 ~ 面向对象 6 大设计原则剖析与实战
设计模式 ~ 模板方法模式分析与实战
设计模式 ~ 观察者模式分析与实战
设计模式 ~ 单例模式分析与实战
设计模式 ~ 深入理解建造者模式与实战
设计模式 ~ 工厂模式剖析与实战
设计模式 ~ 适配器模式分析与实战
设计模式 ~ 装饰模式探究
设计模式 ~ 深入理解代理模式
设计模式 ~ 小结
建造者模式的定义
今天我们来介绍一个开发中也是很常见的设计模式:建造者模式(Builder Pattern)
建造者模式的定义:将一个复杂对象的构建与它的表示分离,使得同样的构造过程可以创建不同的表示
建造者模式主要由以下几个角色组成:
-
产品(Product)
该角色就是建造者要建造的复杂对象
-
抽象建造者(Builder)
用于规范产品的各个组成部分,并进行抽象
-
具体建造者(Concrete Builder)
实现抽象建造者抽象方法
-
导演(Director)
负责建造者构造产品组成部分的顺序,先构造哪部分,然后构造哪部分,最后调用建造者的建造方法
建造者模式的通用类图:
建造者模式的实现
通过上面介绍的建造者模式的类图,我们看下用代码如何实现一个建造者模式
产品类
// Product 由3部分组成: Part1/Part2/Part3
public class Product {
private Part1 part1;
private Part2 part2;
private Part3 part3;
public Product(Part1 part1, Part2 part2, Part3 part3) {
this.part1 = part1;
this.part2 = part2;
this.part3 = part3;
}
}
抽象建造者
public interface IBuilder {
void createPart1();
void createPart2();
void createPart3();
Product composite();
}
具体建造者
public class ProductBuilder implements IBuilder {
private Part1 part1;
private Part2 part2;
private Part3 part3;
@Override
public void createPart1() {
part1 = new Part1();
}
@Override
public void createPart2() {
part2 = new Part2();
}
@Override
public void createPart3() {
part3 = new Part3();
}
@Override
public Product composite() {
return new Product(part1, part2, part3);
}
}
导演类
public class Director {
private IBuilder builder;
public Director(IBuilder builder) {
this.builder = builder;
}
public Product build() {
builder.createPart1();
builder.createPart2();
builder.createPart3();
return builder.composite();
}
public static void main(String[] args) {
IBuilder builder = new ProductBuilder();
Director director = new Director(builder);
Product product = director.build();
System.out.println(product);
}
}
IBuilder
定义了创建产品 Product
的流程,ProductBuilder
为具体的建造者实现具体的创建流程,Director
是对建造者的封装,负责控制建造者创建产品组成部分的顺序
读者可能会有疑问,我们平时看到的建造者模式好像没有这么复杂,如下实例所示:
// 产品类
public class User {
String username;
int age;
public User(String username,int age) {
this.username = username;
this.age = age;
}
}
// 建造者类
class UserBuilder {
private String username;
private int age;
public UserBuilder setAge(int age) {
this.age = age;
return this;
}
public UserBuilder setUsername(String username) {
this.username = username;
return this;
}
public User build() {
return new User(username, age);
}
}
// 测试
class Client {
public static void main(String[] arg) {
User user = new UserBuilder()
.setUsername("Chiclaim")
.setAge(18)
.build();
System.out.print(user);
}
}
核心就两个类: 产品类
、建造者
,没有 抽象建造者
,也没有 导演类
。
其实上面根据建造者类图实现的建造者模式是最具有高度抽象的建造者模式,也就是一般通用的建造者模式的定义。
但是实际开发中大多数情况不需要这样的抽象建造者,所以上面的 UserBuilder
并没有抽象建造者。
上面的 User
使用 建造者模式
的时候,也没有 导演类
,这属于对传统 建造者模式
一种进化或者改造,将 导演类
的功能移到了 建造者
角色上了
从上面的 UserBuilder
就可以看出,对 “产品”
组成部分的构造的先后顺序放到了 UserBuilder
上了,上面的例子是先构造 User
的 username
,然后构造 age
,最后建造整个 User
对象。
进化改造后的建造者模式使用起来更加方便,如:
-
Android AlertDialog
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.dialog_title) .setMessage(R.string.dialog_fire_missiles) .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); AlertDialog dialog = builder.create();
-
Java8 Calendar 类
在
Java8
之前使用Calendar
分两步:通过静态工厂方法获取对象;然后分步设置参数:// 静态工厂方法 public static Calendar getInstance() public static Calendar getInstance(TimeZone zone) public static Calendar getInstance(Locale aLocale) public static Calendar getInstance(TimeZone zone, Locale aLocale) // 获取对象后设置相关参数 Calendar calendar = Calendar.getInstance(); calendar.setTimeZone(xxx); calendar.setTime(xxx); calendar.set(field,value);
从
Java8
开始新增了Builder
构建对象的方式,这样就避免了先获取对象,然后设置参数了:Calendar calendar = new Calendar.Builder() .setTimeZone(xxx) .setLocale(xxx) .set(field,value) .build();
-
OkHttp 框架
Request request = new Request.Builder() .url(url) .post(body) .build();
在
OkHttp
中也对传统的Builder
做了一定的改造如:Request request = new Request.Builder() .url(url) .post(body) .build() // 创建对象 .newBuilder() // 创建新的Builder(保留之前的参数) .addHeader("key","value") .build(); // 创建对象
-
Dagger2 框架
CoffeeShop coffeeShop = DaggerCoffeeShop.builder() .dripCoffeeModule(new DripCoffeeModule()) .build();
将来是否还有其他的建造者模式的变化也不好说,总的一点就是建造模式是对象构建过程与表示的分离,在这个根本原则下,具体的实现形式并不是一成不变的,掌握其基本的实现原理,变种也是很容易理解的。
建造者模式还是构造方法重载
下面我们来看下《Effective Java》关于创建对象的论述,笔者觉得会你让你对建造者模式有更加深刻的理解。
《Effective Java》在 Java 的地位就不用多说了,该书作者是 Java 集合框架的作者,也是《Java并发编程实战》的作者
遇到多个构造器参数是要考虑使用建造者模式
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到 大量
的 可选参数
以《Effective Java》的代码示例说明问题,这个是一个表示 营养成分
的类,先以构造方法重载的方式实现(书上称之为重叠构造器, telescoping constructor)
public class NutritionFacts {
private final int servingSize; // required
private final int servings; // required
private final int calories; // optional
private final int fat; // optional
private final int sodium; // optional
private final int carbohydrate; // optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
从代码中可以看出,有 4 个属性是可选的,2 个属性是必选的,我们构造一个对象如:
NutritionFacts facts = new NutritionFacts(240,8,100,0,35,27);
看上是没有啥问题,但是由于有许多属性是可选的,要么我们新建更多的重叠构造函数(上面的构造函数还不够),要么设置一些我们原本不想设置的参数
除了这个问题外,如果参数还会增加,那么这个类将失去了控制,需要更多的构造函数,或者设置一些原本不需要设置的可选参数,如果可选参数比较多,对使用者更加不友好
当然,除了 重叠构造函数
,还有 JavaBeans
模式,简单来首就是通过 setter
来设置参数,这样就避免了非常多的构造函数和设置可选的参数,代码如下所示:
public class NutritionFacts {
private int servingSize; // required
private int servings; // required
private int calories; // optional
private int fat; // optional
private int sodium; // optional
private int carbohydrate; // optional
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
构建实例也很简单:
NutritionFacts2 facts = new NutritionFacts2();
facts.setServingSize(240);
facts.setServings(8);
facts.setCalories(100);
facts.setSodium(35);
facts.setCarbohydrate(27);
但是 JavaBeans
模式自身有着严重的不足,因为构造过程被分到几个调用中,在构造过程中,JavaBean
可能处于不一致状态,除此以外还有另一个不足,JavaBean
模式阻止了把类做成不可变的可能,需要开发中额外付出努力保证它的线程安全。
这个建造者模式就闪亮登场了,它既能保证像重叠构造器一样的安全性,也能保证像 JavaBeans
模式那样的可读性:
public class NutritionFacts {
private final int servingSize; // required
private final int servings; // required
private final int calories; // optional
private final int fat; // optional
private final int sodium; // optional
private final int carbohydrate; // optional
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
public static class Builder {
private final int servingSize; // required
private final int servings; // required
private int calories; // optional
private int fat; // optional
private int sodium; // optional
private int carbohydrate; // optional
// 必须参数
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder setCalories(int calories){
this.calories = calories;
return this;
}
public Builder setFat(int fat){
this.fat = fat;
return this;
}
public Builder setSodium(int sodium){
this.sodium = sodium;
return this;
}
public Builder setCarbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
}
构建实例也很简单:
NutritionFacts facts = new NutritionFacts.Builder(240, 8)
.setCalories(100)
.setSodium(35)
.setCarbohydrate(27)
.build();
可以看出只有当调用 build
方法的时候才会真正的创建对象,从而保证对象的一致性。而不是像 JavaBeans
模式那样先创建对象,然后分步设置参数。
还可以对 builder
里的参数进行校验,可以在 build
方法里进行校验,如果违反了约束条件,build
方法可以抛出 IllegalStateException
异常。
还可以利用单个建造器创建多个对象,可以 build 方法中,为某些参数设置默认值,builder 的参数可以在创建对象期间进行调整,也可以随着不同的对象而改变:
public Man buildMan() {
if (this.age < 0) {
throw IllegalStateException("年龄不能小于0");
}
this.gender = MALE;
return new Man(this);
}
public Man buildWomen() {
if (this.age < 0) {
throw IllegalStateException("年龄不能小于0");
}
this.gender = FEMALE;
return new Man(this);
}
简而言之,如果类的构造函数或静态工厂具有很多参数(4个或以上),特别是有许多参数是可选的时候,建造者模式就是一个很好的选择。它比传统的重叠构造器相比,建造器模式的客户端代码更容易阅读和编写,建造者模式也比 JavaBeans
模式更加安全
通过上面的代码看出,《Effective Java》的建造者模式案例更加严谨:
- 产品类的构造方法设置为
private
,只能通过建造器构造对象 - Builder 类作为产品的
静态内部类
,内聚性更好 - 可以将
必要参数
作为 Builder 的构造方法的参数
使用建造者模式还是静态工厂模式?
在实际开发中一般都需要请求网络,请求接口的时候我们一般都会将参数封装到一个对象里面去,但是有的时候并不是所有的参数都是需要的,这个时候是用建造模式还是简单工厂模式
如果是网络请求参数的封装 bean
,个人还是建议使用简单工厂模式,原因如下:
-
工作量相对大了一些
因为要实现建造者模式相对来说还是挺烦的,要实现建造过程和表示的分类需要额外维护一个类,项目中的网络请求是非常多的,使用建造者模式在一定程度加大了工作量
-
网络请求的参数一般都是需要的
使用建造者模式,一般是参数比较多的情况,而且许多是可选参数。网络请求的参数一般来说都是需要的。假如某个请求需要 4 个参数,不管任何时候构建对象,都要把 4 个参数设置进去:
XXXParameter param = new XXXParameter.Builder() .setParam1(param1) .setParam2(param2) .setParam3(param3) .setParam4(param4) .build();
一个接口的功能一般比较单一,接口的参数不会太多,一般不会出现过多的构造方法重载的问题。
而且,如果后面请求的接口需要添加参数,使用简单工厂的话编译器就会报错(强提醒),builder 则不会,需要全局搜索哪些地方使用到了,然后设置新添加的参数。 -
使用静态工厂模式内聚性、可读性更强
使用工厂模式需要哪些参数一目了然,一方面不用担心哪些参数没有设置,另一方面工厂模式核心是屏蔽对象创建的过程,更符合我们的需求,这样说可能比较抽象,举个项目中的案例解释下,比如某个接口用获取订单各个状态下总数,店铺又分为餐饮行业和零售行业,这个接口的调用需要告知后台是获取餐饮的还是零售的,这个参数当然就可以使用常量来表示,这个参数不用每次都从外面传递进来,因为创建的过程可以屏蔽,代码如下:
// 餐饮行业,只需要传递用户ID public static OrderStatusRequest createForCatering(String userId) { OrderStatusRequest request = new OrderStatusRequest(); request.setUserId(userId); request.setType(IndustryTypeUtils.getIndustryType(IndustryType.CATERING)); return request; } // 零售行业,只需要传递用户ID public static OrderStatusRequest createForRetail(String userId) { OrderStatusRequest request = new OrderStatusRequest(); request.setUserId(userId); request.setType(IndustryTypeUtils.getIndustryType(IndustryType.RETAIL)); return request; }
不仅屏蔽了创建对象的细节,通过方法名开发者也可以很清楚的知道使用哪个静态方法,代码的可读性更强。(当然建造者也可以实现,但是需要额外维护建造者类)
虽然 静态工厂模式
和 建造者模式
都属于设计模式 创建型
这个分类,都是用来创建对象,但是他们的侧重点不一样,在实际的开发中不要对象的属性比较多就想都不想就直接使用建造者模式,不能为了使用模式而使用模式,使用前,我们要思考,使用这个模式是不是给了我们便利,代码可读性更好,还是额外增加工作量?
项目实践
经过上面的分析,相信读者对什么时候 建造者模式
有了比较深入的理解了。下面再来对笔者实际项目中代码进行改造,感受下在实际开发中如何使用 建造者模式
Android
开发者都知道,一般实现一个列表页面都是使用 RecyclerView
,RecyclerView
需要 RecyclerView.Adapter
来创建 Holder
和绑定视图,也就说列表具体展示什么样,是由 Adapter
来控制的
由于我们对列表类的界面进行了统一的封装,,关于这方面的可以看之前的文章:设计模式 ~ 模板方法模式分析与实战,所以我们也对 RecyclerView.Adapter
进行了二次封装,名字叫做 BaseListAdapter
,里面封装了空页面如何展示、底部Item布局、底部Item点击处理等逻辑,这些都是从外部传递进来的:
public abstract class BaseListAdapter extends RecyclerView.Adapter<BaseHolder> {
public BaseListAdapter(Context context) {
this(context, null);
}
public BaseListAdapter(Context context, FooterClick footerClick) {
this(context, footerClick, null);
}
public BaseListAdapter(Context context, FooterClick footerClick, View emptyView) {
this(context, footerClick, emptyView, 0);
}
public BaseListAdapter(Context context, FooterClick footerClick, @LayoutRes int emptyLayout) {
this(context, footerClick, emptyLayout, 0);
}
public BaseListAdapter(Context context, FooterClick footerClick, View emptyLayout, @LayoutRes int footerLayout) {
this(context, footerClick, 0, footerLayout);
this.mEmptyView = emptyLayout;
}
public BaseListAdapter(Context context, FooterClick footerClick, @LayoutRes int emptyLayout, @LayoutRes int footerLayout) {
this.mContext = context;
this.mEmptyLayout = emptyLayout;
this.mFooterLayout = footerLayout;
this.mFooterClick = footerClick;
this.mInflater = LayoutInflater.from(context);
}
// 省略其他代码
}
关于空页面参数一开始封装的时候是传递 layoutId
,后来有些使用的地方已经存在了空布局View对象,所以后面又需要添加新的参数 View emptyLayout
,所以又需要添加新的构造函数,从而最终形成上面 重叠构造函数(telescoping constructor)
的样子
所以这个时候 建造者模式
最合适不过了,但是问题来了,由于我们这个 BaseListAdapter
是抽象类,无法在 build
方法中直接实例化,所以只能将 Builder
类也生命为抽象的,build
方法交给子类实现:
// 抽象建造器(abstract Builder)
public abstract static class Builder<T extends BaseListAdapter> {
// 必选参数
private final Context context;
private final LayoutInflater inflater;
// 下面都是可选参数
private @LayoutRes int emptyLayout;
private View emptyView;
private @LayoutRes int footerLayout;
private FooterClick footerClick;
//必须参数通过构造方法传递
public Builder(Context context){
this.context = context;
this.inflater = LayoutInflater.from(context);
}
public Builder setEmptyLayout(@LayoutRes int emptyLayout) {
this.emptyLayout = emptyLayout;
return this;
}
public Builder setEmptyView(View emptyView){
this.emptyView = emptyView;
return this;
}
public Builder setFooterLayout(@LayoutRes int footerLayout) {
this.footerLayout = footerLayout;
return this;
}
public Builder setFooterClick(FooterClick footerClick){
this.footerClick = footerClick;
return this;
}
// 由于抽象类不能实例化,交给子类处理
public abstract T build();
}
// 在具体 Adapter 中声明具体建造器(Concrete Builder)
public class OrderDetailAdapter extends BaseListAdapter {
// 只能通过建造器生成对象
private OrderDetailAdapter(Builder builder) {
super(builder);
}
// 具体建造器
public static class Builder extends BaseListAdapter.Builder<OrderDetailAdapter> {
public Builder(Context context) {
super(context);
}
@Override
public OrderDetailAdapter build() {
return new OrderDetailAdapter(this);
}
}
// 省略其他代码 ...
}
// 在列表界面构造Adapter对象
@Override
protected BaseListAdapter createAdapter() {
// 通过构方法
//mOrderDetailAdapter = new OrderDetailAdapter(getActivity(), this,R.layout.empty_order_detail_layout );
//return mOrderDetailAdapter;
// 通过建造器模式
return new OrderDetailAdapter.Builder(getActivity())
.setEmptyLayout(R.layout.empty_order_detail_layout)
.setFooterClick(this)
.build();
}
这样就将通过重叠构造方法的方式创建 Adapter
对象 改成 通过 Builder
模式创建 Adapter
对象,后面添加新的参数也非常容易,对 Client
来说也更加容易阅读和编写。
但是重构完后,需要将以前创建对象的地方,统统改成 Builder
模式的编写方式,或者 重叠构造器
和 Builder
共存,但是很不协调。这也就是为什么《Effective Java》中说:
将来你可能需要添加参数,如果一开始就使用构造器或静态工厂,等到类需要更多参数才添加建造者模式,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调,因此,通常最好一开始就是用建造者模式。
所以有的时候一开始尽管类的参数不多,但是有可能将来会因为扩展需要添加参数, 而且参数是可选的,这种情况下一开始就应该使用 建造者模式
。
Reference
- 《Effective Java》
- 《设计模式之禅》
- 《Java设计模式及实践》
- 《Java设计模式深入研究》
- 《设计模式(Java版)》
如果你觉得本文帮助到你,给我个关注和赞呗!
另外,我为 Android 程序员编写了一份:超详细的 Android 程序员所需要的技术栈思维导图。
如果有需要可以移步我的 GitHub -> AndroidAll,里面包含了最全的目录和对应知识点链接,帮你扫除 Android 知识点盲区。 由于篇幅原因只展示了 Android 思维导图: