|
| 1 | +# 建造者模式 |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +> StringBuilder 你肯定用过,JDK 中的建造者模式 |
| 6 | +> |
| 7 | +> lombok 中的 @Bulider,你可能也用过,恩,这也是我们要说的建造者模式 |
| 8 | +
|
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +> 直接使用构造函数或者配合 set 方法就能创建对象,为什么还需要建造者模式来创建呢? |
| 14 | +> |
| 15 | +> 建造者模式和工厂模式都可以创建对象,那它们两个的区别在哪里呢? |
| 16 | +
|
| 17 | +## 简介 |
| 18 | + |
| 19 | +Builder Pattern,中文翻译为**建造者模式**或者**构建者模式**,也有人叫它**生成器模式**。 |
| 20 | + |
| 21 | +**建造者模式**是一种创建型设计模式, 使你能够分步骤创建复杂对象。它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。 |
| 22 | + |
| 23 | +**定义**:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +## hello world |
| 32 | + |
| 33 | +程序员麽,先上个 `hello world` 热热身 |
| 34 | + |
| 35 | +```java |
| 36 | +public class User { |
| 37 | + |
| 38 | + private Long id; |
| 39 | + private String name; |
| 40 | + private Integer age; //可选 |
| 41 | + private String desc; //可选 |
| 42 | + |
| 43 | + private User(Builder builder) { |
| 44 | + this.id = builder.id; |
| 45 | + this.name = builder.name; |
| 46 | + this.age = builder.age; |
| 47 | + this.desc = builder.desc; |
| 48 | + } |
| 49 | + |
| 50 | + public static Builder newBuilder(Long id, String name) { |
| 51 | + return new Builder(id, name); |
| 52 | + } |
| 53 | + |
| 54 | + public Long getId() {return id;} |
| 55 | + public String getName() {return name;} |
| 56 | + public Integer getAge() {return age;} |
| 57 | + public String getDesc() {return desc;} |
| 58 | + |
| 59 | + @Override |
| 60 | + public String toString() { |
| 61 | + return "Builder{" + |
| 62 | + "id=" + id + |
| 63 | + ", name='" + name + '\'' + |
| 64 | + ", age=" + age + |
| 65 | + ", desc='" + desc + '\'' + |
| 66 | + '}'; |
| 67 | + } |
| 68 | + |
| 69 | + public static class Builder { |
| 70 | + private Long id; |
| 71 | + private String name; |
| 72 | + private Integer age; |
| 73 | + private String desc; |
| 74 | + |
| 75 | + private Builder(Long id, String name) { |
| 76 | + Assert.assertNotNull("标识不能为空",id); |
| 77 | + Assert.assertNotNull("名称不能为空",name); |
| 78 | + this.id = id; |
| 79 | + this.name = name; |
| 80 | + } |
| 81 | + public Builder age(Integer age) { |
| 82 | + this.age = age; |
| 83 | + return this; |
| 84 | + } |
| 85 | + public Builder desc(String desc) { |
| 86 | + this.desc = desc; |
| 87 | + return this; |
| 88 | + } |
| 89 | + public User build() { |
| 90 | + return new User(this); |
| 91 | + } |
| 92 | + |
| 93 | + } |
| 94 | + |
| 95 | + public static void main(String[] args) { |
| 96 | + User user = User.newBuilder(1L, "starfish").age(22).desc("test").build(); |
| 97 | + System.out.println(user.toString()); |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +这样的代码有什么优缺点呢? |
| 103 | + |
| 104 | +主要优点: |
| 105 | + |
| 106 | +1. 明确了必填参数和可选参数,在构造方法中进行验证; |
| 107 | +2. 可以定义为不可变类,初始化后属性字段值不可变更; |
| 108 | +3. 赋值代码可读性较好,明确知道哪个属性字段对应哪个值; |
| 109 | +4. 支持链式方法调用,相比于调用 Setter 方法,代码更简洁。 |
| 110 | + |
| 111 | +主要缺点: |
| 112 | + |
| 113 | +1. 代码量较大,多定义了一个 Builder 类,多定义了一套属性字段,多实现了一套赋值方法; |
| 114 | +2. 运行效率低,需要先创建 Builder 实例,再赋值属性字段,再创建目标实例,最后拷贝属性字段。 |
| 115 | + |
| 116 | +> 当然,以上代码,就可以通过 Lombok 的 @Builder 简化代码 |
| 117 | +> |
| 118 | +> 如果我们就那么三三两两个参数,直接构造函数配合 set 方法就能搞定的,就不用套所谓的模式了。 |
| 119 | +> |
| 120 | +> 高射炮打蚊子——不合算 |
| 121 | +> |
| 122 | +> 假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。 |
| 123 | +> |
| 124 | +> 这时候才是构造器模式上场的时候 |
| 125 | +
|
| 126 | + |
| 127 | + |
| 128 | +上边的例子,其实属于简化版的建造者模式,只是为了方便构建类中的各个参数,”正经“的和这个有点差别,更倾向于用同样的构建过程分步创建不同的产品类。 |
| 129 | + |
| 130 | +我们接着扯~ |
| 131 | + |
| 132 | +## 结构 |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | +从 UML 图上可以看到有 4 个不同的角色 |
| 137 | + |
| 138 | +- 抽象建造者(Builder):创建一个 Produc 对象的各个部件指定的接口/抽象类 |
| 139 | +- 具体建造者(ConcreteBuilder):实现接口,构建和装配各个组件 |
| 140 | +- 指挥者/导演类(Director):构建一个使用 Builder 接口的对象。负责调用适当的建造者来组建产品,导演类一般不与产品类发生依赖关系,与导演类直接交互的是建造者类。 |
| 141 | +- 产品类(Product):一个具体的产品对象 |
| 142 | + |
| 143 | + |
| 144 | + |
| 145 | +## demo |
| 146 | + |
| 147 | +假设我是个汽车工厂,需求就是能造各种车(或者造电脑、造房子、做煎饼、生成不同文件TextBuilder、HTMLBuilder等等,都是一个道理) |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | +1、生成器(Builder)接口声明在所有类型生成器中通用的产品构造步骤 |
| 152 | + |
| 153 | +```java |
| 154 | +public interface CarBuilder { |
| 155 | + void setCarType(CarType type); |
| 156 | + void setSeats(int seats); |
| 157 | + void setEngine(Engine engine); |
| 158 | + void setGPS(GPS gps); |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +2、具体的生成器(Concrete Builders)提供构造过程的不同实现 |
| 163 | + |
| 164 | +```java |
| 165 | +public class SportsCarBuilder implements CarBuilder { |
| 166 | + |
| 167 | + private CarType carType; |
| 168 | + private int seats; |
| 169 | + private Engine engine; |
| 170 | + private GPS gps; |
| 171 | + |
| 172 | + @Override |
| 173 | + public void setCarType(CarType type) { |
| 174 | + this.carType = type; |
| 175 | + } |
| 176 | + |
| 177 | + @Override |
| 178 | + public void setSeats(int seats) { |
| 179 | + this.seats = seats; |
| 180 | + } |
| 181 | + |
| 182 | + @Override |
| 183 | + public void setEngine(Engine engine) { |
| 184 | + this.engine = engine; |
| 185 | + } |
| 186 | + |
| 187 | + @Override |
| 188 | + public void setGPS(GPS gps) { |
| 189 | + this.gps = gps; |
| 190 | + } |
| 191 | + |
| 192 | + public Car getResult() { |
| 193 | + return new Car(carType, seats, engine, gps); |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +3、产品(Products)是最终生成的对象 |
| 199 | + |
| 200 | +```java |
| 201 | +@Setter |
| 202 | +@Getter |
| 203 | +@ToString |
| 204 | +public class Car { |
| 205 | + |
| 206 | + private final CarType carType; |
| 207 | + private final int seats; |
| 208 | + private final Engine engine; |
| 209 | + private final GPS gps; |
| 210 | + private double fuel; |
| 211 | + |
| 212 | + public Car(CarType carType,int seats,Engine engine,GPS gps){ |
| 213 | + this.carType = carType; |
| 214 | + this.seats = seats; |
| 215 | + this.engine = engine; |
| 216 | + this.gps = gps; |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +4、主管(Director)类定义调用构造步骤的顺序,这样就可以创建和复用特定的产品配置(Director 类的构造函数的参数是 CarBuilder,但实际上没有实例传递出去作参数,因为 CarBuilder 是接口或抽象类,无法产生对象实例,实际传递的是 Builder 的子类,根据子类类型,决定生产内容) |
| 222 | + |
| 223 | +```java |
| 224 | +public class Director { |
| 225 | + |
| 226 | + public void constructSportsCar(CarBuilder builder){ |
| 227 | + builder.setCarType(CarType.SPORTS_CAR); |
| 228 | + builder.setSeats(2); |
| 229 | + builder.setEngine(new Engine(2.0,0)); |
| 230 | + builder.setGPS(new GPS()); |
| 231 | + } |
| 232 | + |
| 233 | + public void constructCityCar(CarBuilder builder){ |
| 234 | + builder.setCarType(CarType.CITY_CAR); |
| 235 | + builder.setSeats(4); |
| 236 | + builder.setEngine(new Engine(1.5,0)); |
| 237 | + builder.setGPS(new GPS()); |
| 238 | + } |
| 239 | + |
| 240 | + public void constructSUVCar(CarBuilder builder){ |
| 241 | + builder.setCarType(CarType.SUV); |
| 242 | + builder.setSeats(4); |
| 243 | + builder.setEngine(new Engine(2.5,0)); |
| 244 | + builder.setGPS(new GPS()); |
| 245 | + } |
| 246 | + |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +5、客户端使用(最终结果从建造者对象中获取,主管并不知道最终产品的类型) |
| 251 | + |
| 252 | +```java |
| 253 | +public class Client { |
| 254 | + |
| 255 | + public static void main(String[] args) { |
| 256 | + Director director = new Director(); |
| 257 | + SportsCarBuilder builder = new SportsCarBuilder(); |
| 258 | + director.constructSportsCar(builder); |
| 259 | + |
| 260 | + Car car = builder.getResult(); |
| 261 | + System.out.println(car.toString()); |
| 262 | + } |
| 263 | +} |
| 264 | +``` |
| 265 | + |
| 266 | + |
| 267 | + |
| 268 | +## 适用场景 |
| 269 | + |
| 270 | +适用场景其实才是理解设计模式最重要的,只要知道这个业务场景需要什么模式,网上浪程序员能不会吗 |
| 271 | + |
| 272 | +- **使用建造者模式可避免重叠构造函数的出现**。 |
| 273 | + |
| 274 | + 假设你的构造函数中有 N 个可选参数,那 new 各种实例的时候就很麻烦,需要重载构造函数多次 |
| 275 | + |
| 276 | +- 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用建造者模式。 |
| 277 | + |
| 278 | + 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用建造者模式。 |
| 279 | + |
| 280 | +- **使用生成器构造组合树或其他复杂对象**。 |
| 281 | + |
| 282 | + 建造者模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。 |
| 283 | + |
| 284 | + |
| 285 | + |
| 286 | +## VS 抽象工厂 |
| 287 | + |
| 288 | +抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心抽象过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而生产一个新的产品。 |
| 289 | + |
| 290 | + |
| 291 | + |
| 292 | +## 最后 |
| 293 | + |
| 294 | +设计模式,这玩意看简单的例子,肯定能看得懂,主要是结合自己的业务思考怎么应用,让系统设计更完善,懂了每种模式后,可以找找各种框架源码或在 github 搜搜相关内容,看看实际中是怎么应用的。 |
| 295 | + |
| 296 | + |
| 297 | + |
| 298 | +> 公众号回复 ”设计模式“,领取 10 本设计模式 pdf 书籍 |
| 299 | +
|
| 300 | + |
| 301 | + |
| 302 | +## 参考 |
| 303 | + |
| 304 | +- refactoringguru.cn |
| 305 | + |
0 commit comments