> 文档中心 > [ 设计模式 ] 彻底搞懂建造者模式 —— 全网最透彻理解

[ 设计模式 ] 彻底搞懂建造者模式 —— 全网最透彻理解


   相信很多人搜索 “建造者模式 的时候和我一样,首先映入眼帘的就是下面这张UML图(除了属性的区别,结构完全一样):
 
[小声说:“不了解UML类图的话,欢迎移步 —— UML类图介绍 ——程序员(灵魂画手)必备画图技能之一”  😏​ ]

在这里插入图片描述

[大家好,我是建造者模式图]

 
 
 
看完这张图后再来一个对Builder的实现类,然后再Director完成构造,再到末尾附上几句雷同的解释,让你有种似懂非懂,飘飘欲仙的感觉(此处可省略1w字)~
 
在这里插入图片描述

 

好了,如果觉得没飘够的同学可以继续飘一会,反之请忘掉上面的一切吧(包括那张UML图),跟随笔者迅雷不及掩耳盗铃般的脚步,深入探索如何真正成为一名合格的 “建造者” ! 
  
 


关于建造者模式(当然也有人叫它构建者模式创建者模式Builder模式生成器模式…Anyway)你要记住关键的也是第一的要素就是(个人提炼,如有雷同纯属巧合):

  • 把一个对象原本看似复杂繁琐的 [构造过程] 变得更加简单、清晰、明了

当然,设计模式的好处肯定不止一两个,建造者也不会例外。但是笔者认为建造者最重要的体现就是我上面说的这句话,你也可以理解为让构造的语义更加简单明了。那么其他就都是根据不同需求场景下,体现出来边边角角的好处了。前提,首先你记住上面这句话,然后思考这个问题 —— 假如让你设计一个完美的机器人"类",包含head、body、hands、legs、size洗个属性,你会怎么设计 ?(请面壁十秒后再来看下文)


 
相信你已经有了自己的答案,没错,一个可供人使用的完美的机器人"类"应该是可以配置的,是不是应该有默认值,和非默认值的必填选项? (可以类比第三方工具的配置类),具体代码如下

public class Robot {    private static final int DEFAULT_HANDS = 2;    private static final int DEFAULT_LEGS = 2;    private static final int DEFAULT_SIZE = 10;    // 假设大脑必须根据个人要求设定(必填)    private String head;    // 假设身体必须根据个人要求设定(必填)    private String body;    private int hands = DEFAULT_HANDS;    private int legs = DEFAULT_LEGS;    private int size = DEFAULT_SIZE;    public Robot(String head, String body, Integer hands, Integer legs, Integer size) { if (StringUtils.isBlank(head)) {     throw new IllegalArgumentException("WTF , don't you have head ?"); } this.head = head; if (StringUtils.isBlank(body)) {     throw new IllegalArgumentException("WTF , don't you have body ?"); } this.body = body; if(null !=hands) {     if (hands <= 0) {  throw new IllegalArgumentException("Come on , you need hands to work !");     }     this.head = head; } if(null !=legs) {     if (legs <= 0) {  throw new IllegalArgumentException("Come on , you need legs to walk !");     }     this.legs = legs; } if(null !=size) {     if (size <= 0) {  throw new IllegalArgumentException("Come on , you need to be exist !");     }     this.size = size; }    }    // getter方法省略...}

好上面这个Robot让你new出来的画风是不是这样的?

Robot robot = new Robot("bigHead","bigBody",null,null,null);

这还只有6个构造参数还好,那么如果有16个、60个、100个、10086个呢?代码可读性是否就降低了?而且可能一不小心就弄错了构造参数的顺序?就算你乍一眼能看出来这些参数的意思,你让熟悉代码的新员工咋办?玩锤子?作为一名负责的程序猿是否得对新猿友好?

在这里插入图片描述

品了半天,你可能想到了全部改为Setter,嗯….兄嘚果然天资聪颖,是稍微好看点了(setter细节就不改了,大体构造就变会成如下)

Robot robot = new Robot("bigHead","bigBody");robot.setHands();robot.setLegs();robot.setSize();...

但是稍微加大点难度,上面的设计思路就不满足了:

  • 参数冗长: 必填属性多,任然会导致构造参数变长
  • 安全问题: 假设我们要求一个robot在构造完后就不能再对其做改动了(否则视为不安全的异常操作)
  • 依赖问题: 假设我们要求hands被改变了那legs也必须被改变,或者hands/legs必须小于size等特殊校验需求,那代码又会近一步变的凌乱

兄嘚我就问你怎么办???

在这里插入图片描述

这时候,建造者模式的各个闪光点就充分体现出来了:

  1. 所有Setter方法放入一个Builder内部类中(robot没有暴露Setter方法,外部自然无法改变
  2. 把robot的构造方法改为私有的(这样robot就 只有Bulder能够构造 了)
  3. Bulder中自然会调用robot的构造方法,把所有 特殊且必要的校验 在Robot构造方法被调用之前 统一完成
     

   改造代码如下:

public class Robot {    // 默认值    private static final int DEFAULT_HANDS = 2;    private static final int DEFAULT_LEGS = 2;    private static final int DEFAULT_SIZE = 10;    // Robot的属性    private String head;    private String body;    private int hands = DEFAULT_HANDS;    private int legs = DEFAULT_LEGS;    private int size = DEFAULT_SIZE;    // 注意:Robot的构造是私有的,且是通过传入Builder来构造    // 你无法在外面对已构造好的Robot做出任何改变    private Robot(Builder builder) { this.head = builder.head; this.body = builder.body; this.hands = builder.hands; this.legs = builder.legs; this.size = builder.size;    }    // 省略Getter方法...     public static class Builder {    private static final int DEFAULT_HANDS = 2;    private static final int DEFAULT_LEGS = 2;    private static final int DEFAULT_SIZE = 10;    private String head;    private String body;    private int hands = DEFAULT_HANDS;    private int legs = DEFAULT_LEGS;    private int size = DEFAULT_SIZE;    // Robot在此构建,在构建之前完成所有必须的 "特殊校验"    public Robot build() { if (StringUtils.isBlank(head)) {     throw new IllegalArgumentException("WTF , don't you have head ?"); } if (StringUtils.isBlank(body)) {     throw new IllegalArgumentException("WTF , don't you have body ?"); } if (hands != DEFAULT_HANDS && legs == DEFAULT_LEGS) {     throw new IllegalArgumentException("Bro, you should update your legs"); } if (hands == DEFAULT_HANDS && legs != DEFAULT_LEGS) {     throw new IllegalArgumentException("Bro, you should update your hands"); } return new Robot(this);    }    // 可以看到每个setter方法返回的都是Builder本身    public Builder setHead(String head) { if (StringUtils.isBlank(head)) {     throw new IllegalArgumentException("WTF , don't you have head ?"); } this.head = head; return this;    }    public Builder setBody(String body) { if (StringUtils.isBlank(body)) {     throw new IllegalArgumentException("WTF , don't you have body ?"); } this.body = body; return this;    }    public Builder setHands(Integer hands) { if (null != hands) {     if (hands <= 0) {  throw new IllegalArgumentException("Come on , you need hands to work !");     }     this.head = head; } return this;    }    public Builder setLegs(Integer legs) { if (null != legs) {     if (legs <= 0) {  throw new IllegalArgumentException("Come on , you need legs to walk !");     }     this.legs = legs; } return this;    }    public Builder setSize(Integer size) { if (null != size) {     if (size <= 0) {  throw new IllegalArgumentException("Come on , you need to be exist !");     }     this.size = size; } return this;    }  }}

这样一来,一个构造条理清晰,且一旦构造完成,则外部无法改变的超级Robot就此诞生 !!

在这里插入图片描述

  Robot robot = new Robot.Builder()  .setHead("superHead")  .setBody("superBody")  .setHands(999)  .setLegs(999)  .setSize(10086)  .build();

 
至此,一个《建造者模式》被完美演绎完毕!其实除了建造者除了上述的边角闪光点,还有另一个闪光点就是能避免对象的无效存在,什么是无效存在呢?比如一个“矩形”对象,你是不是必须设置完长、宽才能算有效?不然你单独设置一个长你就当矩形用?那建造者模式就完美解决了这个问题,因为被此模式建造出来的对象一定是完整可用的。
 
 

【 小结 】

想必你已经摸清建造者模式的大致轮廓了,接下来依然离不开谈到建造者模式必然和工厂模式对比,那个老生常谈的问题了!这里必须套娃引用某位网友对此的看法

—— “ 顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。 ”

上面的故事应该很容易理解 :

  • 其实不管是工厂模式还是抽象工厂模式,本质上都是站在类的层面,对各种类进行创建。
  • 建造者模式,关注的是类本身的创建,所以区别还是很明显的(其实和模板模式也有点像,都是设计好一个清晰的车骨架,然后让你在骨架里造轮子和其他部件。只不过模板模式是行为型模式,属于方法的运作,而建造者是创建型模式,属于对象的构建)。
     
     

上面大体已经应该把建造者模式说清楚了,年轻人,如果你觉得已经可以了,那就放心大胆的出去闯吧!建造你自己的世界!!

在这里插入图片描述

但是你如果意犹未尽,执意要做最强的建造者,那我这里还有一本《RabbitMQ的建造者模式》演绎法,你品品和我上面的例子有什么不同?请带着最开头我说的那句话 —— 让构造的语义更加简单明了 来欣赏下文:

在这里插入图片描述

如果你使用过或者见过RabbitMQ的配置类,那么你应该可以看到大致如下的代码:

return BindingBuilder.bind(queue).to(exchange);
 return BindingBuilder.bind(queue).to(exchange).with(routingKey);

兄弟,惊讶吗?这语义是有多明显???稍微有点小学英语文化水平的也懂这个to、with的意思吧?这还用去看底层源码?隔壁新猿小王是不是直接就可以上手?

在这里插入图片描述

如果你看过BindingBuilder内的代码逻辑,就会发现,其实这两段绑定代码也可以写成这样,依然有效。

return new Binding(queue.getName(),Binding.DestinationType.QUEUE,exchange.getName(),"",null);
return new Binding(queue.getName(),Binding.DestinationType.QUEUE,exchange.getName(),routingKey,null);

但是,这样又回到了最开始的问题,你让隔壁新猿小王玩锤子??玩锤子呢?!

总之,四不像和独角兽,你选一个,还是最初我说道的那句话 —— 让构造的语义更加简单明了,你品你细品。

最后,如果你点进BindingBuilder的代码看,会发现他每层的setter返回的并不是BindingBuilder本身,而是一个叫…哎光说不练假把式,talk is cheap 为了不被怼有烂尾的嫌疑,我还是show you the code 把。

首先看 BindingBuilder.bind() 方法

public static DestinationConfigurer bind(Queue queue) {return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE);}

你可以看到他们无一例外都返回了一个DestinationConfigurer

接下来看 BindingBuilder.bind().to() 方法

public static final class DestinationConfigurer {protected final String name; // NOSONARprotected final DestinationType type; // NOSONARDestinationConfigurer(String name, DestinationType type) {this.name = name;this.type = type;} // 看到没,这下面足足有5个to方法来应对各种交换机public Binding to(FanoutExchange exchange) {return new Binding(this.name, this.type, exchange.getName(), "",    new HashMap<String, Object>());}public HeadersExchangeMapConfigurer to(HeadersExchange exchange) {return new HeadersExchangeMapConfigurer(this, exchange);}    // 这个是我下面要举例的方法哦public DirectExchangeRoutingKeyConfigurer to(DirectExchange exchange) {return new DirectExchangeRoutingKeyConfigurer(this, exchange);}public TopicExchangeRoutingKeyConfigurer to(TopicExchange exchange) {return new TopicExchangeRoutingKeyConfigurer(this, exchange);}public GenericExchangeRoutingKeyConfigurer to(Exchange exchange) {return new GenericExchangeRoutingKeyConfigurer(this, exchange);}}

如果你了解RabbitMQ的话,你应该知道Fanout模式是不需要指定routingKey的,所以你可以看到Fanout交换机直接执行最终的建造方法 —— 也就是 Binding() 方法

但是你也能看到在之后需要with方法的to都返回了另一个对象,其实这对象都是继承自AbstractRoutingKeyConfigurer的子类,这里就拿DirectExchangeRoutingKeyConfigurer举例把

  • 首先看 AbstractRoutingKeyConfigurer
private abstract static class AbstractRoutingKeyConfigurer {protected final DestinationConfigurer destination; // NOSONARprotected final String exchange; // NOSONARAbstractRoutingKeyConfigurer(DestinationConfigurer destination, String exchange) {this.destination = destination;this.exchange = exchange;}}
  • 然后看DirectExchangeRoutingKeyConfigurer
public static final class DirectExchangeRoutingKeyConfigurer extends AbstractRoutingKeyConfigurer {DirectExchangeRoutingKeyConfigurer(DestinationConfigurer destination,  DirectExchange exchange) {     // 这里调用父类构造就是赋值而已super(destination, exchange.getName());}   // 可以看到下面有三个with方法来完成对routingKey的绑定,以及最终的建造Binding()方法public Binding with(String routingKey) {return new Binding(destination.name, destination.type, exchange, routingKey,Collections.<String, Object>emptyMap());}public Binding with(Enum<?> routingKeyEnum) {return new Binding(destination.name, destination.type, exchange, r   outingKeyEnum.toString(),Collections.<String, Object>emptyMap());}public Binding withQueueName() {return new Binding(destination.name, destination.type, exchange,    destination.name,Collections.<String, Object>emptyMap());}}

至此你已经看到了整个绑定关系的构建流程,无一例外最终都会指向那个对象的构造方法(这一点和我上面的例子一样,也是建造者模式必然的宿命),下面我们来看看这个最终的构造方法 —— Binding()

public class Binding extends AbstractDeclarable {/** * The binding destination. */public enum DestinationType {/** * Queue destination. */QUEUE,/** * Exchange destination. */EXCHANGE;}private final String destination;private final String exchange;private final String routingKey;private final DestinationType destinationType;    // 构造方法在这里public Binding(String destination, DestinationType destinationType, String exchange,      String routingKey, @Nullable Map<String, Object> arguments) { super(arguments);this.destination = destination;this.destinationType = destinationType;this.exchange = exchange;this.routingKey = routingKey;}public String getDestination() {return this.destination;}public DestinationType getDestinationType() {return this.destinationType;}public String getExchange() {return this.exchange;}public String getRoutingKey() {return this.routingKey;}public boolean isDestinationQueue() {return DestinationType.QUEUE.equals(this.destinationType);}}

 
可以看到,这个构造方法和我上面的例子有共同点也有不同点,分别是:

  • 共同点:
    • 都没有提供Setter方法,而是在Bulder(这里是BindingBuilder)中完成各属性的Setter
    • 都是依照建造者模式,在最终调用待使用对象的构造方法,来构造目标对象。
  • 不同点:
    • 我写的例子中Builder的Setter返回的总是Builder自身,所以在目标对象的构造方法中传入的也是Builder
    • BindingBuilder的Setter每一步返回的是不同的对象,然后在目标对象的构造方法中传入的又是各参数。
    • 目标对象Binding的构造方法是public的(这也是为什么我上面说可以改用另外一种形式绑定QUEUE和Exchange) 
       

那RabbitMQ这样运用建造者模式出于什么目的呢?我觉得有两点:

  • 区别于我上面说的再构造之前统一校验,Rabbit直接在构造的时候就把该设定的属性和先后顺序定死,你必须根据它的来,也就是说提供的方法你必须bind一个Queue才能to一个Exchange然后在看是否with一个routingKey(不能倒过来,或者说有routingKey就一定要有Exchange必然有Queue,这在我上面的例子是放在最终构造方法前统一校验的)。
  • 第二个目的,兄嘚,别的不说,你就看看这取名,意思明显的还用我重复吗(人家的关注点根本没在目标对象构造完后,外部是否可修改)?
     
     

【 总结 】

建造者模式的核心思想是为了让构建语义更加清晰明了,它们都离不开下面几点:
 

  • 目标对象一定是通过 另一个对象 构建出来的
  • 不管中间的构建过程怎样(你可以返回构建者自身,也可以像RabbitMQ一样返回套娃类),最终都会调用目标对象的构造方法 —— 这点满足了建造出来的对象一定是 有效对象
  • 结合实际场景,注重Setter方法的 命名 可以让 语义进一步清晰
     

   张无忌学剑法的那个故事你知道么?不知道也没关系,大意是 —— 一个叫张无忌的人学剑法,学到最后忘记剑法的一招一式了,张三丰却说可以去PK了,这是为什么呢?因为它已经把剑法 意会于心 了,其实不管是设计模式还是其他技能的学习,都能看到这样类似的影子。

 
    假如今天我依然给你看上面的UML类图,然后噼里啪啦糊弄一顿,然后扬长而去。改天你再其它地方看到建造者模式,发现和类图上描述的不一样。你是怀疑你自己呢?还是怀疑代码的书写者呢?就比如说我上面的这两个例子,就已经构成区别了,而且我认为建造者模式是最不适合用一张UML类图表述清除的设计模式之一,如果你执意要生搬硬套,那你就输了…
 
你品你细品,我是螺丝刀,今天的建造者模式演绎就到此为止,如果有任何建议或者是意见请直接评论区留言……

ps:最近莫名其妙参加了CSDN的新星计划,所以这是我第一次这么用心的肝一篇文章,不求浏览量的多少,只要此文能给到你实质上的帮助就好,觉得有帮助请点赞,我是螺丝刀,我们有空再聊,洗洗睡了,拜拜 ~ ​ 🏃

 
 

在这里插入图片描述

老人咖美文网