概念
-
将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示。它使将一个复杂的对象分解成多个简单的对象,然后一步步构建而成。
-
每一个具体建造者都相对独立,而与其它的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合“开闭原则”。
未用建造者模式
-
以下举个最简单的例子:电脑配件(包括品牌、价格、描述)、组装电脑。
电脑接口
/**
* 电脑接口
*/
public interface Computer {
/**
* 组件(主机Host、显示器Monitor、鼠标Mouse、键盘Keyboard)
*/
String parts();
/**
* 品牌
*/
String brand();
/**
* 价格
*/
Double price();
/**
* 描述
*/
String desc();
}
主机Host
/**
* 惠普主机
*/
public class HPHost implements Computer {
@Override
public String parts() {
return "惠普主机";
}
@Override
public String brand() {
return "惠普品牌";
}
@Override
public Double price() {
return 6999.00;
}
@Override
public String desc() {
return "HP Computer Welcome";
}
}
/**
* 联想主机
*/
public class LenovoHost implements Computer {
@Override
public String parts() {
return "联想主机";
}
@Override
public String brand() {
return "联想品牌";
}
@Override
public Double price() {
return 6899.00;
}
@Override
public String desc() {
return "Lenovo Computer Welcome";
}
}
显示器Monitor
/**
* 小米显示器
*/
public class RedmiMonitor implements Computer {
@Override
public String parts() {
return "小米显示器";
}
@Override
public String brand() {
return "小米品牌";
}
@Override
public Double price() {
return 1399.00;
}
@Override
public String desc() {
return "Redmi Monitor Welcome";
}
}
/**
* 华硕显示器
*/
public class ROGMonitor implements Computer {
@Override
public String parts() {
return "华硕显示器";
}
@Override
public String brand() {
return "华硕品牌";
}
@Override
public Double price() {
return 1899.00;
}
@Override
public String desc() {
return "ROG Monitor Welcome";
}
}
鼠标Monse
/**
* 罗技鼠标
*/
public class GMouse implements Computer {
@Override
public String parts() {
return "罗技鼠标";
}
@Override
public String brand() {
return "罗技品牌";
}
@Override
public Double price() {
return 139.00;
}
@Override
public String desc() {
return "G Mouse Welcome";
}
}
/**
* 联想鼠标
*/
public class LenovoMouse implements Computer {
@Override
public String parts() {
return "联想鼠标";
}
@Override
public String brand() {
return "联想品牌";
}
@Override
public Double price() {
return 89.00;
}
@Override
public String desc() {
return "Lenovo Mouse Welcome";
}
}
键盘Keyboard
/**
* 罗技键盘
*/
public class GKeyboard implements Computer {
@Override
public String parts() {
return "罗技键盘";
}
@Override
public String brand() {
return "罗技品牌";
}
@Override
public Double price() {
return 239.00;
}
@Override
public String desc() {
return "G Keyboard Welcome";
}
}
/**
* 惠普键盘
*/
public class HPKeyboard implements Computer {
@Override
public String parts() {
return "惠普键盘";
}
@Override
public String brand() {
return "惠普品牌";
}
@Override
public Double price() {
return 89.00;
}
@Override
public String desc() {
return "HP Keyboard Welcome";
}
}
组装电脑
**
* 组装电脑
* 不同的套装配不同的设备
*/
public class PackageComputer {
/**
* 根据套餐数字对应返回整套电脑配置详情
*
* @param choose 套餐数字
* @return 电脑配置
*/
public String getComputer(Integer choose) {
// 价格初始值
double price;
// 组装电脑配件
List<Computer> parts = new ArrayList<>();
StringBuilder stringBuilder = new StringBuilder();
if(choose == 1) {
HPHost hpHost = new HPHost();
RedmiMonitor redmiMonitor = new RedmiMonitor();
LenovoMouse lenovoMouse = new LenovoMouse();
HPKeyboard hpKeyboard = new HPKeyboard();
// 组装电脑
parts.add(hpHost);
parts.add(redmiMonitor);
parts.add(lenovoMouse);
parts.add(hpKeyboard);
// 计算价格
price = hpHost.price() + redmiMonitor.price() + lenovoMouse.price() + hpKeyboard.price();
stringBuilder.append("套餐为:" + choose + "号套餐\r\n");
stringBuilder.append("配件如下:\r\n");
for(Computer c : parts) {
stringBuilder.append(c.parts() + "、");
stringBuilder.append(c.brand() + "、");
stringBuilder.append(c.price() + "、");
stringBuilder.append(c.desc() + "\r\n");
}
stringBuilder.append("总价格为:" + price + "RMB\r\n");
} else if(choose == 2) {
LenovoHost lenovoHost = new LenovoHost();
ROGMonitor rogMonitor = new ROGMonitor();
GMouse gMouse = new GMouse();
GKeyboard gKeyboard = new GKeyboard();
// 组装电脑
parts.add(lenovoHost);
parts.add(rogMonitor);
parts.add(gMouse);
parts.add(gKeyboard);
// 计算价格
price = lenovoHost.price() + rogMonitor.price() + gMouse.price() + gKeyboard.price();
stringBuilder.append("套餐为:" + choose + "号套餐\r\n");
stringBuilder.append("配件如下:\r\n");
for(Computer c : parts) {
stringBuilder.append(c.parts() + "、");
stringBuilder.append(c.brand() + "、");
stringBuilder.append(c.price() + "、");
stringBuilder.append(c.desc() + "\r\n");
}
stringBuilder.append("总价格为:" + price + "RMB\r\n");
} else if(choose == 3) {
LenovoHost lenovoHost = new LenovoHost();
RedmiMonitor redmiMonitor = new RedmiMonitor();
GMouse gMouse = new GMouse();
LenovoMouse lenovoMouse = new LenovoMouse();
// 组装电脑
parts.add(lenovoHost);
parts.add(redmiMonitor);
parts.add(gMouse);
parts.add(lenovoMouse);
// 计算价格
price = lenovoHost.price() + redmiMonitor.price() + gMouse.price() + lenovoMouse.price();
stringBuilder.append("套餐为:" + choose + "号套餐\r\n");
stringBuilder.append("配件如下:\r\n");
for(Computer c : parts) {
stringBuilder.append(c.parts() + "、");
stringBuilder.append(c.brand() + "、");
stringBuilder.append(c.price() + "、");
stringBuilder.append(c.desc() + "\r\n");
}
stringBuilder.append("总价格为:" + price + "RMB\r\n");
}
return stringBuilder.toString();
}
}
测试
public class BuilderDesign {
public static void main(String[] args) {
PackageComputer computer = new PackageComputer();
System.out.println(computer.getComputer(1));
System.out.println("=======================================================");
System.out.println(computer.getComputer(2));
System.out.println("=======================================================");
System.out.println(computer.getComputer(3));
}
}
使用建造者模式
-
从上面可以看出来,电脑的每个配件都要去建对应的类。例子中我给了主机、显示器、鼠标、键盘四种部件,每个部件假设两种品牌,就写了 2 * 4 = 8个类。虽说不会是指数型增长,但是无论哪个增加都会是很明显的增长趋势。而且在组装电脑时,要根据每个不同要求的去返回对应的信息,每一个if语句都有二十行代码左右,看起来十分臃肿。
-
接下来将会用到建造者模式去优化上面的代码量。
组装电脑接口
public interface IComputer {
/**
* 主机
*/
IComputer appendHost(Computer computer);
/**
* 显示器
*/
IComputer appendMonitor(Computer computer);
/**
* 鼠标
*/
IComputer appendMouse(Computer computer);
/**
* 键盘
*/
IComputer appendKeyboard(Computer computer);
/**
* @return 电脑清单
*/
String computerDetail();
}
建造者组装电脑
/**
* 建造者组装电脑
*/
public class BuilderComputer implements IComputer{
List<Computer> parts = new ArrayList<>();
private double price = 0.00;
private Integer choose;
public BuilderComputer(){}
public BuilderComputer(Integer choose) {
this.choose = choose;
}
@Override
public IComputer appendHost(Computer computer) {
parts.add(computer);
price = price + computer.price();
return this;
}
@Override
public IComputer appendMonitor(Computer computer) {
parts.add(computer);
price = price + computer.price();
return this;
}
@Override
public IComputer appendMouse(Computer computer) {
parts.add(computer);
price = price + computer.price();
return this;
}
@Override
public IComputer appendKeyboard(Computer computer) {
parts.add(computer);
price = price + computer.price();
return this;
}
@Override
public String computerDetail() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("套餐为:" + choose + "号套餐\r\n");
stringBuilder.append("配件如下:\r\n");
for(Computer c : parts) {
stringBuilder.append(c.parts() + "、");
stringBuilder.append(c.brand() + "、");
stringBuilder.append(c.price() + "、");
stringBuilder.append(c.desc() + "\r\n");
}
stringBuilder.append("总价格为:" + price + "RMB\r\n");
return stringBuilder.toString();
}
}
建造者
去掉了繁琐的if else,符合单一职责原则、开闭原则,代码可读性、复用性、拓展性强。这里面就完美的展示了什么叫做将一个复杂对象的构造与它的表示分离。并且链式编程的语法比不断的set()要美观得多,这会在后续Lambok中的@Builder中进行说明。
/**
* 建造者
*/
public class Builder {
/**
* @return 一号套餐
*/
public IComputer chooseOne() {
return new BuilderComputer(1)
.appendHost(new HPHost())
.appendMonitor(new RedmiMonitor())
.appendMouse(new LenovoMouse())
.appendKeyboard(new HPKeyboard());
}
/**
* @return 二号套餐
*/
public IComputer chooseTwo() {
return new BuilderComputer(2)
.appendHost(new LenovoHost())
.appendMonitor(new ROGMonitor())
.appendMouse(new GMouse())
.appendKeyboard(new GKeyboard());
}
/**
* @return 三号套餐
*/
public IComputer chooseThree() {
return new BuilderComputer(3)
.appendHost(new LenovoHost())
.appendMonitor(new RedmiMonitor())
.appendMouse(new GMouse())
.appendKeyboard(new LenovoMouse());
}
}
测试
public class BuilderDesign {
public static void main(String[] args) {
Builder builder = new Builder();
System.out.println(builder.chooseOne().computerDetail());
System.out.println("=======================================================");
System.out.println(builder.chooseTwo().computerDetail());
System.out.println("=======================================================");
System.out.println(builder.chooseThree().computerDetail());
}
}
@Builder
此注解是Lombok依赖下的,而Lombok基本是各个公司都会使用到的工具包。可以用来简化开发。上面的建造者组装电脑的示例代码就是链式编程的关键之处:每个方法除了会传参还会返回this自身。我创建了一个用户User类,其带有六个属性。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
private String sex;
private Integer age;
private String address;
private String qq;
private String email;
}
底层
为了验证此注解背后的样子,最简单的实践方法就是加上此注解然后查看编译后的class文件中的代码。等编译后我发现多了以下内容。会发现多了一个静态内部类UserBuilder以及返回User.UserBuilder的build()方法。
其实User中的builder()方法以及User类的静态内部类UserBuilder的build()方法。这两个方法名在@Builder注解中已经是默认的值了。并且或者注解可以用于类、普通方法和构造方法上。关于其底层是如何在User类中生成静态内部类并且具体的方法代码块就不深究Lombok中的源码了。这里我需要强调的是使用建造者赋值的时候就是赋值给其内部类属性的。
优势
可读性好
其实当使用过@Builder这个注解的时候就已经可以感受到它的好处之一了:美观且可读性高。这里我使用了三种创建对象的方式来作比较出优劣处。
第一个User对象使用有参构造的真是长的让人反胃,甚至如果在真实的复杂业务场景中,还不知道其中一个参数是什么含义,还需要点进去看注释。并且自己使用这种有参构造的话,如果没有背下来每个位置要放什么参数那就更麻烦了。所以说有参构造的劣势就是:可读性差、参数过多可能导致传递错误。
第二个User对象就是一直Setter。相比于第三种而言没有那么好的可读性。所以说使用建造者模式的链式编程可读性好。但是要记住建造者模式的赋值是给其内部类属性的。
public class BuilderDesign {
public static void main(String[] args) {
User u1 = new User("张三x", "男", 18, "福建省厦门市xxx镇xxxx小区x楼xxx号", "465795464", "465795464@qq.com");
User u2 = new User();
u2.setUsername("李四");
u2.setSex("女");
u2.setAge(20);
u2.setAddress("福建省泉州市xxx镇xxxx小区x楼xxx号");
u2.setQq("504899214");
u2.setEmail("504899214@qq.com");
User u3 = User.builder()
.username("王五")
.sex("男")
.age(22)
.address("福建省福州市xxx镇xxxx小区x楼xxx号")
.qq("684354768")
.email("684354768@qq.com")
.build();
}
}
JavaBean创建
我曾在某个地方看到一个大佬说过使用set()方法注入属性和静态内部类Builder注入属性值的区别,但具体怎么说的已经忘记了,
这里由衷希望看到这里的读者可以在评论里说一下关于JavaBean赋值可能涉及到的线程安全问题或者其它问题。谢谢。
避坑
在上面有说过一个问题就是:使用builder()方法赋值是赋值给其静态内部类建造者类的。那么这句话是什么意思呢?这句话的意思就是当我们在实体类上已经附带初始值了,但是使用建造者模式去构建实体类打印toString()方法出来的时候是看到为类加载的初始值的(比如0/false/null等)。具体看以下代码以及控制台输出。
public class BuilderDesign {
public static void main(String[] args) {
User u = User.builder()
.username("王五")
.sex("男")
.address("福建省福州市xxx镇xxxx小区x楼xxx号")
.qq("684354768")
.email("684354768@qq.com")
.build();
System.out.println(u);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class User {
private String username;
private String sex;
private Integer age = 30;
private String address;
private String qq;
private String email;
}
可以看到age = null。因为age是包装类型Integer,所以类加载时的初始值为null,而不是0。这里的原因就是User的age属性初始值为30,但是其内部的UserBuilder类的age属性并没有,所以导致获取到的User对象的age属性为初始值null。为了避免这个情况发生,@Builder注解中有一个内部注解来解决这个问题,就是@Builder.Default。只需要在设置初始值的属性上使用此注解即可。编译生成的User对象会多生成个静态的$default$age()方法。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
private String sex;
@Builder.Default
private Integer age = 30;
private String address;
private String qq;
private String email;
}