建造者模式
1.定义
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示
2.使用场景
当一个对象的创建过程很复杂,类的构造函数参数个数比较多,而且这些参数有些是可选的参数,考虑使用构造者模式。
举例:例如我们现在有如下一个类计算机类Computer,其中cpu与ram是必填参数,而其他3个是可选参数,那么我们如何构造这个类的实例呢,通常有两种常用的方式:
public class Computer {
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
public Computer(String cpu, String ram) {
this(cpu, ram, 0);
}
public Computer(String cpu, String ram, int usbCount) {
this(cpu, ram, usbCount, "罗技键盘");
}
public Computer(String cpu, String ram, int usbCount, String keyboard) {
this(cpu, ram, usbCount, keyboard, "三星显示器");
}
public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
this.cpu = cpu;
this.ram = ram;
this.usbCount = usbCount;
this.keyboard = keyboard;
this.display = display;
}
}
第二种:Javabean 模式,就是通过set方式赋值
public class Computer {
...
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getRam() {
return ram;
}
public void setRam(String ram) {
this.ram = ram;
}
public int getUsbCount() {
return usbCount;
}
...
}
那么这两种方式有什么弊端呢? 第一种主要是使用及阅读不方便。你可以想象一下,当你要调用一个类的构造函数时,你首先要决定使用哪一个,然后里面又是一堆参数,如果这些参数的类型很多又都一样,你还要搞清楚这些参数的含义,很容易就传混了 ,
第二种方式在构建过程中对象的状态容易发生变化,造成错误。因为那个类中的属性是分步设置的,所以就容易出错。为了解决这两个痛点,builder模式就横空出世了。
3.如何实现建造者模式
- 创建一个产品类,这个类是建造者模式中,要返回的产品对象的类
package com.gupaoedu.vip.pattern.builder;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class Computer {
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
// 原来的写法:通过重载构造方法,通过不同的参数组合,获取不同的对象
/*public Computer(String cpu, String ram) {
this(cpu, ram, 0);
}
public Computer(String cpu, String ram, int usbCount) {
this(cpu, ram, usbCount, "罗技键盘");
}
public Computer(String cpu, String ram, int usbCount, String keyboard) {
this(cpu, ram, usbCount, keyboard, "三星显示器");
}
public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
this.cpu = cpu;
this.ram = ram;
this.usbCount = usbCount;
this.keyboard = keyboard;
this.display = display;
}*/
}
- 创建一个抽象的建造者接口(简写模式下,可以不用定义该接口)
package com.gupaoedu.vip.pattern.builder;
public interface IComputerBuilder {
Computer build(String cpu,String ram) ;
}
- 建造者:持有要构建的对象
在建造者模式中,为产品属性赋值,原本的返回值类似是void,但是我们把返回值类型改为建造者本身,通过各个set方法,为产品对象的属性分别 赋值, 在赋值之后,返回建造者这个类自己(这样写的好处就是调用者使用时可以进行链式调用,非常方便),然后在build方法中,返回最终创建好的对象给外部调用者。
package com.gupaoedu.vip.pattern.builder;
/**
* 建造者
*/
public class ComputerBuilder implements IComputerBuilder{
// 要构建的对象
private Computer computer = new Computer();
// build方法返回构建好的完整的对象
@Override
public Computer build(String cpu,String ram) {
computer.setCpu(cpu);
computer.setRam(ram);
return computer;
}
public ComputerBuilder setDisplay(String display) {
computer.setDisplay(display);
return this;
}
public ComputerBuilder setKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
return this;
}
public ComputerBuilder setUsbCount(int usbCount) {
computer.setUsbCount(usbCount);
return this;
}
}
- Director: 自由构建对象
package com.gupaoedu.vip.pattern.builder;
public class Director {
public static void main(String[] args) {
ComputerBuilder computerBuilder = new ComputerBuilder();
//1.每次都是由builder去操作这个对象,对使用者来讲,对象的创建过程被封装了起来
//2.对于可选参数,用户可以设置,也可以不设置,先设置哪个,后设置哪个,都不影响对象的构建,使用者可以自由定制
// 缺点:产生多余的builder对象;如果产品内部发生变化,建造者都要修改
computerBuilder.setDisplay("27寸显示器")
.setKeyboard("罗技键盘")
.setUsbCount(3);
Computer computer = computerBuilder.build("英特尔cpu", "三星ram");
System.out.println(computer);
StringBuilder stringBuilder = new StringBuilder();
}
}
测试:
假如我现在不想要键盘,那么直接使用 computerBuilder.setDisplay(“27寸显示器”).setUsbCount(3); 即可,是不是很方便!
再也不用考虑蛋疼的属性排列组合问题了!
4. 建造者模式的优点
- 封装性好,对象的创建和使用分离
- 扩展性好,建造类之间独立,一定 程度上解耦
5.建造者模式的缺点
- 原本只需要构造方法就完成了对象的创建,使用建造者模式之后,额外产生了builder对象
- 产品内部一旦发生变化,建造者内部也得跟着修改,维护不方便,不符合开闭原则
6.建造者模式与工厂模式区别
- 建造者模式更加注重方法的调用顺序,工厂模式注重于创建对象(不同工厂创建不同的对象 )
- 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样
- 关注点不同:工厂模式只要把对象创建出来就可以了,建造者模式不仅要创建出对象,还要知道这个对象由哪些部件组成
- 建造者模式根据建造过程中的顺序不同,最终的对象部件组成也不同。
7. 建造者模式的实际应用场景
Mybatis Generator相信很多人都用过,如下代码,获取一个example,然后调用example.createCriteria() 创建一个Criteria 对象,然后设置查询条件:
public EUDataGridResult getContentList(Integer page, Integer rows, Long categoryId) {
TbContentExample example = new TbContentExample();
// 设置分页条件,必须写在设置查询条件之前
PageHelper.startPage(page, rows);
Criteria criteria = example.createCriteria();
criteria.andCategoryIdEqualTo(categoryId);
List<TbContent> contentList = mapper.selectByExample(example);
// 封装返回对象
EUDataGridResult result = new EUDataGridResult();
result.setRows(contentList);
// 通过pagehelper jar包的PageInfo类 获取记录总条数
result.setTotal(new PageInfo<>(contentList).getTotal());
return result;
}
如上,这里的查询条件是不确定的,那么Mybatis Generator生成出来的代码里面对于Criteria 设置查询条件是怎么写的呢?
public Criteria andCategoryIdEqualTo(Long value) {
addCriterion("category_id =", value, "categoryId");
return (Criteria) this;
}
public Criteria andCategoryIdNotEqualTo(Long value) {
addCriterion("category_id <>", value, "categoryId");
return (Criteria) this;
}
public Criteria andCategoryIdGreaterThan(Long value) {
addCriterion("category_id >", value, "categoryId");
return (Criteria) this;
}
可以看到,这里andCategoryIdEqualTo方法返回的是Criteria 对象,这个Criteria 对象其实就是一个建造者模式的一种应用