Java设计模式实战 ~ 深入理解建造者模式与实战

<<<返回总目录

1、建造者模式的定义

今天我们来介绍一个开发中也是很常见的设计模式:建造者模式(Builder Pattern)

建造者模式的定义:将一个复杂对象的构建与它的表示分离,使得同样的构造过程可以创建不同的表示

建造者模式主要由以下几个角色组成:

  • 产品(Product)

    该角色就是建造者要建造的复杂对象

  • 抽象建造者(Builder)

    用于规范产品的各个组成部分,并进行抽象

  • 具体建造者(Concrete Builder)

    实现抽象建造者抽象方法

  • 导演(Director)

    负责建造者构造产品组成部分的顺序,先构造哪部分,然后构造哪部分,最后调用建造者的建造方法

建造者模式的通用类图:

BuilderPattern UML

2、建造者模式的实现

通过上面介绍的建造者模式的类图,我们看下用代码如何实现一个建造者模式

产品类

// 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 上了,上面的例子是先构造 Userusername,然后构造 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();
    

将来是否还有其他的建造者模式的变化也不好说,总的一点就是建造模式是对象构建过程与表示的分离,在这个根本原则下,具体的实现形式并不是一成不变的,掌握其基本的实现原理,变种也是很容易理解的。

3、建造者模式还是构造方法重载

下面我们来看下《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 的构造方法的参数

4、使用建造者模式还是静态工厂模式?

在实际开发中一般都需要请求网络,请求接口的时候我们一般都会将参数封装到一个对象里面去,但是有的时候并不是所有的参数都是需要的,这个时候是用建造模式还是简单工厂模式

如果是网络请求参数的封装 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;
    }
    

    不仅屏蔽了创建对象的细节,通过方法名开发者也可以很清楚的知道使用哪个静态方法,代码的可读性更强。(当然建造者也可以实现,但是需要额外维护建造者类)

虽然 静态工厂模式建造者模式 都属于设计模式 创建型 这个分类,都是用来创建对象,但是他们的侧重点不一样,在实际的开发中不要对象的属性比较多就想都不想就直接使用建造者模式,不能为了使用模式而使用模式,使用前,我们要思考,使用这个模式是不是给了我们便利,代码可读性更好,还是额外增加工作量?

6、项目实践

经过上面的分析,相信读者对什么时候 建造者模式 有了比较深入的理解了。下面再来对笔者实际项目中代码进行改造,感受下在实际开发中如何使用 建造者模式

Android 开发者都知道,一般实现一个列表页面都是使用 RecyclerViewRecyclerView 需要 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版)》
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值