结合Android浅谈Builder模式

Android

前言

Builder模式,对象创建型的设计模式。说起设计模式,可能会觉得有点高深莫测,其实不然,我们每天写代码都在多多少少的和各种各样的设计模式接触,只是没有察觉而已。这里就来说一说Builder模式。

Android中的Builder模式

在Android开发中,什么时候会用到Builder模式呢?其实很简单,就是当你想使用一个控件时或者是一个对象时,没有办法直接把他New 出来;那么这个控件(对象)的实现多半就是用到了Builder模式。

AlertDialog.Builder

    private void InitView() {
        //直接创建对象
        TextView mTextView = new TextView(this);
        Button mButton = new Button(this);
        
        // 用Builder模式创建Dialog
        AlertDialog.Builder builder=new 
                AlertDialog.Builder(this)
                .setTitle("My Dialog")
                .setMessage("This is Test Dialog")
                .setIcon(R.drawable.application_icon);
        AlertDialog dialog=builder.create();
    }

如上面的代码,我们可以按照普通的方式(new)创建TextView对象和Button对象;但是轮到AlertDialog时,却需要首先创建一个AlertDialog.Builder对象,然后通过这个Builder对象才能创建AlertDialog的一个实例。同样都是Android的控件,差距为什么这么大呢?(因为Dialog复杂呀!o(╯□╰)o)。

下面可以结合AlertDialog的源码简单分析一下。

    protected AlertDialog(@NonNull Context context) {
        this(context, 0);
    }
  • 首先是他的构造方法,可以看到这个方法是用 protected 修饰的,这意味着除了AlertDialog的子类之外,其他类是无法访问这个方法的。AlertDialog的其他两个重载的构造方法也是用到protected关键字修饰,有兴趣的同学可以自己参考源码;因此,在Activity或者是Fragment里,我们是无法直接创建AlertDialog的实例的,而是需要通过Builder对象。
        public static class Builder {
        private final AlertController.AlertParams P;

        public Builder(@NonNull Context context) {
            this(context, resolveDialogTheme(context, 0));
        }
        public Builder(@NonNull Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }
        public Builder setTitle(@Nullable CharSequence title) {
            P.mTitle = title;
            return this;
        }
        public Builder setMessage(@Nullable CharSequence message) {
            P.mMessage = message;
            return this;
        }
        public Builder setIcon(@DrawableRes int iconId) {
            P.mIconId = iconId;
            return this;
        }
        
    .....
}
  • Builder类是AlertDialog内部的一个静态类。在这个类里有一个很关键的属性P,可以关注一下,这个变量是final类型的;除此之外剩下的就是一系列的setxxx 方法,用于设置AlertDialog的不同属性,例如上面列举的三个方法,可以分别设置AlertDialog的Title,Message及 Icon 信息。在这些方法中,都是把参数直接传递给了之前所说的P这个实例,而且每一个方法的返回值都是Builder类自身,这样就方便开发者链式调用每一个方法,这样不仅写起来简单,而且读起来也很有逻辑感
        public AlertDialog create() {
            // We can't use Dialog's 3-arg constructor with the createThemeContextWrapper param,
            // so we always have to re-set the theme
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }
  • 最后,在Builder类的create方法中完成了AlertDialog的创建;万变不离其宗,AlertDialog的实例化,还是通过new创建出来,并通过之前所说的实例P和dialog实例实现了某种关联(具体如何实现暂不展开讨论),总之就是把之前通过Builder方法设置的一系列参数都配置到了最终的AlertDialog之上。

OKHttp中的Request.Builder

对于OKHttp,相信大家都不陌生,在构造Request对象时,就用到了Request.Builder。顾名思义,这里也用到了Builder模式。

        findViewById(R.id.get).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText("");
                loading.setVisibility(View.VISIBLE);
                OkHttpClient client = new OkHttpClient();
                Request.Builder builder = new Request.Builder()
                        .url(BASE_URL)
                        .method("GET", null);

                Request request = builder.build();
                Call mCall = client.newCall(request);
                mCall.enqueue(new MyCallback());
            }
        });

以上是一个很典型的关于OKHttp的使用方式,这里Request对象也不是直接创建,而是通过首先创建一个Request.Builder对象,再通过他的build方法创建出最终的request对象。
这里可以粗略的看一下Request类的源码。

public final class Request {
  private final HttpUrl url;
  private final String method;
  private final Headers headers;
  private final RequestBody body;
  private final Object tag;


  private Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tag = builder.tag != null ? builder.tag : this;
  }
    // 省略部分....
    
  public static class Builder {
    private HttpUrl url;
    private String method;
    private Headers.Builder headers;
    private RequestBody body;
    private Object tag;

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
    }


    public Builder url(HttpUrl url) {
      if (url == null) throw new NullPointerException("url == null");
      this.url = url;
      return this;
    }

    /**
     * Sets the URL target of this request.
     *
     * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this
     * exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs.
     */
    public Builder url(String url) {
      if (url == null) throw new NullPointerException("url == null");

      // Silently replace websocket URLs with HTTP URLs.
      if (url.regionMatches(true, 0, "ws:", 0, 3)) {
        url = "http:" + url.substring(3);
      } else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
        url = "https:" + url.substring(4);
      }

      HttpUrl parsed = HttpUrl.parse(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }

    /**
     * Sets the URL target of this request.
     *
     * @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code
     * https}.
     */
    public Builder url(URL url) {
      if (url == null) throw new NullPointerException("url == null");
      HttpUrl parsed = HttpUrl.get(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }

    /**
     * Sets the header named {@code name} to {@code value}. If this request already has any headers
     * with that name, they are all replaced.
     */
    public Builder header(String name, String value) {
      headers.set(name, value);
      return this;
    }


    /** Removes all headers on this builder and adds {@code headers}. */
    public Builder headers(Headers headers) {
      this.headers = headers.newBuilder();
      return this;
    }

    public Builder method(String method, RequestBody body) {
      if (method == null) throw new NullPointerException("method == null");
      if (method.length() == 0) throw new IllegalArgumentException("method.length() == 0");
      if (body != null && !HttpMethod.permitsRequestBody(method)) {
        throw new IllegalArgumentException("method " + method + " must not have a request body.");
      }
      if (body == null && HttpMethod.requiresRequestBody(method)) {
        throw new IllegalArgumentException("method " + method + " must have a request body.");
      }
      this.method = method;
      this.body = body;
      return this;
    }


    public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }
  }
}
  • 首先,Request是final类型的,因此他不会有子类;再有就它唯一的构造方法是private的;因此,对于开发者来说就无法通过普通的方法(通过new)创建一个Request对象了。

  • Builder类,是一个静态内部类,通过其构造方法可以看出,Request 默认的请求方式是GET请求方式,同时当我们在创建Builder对象时,如果没有提供url 参数时会抛出异常,这是合理的也是必须的,一个Http请求如果没有url那么一切都是空谈,这里抛出异常 十分必要。在method方法中,会根据参数修改具体的请求方法,同时会根据请求方法判断是否需要RequestBody。

  • 最后,通过build方法创建了Request,可以看到这里调用的就是Request唯一的构造方法,传递的参数就是当前Builder实例。这样创建的Request对象就是根据我们构造出来的Builder实例所量身定制的Request。便于下一步进行同步或异步的网络请求。

至此,你可能会有疑问,所谓的Builder模式有什么意义?

为什么Android系统中创建一个AlertDialog要这么复杂,像TextView一样,直接new出来一个实例然后set各种属性不也一样可用吗?
Request 对象的创建不使用Builder模式一样也是可以的呀?上面各种异常处理,方法执行用普通的方式也可实现,Builder模式的价值在哪里呢?

带着这些疑问,让我们去好好理解一下Builder模式。

Builder 模式

定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

适用场景

1.相同的方法,不同的执行顺序,产生不同的事件结果
2.多个部件或零件,都可以装配到同一个对象中,但是产生的运行结果又不相同
3.产品类非常复杂,或者产品中的调用顺序不同产生了不同的作用
4.需要初始化一个对象特别复杂的对象,这个对象有很多参数,且有默认值

看这样的概念也许有些抽象,下面还是通过代码来看看。在之前工厂方法模式中,我们用工厂方法模式列举了Mobike于Ofo 对象生成的例子。这里依旧以二者为例,看看用Builder模式怎么写。

public final class Bicycle {
    public static final int SHARED = 1;
    public static final int PRIVATE = 0;

    @IntDef({SHARED, PRIVATE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface bicycleType {
    }

    protected String color;
    protected String name;
    protected double charge;
    protected int number;
    protected int type;

    protected Bicycle(BicycleBuilder builder) {
        this.color = builder.color;
        this.name = builder.name;
        this.charge = builder.chager;
        this.number = builder.number;
        this.type = builder.type;
    }

    public static class BicycleBuilder {


        private String color;
        private String name;
        private double chager;
        private int number;
        private int type;

        public BicycleBuilder() {
            this.color = "黑色";
            this.name = "永久";
            this.chager = 0;
            this.number = 0;
            this.type = Bicycle.PRIVATE;
        }

        public BicycleBuilder setColor(String color) {
            this.color = color;
            return this;
        }

        public BicycleBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public BicycleBuilder setCharge(double chager) {
            this.chager = chager;
            return this;
        }

        public BicycleBuilder setNumber(int number) {
            this.number = number;
            return this;
        }

        public BicycleBuilder setType(@bicycleType int type) {
            this.type = type;
            return this;
        }

        public Bicycle build(){
            return new Bicycle(this);
        }
    }

    @Override
    public String toString() {
        String typeStr= type == SHARED ? "共享单车": "私人车辆";

        return "Bicycle{" +
                "color='" + color + '\'' +
                ", name='" + name + '\'' +
                ", charge=每分钟" + charge +"/元"+
                ", number=" + number +
                ", type=" + typeStr +
                '}';
    }
}

在这里Bicycle类包含5个特有的属性,同时将其构造方法设置为protected。通过BicycleBuilder 类来真正实现创建Bicycle的实例。这里BicycleBuilder的默认的构造方法,会创建一个普通的黑色永久牌私人自行车,而通过BicycleBuilder提供的几个方法我们便可以创建不同的Bicycle实例。比如下面这种实现:

public class BuilderPatternActivity extends AppCompatActivity {
    private TextView bike_result;
    private Bicycle mBicycle;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_builder_pattern);
        bike_result = V.f(this, R.id.bike_result);
    }

    /**
     * 普通自行车
     * @param view
     */
    public void NormalBike(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder();
        mBicycle=builder.build();
        updateView(mBicycle);
    }
    /**
     * 膜拜单车
     * @param view
     */
    public void Mobike(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder()
                .setColor("橙色")
                .setName("膜拜单车")
                .setCharge(1.0)
                .setNumber(10010)
                .setType(Bicycle.SHARED);
        mBicycle=builder.build();
        updateView(mBicycle);
    }
    /**
     * OFO 单车
     * @param view
     */
    public void Ofo(View view) {
        Bicycle.BicycleBuilder builder=new Bicycle.BicycleBuilder()
                .setColor("黄色")
                .setName("OFO单车")
                .setCharge(0.5)
                .setNumber(40010)
                .setType(Bicycle.SHARED);
        mBicycle=builder.build();
        updateView(mBicycle);
    }


    private void updateView(Bicycle mBicycle) {
        bike_result.setText("");
        bike_result.setText(mBicycle.toString());
    }
}

通过Bicycle.BicycleBuilder 提供的一系列set方法,我们创建了mobike实例和ofo单车实例。

这就是Builder模式,正如定义和使用场景中提到的那样,通过Builder模式,在同样的构建过程下,我们可以创建不同的结果;通常来说,我们要创建的对象是很复杂的,有很多参数,这些参数中有些是必须的,比如OKHttp中Request的url参数,有些参数又会有默认值;总之,Builder模式,一种对象创建型的设计模式;为我们创建对象提供了一种思路。

最后,再说一个使用了Builder模式的东西-RxJava。说到RxJava我们很容易想到观察者模式。不错,RxJava最核心的思想就是观察者模式;但是想一想我们使用RxJava的过程。

        ArrayList<String> datas = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            datas.add("item_" + i);
        }
        Observable.just(datas)
                .flatMap(new Func1<ArrayList<String>, Observable<String>>() {
                    @Override
                    public Observable<String> call(ArrayList<String> strings) {
                        return Observable.from(strings);
                    }
                })
                .map(new Func1<String, Integer>() {
                    @Override
                    public Integer call(String s) {
                        return s.hashCode();
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer Integer) {
                        Log.e(MainActivity.class.getSimpleName(), "call---->" + Integer);
                    }
                });

如上代码所示,在subscribe 方法执行之前,通过各种各样的操作符,原始数据一个ArrayList变成了一个Integer类型的数据,也就是说我们使用操作符的过程,就是一个Builder模式构建的过程,直到生成我们最终需要的产品为止。这和Builder模式的定义以及使用场景是完全符合的。

RxJava 不是Builder模式

Builder模式 VS 工厂方法模式

工厂模式一般都是创建一个产品,注重的是把这个产品创建出来就行,只要创建出来,不关心这个产品的组成部分。从代码上看,工厂模式就是一个方法,用这个方法就能生产出产品。

建造者模式也是创建一个产品,但是不仅要把这个产品创建出来,还要关系这个产品的组成细节,组成过程。从代码上看,建造者模式在建造产品时,这个产品有很多方法,建造者模式会根据这些相同方法但是不同执行顺序建造出不同组成细节的产品。

工厂模式关心整体,建造者模式关心细节

最后

现在回到我们之前提出的问题,Builder模式的意义是什么?看完之后你可能已经得到答案了,没有任何实质意义,Builder模式的使用并不会使我们的代码运行速度加快。设计模式总的来说就是对是封装、继承、多态和关联的反复使用;是一种编程技巧,让我们能写出高质量代码的技巧。

最后再说一句,严格来说本文讨论的Builder模式并不是标准意义上的Builder模式,在这里我们从Android源码的角度出发,简化了Builder模式,为了方便链式调用及习惯,舍弃了原本应有的Director角色。对正统的Builder模式感兴趣的同学可以再去深入研究。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 面向对象的六大原则 单一职责原则 所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于...
    JxMY阅读 924评论 1 3
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,852评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 1 场景问题# 1.1 继续导出数据的应用框架## 在讨论工厂方法模式的时候,提到了一个导出数据的应用框架。 对于...
    七寸知架构阅读 5,659评论 1 64