暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅谈设计模式-建造者模式

仑哥讲JAVA 2018-11-30
88

    建造者模式同之前的工厂方法、抽象工厂、单例模式同属创建型模式,关于其的描述是这样的:

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

    从结果上来讲,和工厂模式一样,都是获得一个对象,但是是有较大区别的,建造者模式更加关注“零件装配”的顺序,而工厂模式及抽象工厂则关注什么产品由什么工厂生产,不关注其内部结构的构建过程。

    建造者模式通常包含以下四种角色:

  • Director:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

  • Builder:给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。

  • ConcreteBuilder:实现Builder接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例。

  • Product:要创建的复杂对象。

    下面通过实例讲解什么是建造者模式,如何通过建造者模式创建一个复杂的对象:

    以麦当劳点餐为例,我们通常需要点饮料、主食以及零食,例如果汁+汉堡+薯条这种常见的组合。首先构建一个套餐类:

1. Product

package com.allen.design.builder;
public class Meal {
private String food;
   private String drink;
   private String snack;
   public String getFood() {
return food;
   }

public void setFood(String food) {
this.food = food;
   }

public String getDrink() {
return drink;
   }

public void setDrink(String drink) {
this.drink = drink;
   }

public String getSnack() {
return snack;
   }

public void setSnack(String snack) {
this.snack = snack;
   }

@Override
   public String toString() {
return "Meal{" +
"food='" + food + '\'' +
", drink='" + drink + '\'' +
", snack='" + snack + '\'' +
'}';
   }

}
复制

    如果我们想要构建不同的组合以及单点一些项目,例如我们只需要饮料+主食或单点一个主食,通常可以采取多构造器模式:

public Meal(String food){
super();
   this.food = food;
}
public Meal(String drink,String food){
super();
   this.food = food;
   this.drink = drink;
}
复制

    通过new Meal("香辣鸡腿堡"),new Meal("可乐","香辣鸡腿堡")可以快速构建我们想要的对象,但是,我们并不能直观的知道传入的变量代表什么,也解决不了所有类型的构建,例如构建上述例子中,如果需要单点饮料,就无法做到。

    所以,我们需要建造者模式来完成这个构建过程。

2. Builder

    抽象建造者对象主要用来定义一个标准的构建过程,虽然这个并不是必须的,但是为了标准讲解建造者模式,我们创建了这个类:

package com.allen.design.builder;
public abstract class Builder {
protected String food;
   protected String drink;
   protected String snack;
   public Builder setFood(String food){
this.food = food;
       return this;
   }
public Builder setDrink(String drink){
this.drink = drink;
       return this;
   }
public Builder setSnack(String snack){
this.snack = snack;
       return this;
   }

public abstract void buildFood();
   public abstract void buildDrink();
   public abstract void buildSnack();
   public abstract Meal build();
}
复制

    我们看到该类里面有食物、饮料、零食的属性和set方法,以及构建这类属性的抽象方法,接下来我们实现该抽象类。

3. ConcreteBuilder

    具体建造者继承了抽象建造者,并且实现了建造的方法和建造的最终结果。

package com.allen.design.builder;
public class MealBuilder extends Builder{
private Meal meal = new Meal();
   @Override
   public void buildFood() {
meal.setFood(this.food);
   }

@Override
   public void buildDrink() {
meal.setDrink(this.drink);
   }

@Override
   public void buildSnack() {
meal.setSnack(this.snack);
   }

@Override
   public Meal build() {
return meal;
   }
}
复制

4. Director

    指导者,按照指导者的构建顺序进行构建。

package com.allen.design.builder;
public class Director {
public void construct(Builder builder){
builder.buildDrink();
       builder.buildFood();
       builder.buildSnack();
   }
}
复制

测试类如下:

package com.allen.design.builder;
public class MainTest {

public static void main(String[] args) {
Builder builder = new MealBuilder().setDrink("橙汁").setFood("麦辣鸡腿堡").setSnack("薯条");
       Director director = new Director();
       director.construct(builder);
       Meal meal = builder.build();
       System.out.println(meal);
       Builder builder1 = new MealBuilder().setDrink("可乐");
       director.construct(builder1);
       Meal meal1 = builder1.build();
       System.out.println(meal1);
   }
}
复制

    输出结果如下:

    为方便理解,我们画出上述四种角色的UML图:

    如上便是一个建造者模式的实例,可以看到通过建造者模式,我们可以很方便的生产出各式各样的产品,结合具体一些框架的建造者模式应用,可能我们更容易理解该模式的特点和应用:


    OkHttp的建造者模式

    OkHttp创建request对象的方法如下:

Request request = new Request.Builder().url("http://www.baidu.com")
.addHeader("Content-Type", "application/json").build();
复制

    可以看到上述结构和我们的例子中是类似的,打开源码我们看到Request内有个静态内部类:

Builder(Request request) {
this.url = request.url;
   this.method = request.method;
   this.body = request.body;
   this.tag = request.tag;
   this.headers = request.headers.newBuilder();
 }

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 web socket 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;
 }

/**
  * Adds a header with {@code name} and {@code value}. Prefer this method for multiply-valued
  * headers like "Cookie".
  *
  * <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
  * OkHttp may replace {@code value} with a header derived from the request body.
  */
 public Builder addHeader(String name, String value) {
headers.add(name, value);
   return this;
 }

/** Removes all headers named {@code name} on this builder. */
 public Builder removeHeader(String name) {
headers.removeAll(name);
   return this;
 }

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

/**
  * Sets this request's {@code Cache-Control} header, replacing any cache control headers already
  * present. If {@code cacheControl} doesn't define any directives, this clears this request's
  * cache-control headers.
  */
 public Builder cacheControl(CacheControl cacheControl) {
String value = cacheControl.toString();
   if (value.isEmpty()) return removeHeader("Cache-Control");
   return header("Cache-Control", value);
 }

public Builder get() {
return method("GET", null);
 }

public Builder head() {
return method("HEAD", null);
 }

public Builder post(RequestBody body) {
return method("POST", body);
 }

public Builder delete(@Nullable RequestBody body) {
return method("DELETE", body);
 }

public Builder delete() {
return delete(Util.EMPTY_REQUEST);
 }

public Builder put(RequestBody body) {
return method("PUT", body);
 }

public Builder patch(RequestBody body) {
return method("PATCH", body);
 }

public Builder method(String method, @Nullable 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;
 }

/**
  * Attaches {@code tag} to the request. It can be used later to cancel the request. If the tag
  * is unspecified or null, the request is canceled by using the request itself as the tag.
  */
 public Builder tag(Object tag) {
this.tag = tag;
   return this;
 }

public Request build() {
if (url == null) throw new IllegalStateException("url == null");
   return new Request(this);
 }
}
复制

    可以看到这个是将具体建造者和抽象建造者合二为一,并且使用了静态内部类的方式,简化了代码,也更方便理解,在此基础上,我们对原有例子中代码同样进行改造,改造meal类,增加静态内部类和构造器,如下:

public Meal(){
super();
}
public Meal(Builder builder){
this.drink = builder.drink;
   this.food = builder.food;
   this.snack = builder.snack;
}
public static class Builder{
String drink;
   String food;
   String snack;
   public Builder setFood(String food){
this.food = food;
       return this;
   }
public Builder setDrink(String drink){
this.drink = drink;
       return this;
   }
public Builder setSnack(String snack){
this.snack = snack;
       return this;
   }
public Meal build(){
Meal meal = new Meal(this);
       return meal;
   }
}
复制

    测试类:

Meal meal2 = new Meal.Builder().setDrink("咖啡").build();
Meal meal3 = new Meal.Builder().setDrink("咖啡").setSnack("香芋派").build();
System.out.println(meal2);
System.out.println(meal3);
复制

    输出结果:

    修改后的建造者模式写法更加简单,调用起来也方便的多,想set什么属性就set什么属性。

    

    SWAGGER2的建造者模式

    作为当今非常流行的接口描述文档,swagger对rest的描述支持是非常全的,当我们需要发布一个swagger描述时,通常需要生成bean来发布,如下:

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())//调用apiInfo方法,创建一个ApiInfo实例,里面是展示在文档页面信息内容
           .select()
//控制暴露出去的路径下的实例
           //如果某个接口不想暴露,可以使用以下注解
           //@ApiIgnore 这样,该接口就不会暴露在 swagger2 的页面下
           .apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(PathSelectors.any())
.build();
}
//构建 api文档的详细信息函数
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//页面标题
           .title("xx接口api")
//创建人
           .contact("xxxx")
//版本号
           .version("1.0")
//描述
           .description("外部系统对接api")
.build();
}
复制

    其实现也是通过建造者模式,较为复杂,不再详细讲解,有兴趣可以自行翻阅swagger2源码。

    总结而来,建造者有以下优缺点:

    优点:

    1、客户端不需要知道产品内部组成的细节,将产品本身与产品创建的过程解耦,使得相同创建过程可以创建不同的产品对象。

    2、每一个具体建造者都相对独立,用户使用不同的具体建造者可以得到不同的产品对象。

    3、可以更为精细的控制产品的创建过程。将复杂产品的创建不走分解在不同的方法中,使得创建过程更加清晰,也方便使用程序来控制创建过程

    缺点:

    如果产品间组成部分差异过大或产品内部变化比较复杂,则不适合使用建造者模式。

    所以,建造者模式适合构建有着多个组合部分,但是有统一的构建顺序及规则的对象的构建,例如常见的HttpRequest或RequestEntity构建,或者Datasource、StringBuilder的构建等。

    本文通过讲解建造者模式的定义,四个角色,并通过麦当劳点餐实例以及部分应用到该模式的框架分析,来理解建造者模式,并且分析其优缺点,方便以后对相关源码的分析,以及在合适的场合应用建造者模式。

文章转载自仑哥讲JAVA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论