写在前面
❝
Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!
场景模拟
需求描述
❝
我们要构建一个房屋系统,该系统需要包含多种房屋类型,并且这些房屋可能具备不同的特性,如是否带有绿植、泳池、车库等。为此,我们考虑将“房屋”这一概念抽象为一个对应的House类。
第一种方案
首先,我们可以定义一个房屋基类,它就像模拟人生的基础模板,包含房间、门、窗等基本属性。然后,其他类型的房屋可以继承这个基类,并像乐高积木一样,添加各自独特的模块。
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BasisHouse {
/**
* 地址
*/
private String address;
/**
* 房间数
*/
private int numberOfRooms;
/**
* 居住面积
*/
private double livingArea;
/**
* 包括院子、车库等的总面积
*/
private double totalArea;
/**
* 售价
*/
private double price;
/**
* 建造年份
*/
private String constructionYear;
@Override
public String toString() {
return "详细信息: \n\r" +
" 地址: " + address + "\n\r" +
" 房间数: " + numberOfRooms + "间\n\r" +
" 居住面积: " + livingArea + "平米\n\r" +
" 总面积: " + totalArea + "平米\n\r" +
" 价格: " + price + "元\n\r" +
" 建造年份: " + constructionYear + "\n\r";
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class HouseWithGarage extends BasisHouse {
/**
* 车库容量(车辆数)
*/
private int garageCapacity;
/**
* 车库是否加热
*/
private boolean isGarageHeated;
@Override
public String toString() {
return "带有停车房的房屋" + super.toString() +
" 车库容量: " + garageCapacity + "辆\n\r" +
" 车库是否加热: " + isGarageHeated + "\n\r";
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class HouseWithGreenery extends BasisHouse {
/**
* 绿植面积
*/
private double greeneryArea;
/**
* 花园类型(如:日式、欧式、现代等)
*/
private String gardenType;
@Override
public String toString() {
return "带有花园的房屋" + super.toString() +
" 绿植面积: " + greeneryArea + "平米\n\r" +
" 花园类型: " + gardenType + "\n\r";
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class HouseWithPool extends BasisHouse {
/**
* 游泳池是否为室内
*/
private boolean isPoolIndoor;
/**
* 游泳池面积
*/
private double poolArea;
@Override
public String toString() {
return "带有泳池的房屋" + super.toString() +
" 是否为室内: " + isPoolIndoor + "\n\r" +
" 泳池面积: " + poolArea + "平米 \n\r";
}
}
public class HouseTest {
@Test
public void testGetHouseWithGarage() {
HouseWithGarage basisHouse = new HouseWithGarage();
basisHouse.setAddress("XXX省XXX市XXX区220号");
basisHouse.setConstructionYear("2025年2月10日");
basisHouse.setLivingArea(100D);
basisHouse.setTotalArea(130D);
basisHouse.setPrice(1000000D);
basisHouse.setNumberOfRooms(4);
basisHouse.setGarageCapacity(2);
basisHouse.setGarageHeated(true);
System.out.println(basisHouse.toString());
}
@Test
public void testGetHouseWithGreenery() {
HouseWithGreenery basisHouse = new HouseWithGreenery();
basisHouse.setAddress("XXX省XXX市XXX区220号");
basisHouse.setConstructionYear("2025年2月10日");
basisHouse.setLivingArea(100D);
basisHouse.setTotalArea(130D);
basisHouse.setPrice(1000000D);
basisHouse.setNumberOfRooms(4);
basisHouse.setGreeneryArea(50D);
basisHouse.setGardenType("欧式花园");
System.out.println(basisHouse.toString());
}
@Test
public void testGetHouseWithPool() {
HouseWithPool basisHouse = new HouseWithPool();
basisHouse.setAddress("XXX省XXX市XXX区220号");
basisHouse.setConstructionYear("2025年2月10日");
basisHouse.setLivingArea(100D);
basisHouse.setTotalArea(130D);
basisHouse.setPrice(1000000D);
basisHouse.setNumberOfRooms(4);
basisHouse.setPoolArea(30D);
basisHouse.setPoolIndoor(true);
System.out.println(basisHouse.toString());
}
}
出现的问题: 随着房屋类型逐渐增多,你会发现子类数量庞大,如同乐高积木的款式一样繁多,难以管理。
第二种方案
将所有可能的属性都集中在基类中,创建房屋时根据需求填写相应的属性。
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class House {
/**
* 地址
*/
private String address;
/**
* 房间数
*/
private Integer numberOfRooms;
/**
* 居住面积
*/
private Double livingArea;
/**
* 包括院子、车库等的总面积
*/
private Double totalArea;
/**
* 售价
*/
private Double price;
/**
* 建造年份
*/
private String constructionYear;
/**
* 房屋类型
*/
private String type;
/**
* 车库容量(车辆数)
*/
private Integer garageCapacity;
/**
* 车库是否加热
*/
private Boolean isGarageHeated;
/**
* 绿植面积
*/
private Double greeneryArea;
/**
* 花园类型(如:日式、欧式、现代等)
*/
private String gardenType;
/**
* 游泳池是否为室内
*/
private Boolean isPoolIndoor;
/**
* 游泳池面积
*/
private Double poolArea;
@Override
public String toString() {
return "房屋信息: \n\r" +
" 房屋类型: " + type + "\n\r" +
" 地址: " + address + "\n\r" +
" 房间数: " + numberOfRooms + "间\n\r" +
" 居住面积: " + livingArea + "平米\n\r" +
" 总面积: " + totalArea + "平米\n\r" +
" 价格: " + price + "元\n\r" +
" 建造年份: " + constructionYear + "\n\r" +
" 车库容量: " + garageCapacity + "\n\r" +
" 车库是否加热: " + isGarageHeated + "\n\r" +
" 绿植面积: " + greeneryArea + "\n\r" +
" 花园类型: " + gardenType + "\n\r" +
" 游泳池是否为室内: " + isPoolIndoor + "\n\r" +
" 游泳池面积: " + poolArea + "\n\r";
}
}
public class HouseTest {
@Test
public void testGetHouseWithGarage() {
House house = new House("XXX省XXX市XXX区220号",4,100D,130D,1000000D,"2025年2月10日","带有停车房的房屋",2,true,null,null,null,null);
System.out.println(house.toString());
}
@Test
public void testGetHouseWithGreenery() {
House house = new House("XXX省XXX市XXX区220号",4,100D,130D,1000000D,"2025年2月10日","带有花园的房屋",null,null,50D,"欧式花园",null,null);
System.out.println(house.toString());
}
@Test
public void testGetHouseWithPool() {
House house = new House("XXX省XXX市XXX区220号",4,100D,130D,1000000D,"2025年2月10日","带有游泳池的房屋",null,null,null,null,true,30D);
System.out.println(house.toString());
}
}
出现的问题: 随着属性的增多,每次创建房屋对象都变得像填写一份冗长的调查问卷。要么使用全参构造函数(调用起来非常繁琐),要么为每种房屋类型提供一个专门的构造方法(维护起来极其复杂)。
结论 显然,当前的房屋系统在实现过程中已经显现出复杂性和维护难度,因此,我们迫切需要一种更为灵活高效的解决方案来应对这些挑战。而建造者模式正是解决这类问题的有效方法。
1.定义
建造者模式是一种 创建型设计模式,它将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
2.核心角色
-
产品(Product) :
- 最终要构建的复杂对象。
- 例如:一辆汽车、一份套餐、一个文档。
-
建造者(Builder) :
- 定义构建产品的各个步骤的接口。
- 例如:定义如何安装引擎、如何安装车轮。
-
具体建造者(Concrete Builder) :
- 实现建造者接口,完成具体产品的构建。
- 例如:具体实现如何安装引擎、如何安装车轮。
-
指挥者(Director) :
- 负责调用建造者的方法,按照一定的顺序构建产品。
- 例如:指挥先安装引擎,再安装车轮。
3.使用场景
-
对象构造过程复杂:
- 当一个对象的构造过程涉及多个步骤,且这些步骤的顺序和逻辑复杂。
- 例如:构建一辆汽车需要安装引擎、车轮、座椅等。
-
对象属性多且可选:
- 当对象的属性很多,且某些属性是可选的时。
- 例如:构建一份套餐,可以选择是否加饮料、是否加甜点。
-
需要构建不同表示的对象:
- 当需要构建多个不同表示的对象,但构建过程相似时。
- 例如:构建豪华版汽车和经济版汽车。
4.重构房屋对象
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class House {
/**
* 地址
*/
private String address;
/**
* 房间数
*/
private Integer numberOfRooms;
/**
* 居住面积
*/
private Double livingArea;
/**
* 包括院子、车库等的总面积
*/
private Double totalArea;
/**
* 售卖价格
*/
private Double price;
/**
* 建造年份
*/
private String constructionYear;
/**
* 房屋类型
*/
private String type;
/**
* 车库容量(车辆数)
*/
private Integer garageCapacity;
/**
* 车库是否加热
*/
private Boolean isGarageHeated;
/**
* 绿植面积
*/
private Double greeneryArea;
/**
* 花园类型(如:日式、欧式、现代等)
*/
private String gardenType;
/**
* 游泳池是否为室内
*/
private Boolean isPoolIndoor;
/**
* 游泳池面积
*/
private Double poolArea;
private House(Builder builder) {
this.address = builder.address;
this.numberOfRooms = builder.numberOfRooms;
this.livingArea = builder.livingArea;
this.totalArea = builder.totalArea;
this.price = builder.price;
this.constructionYear = builder.constructionYear;
this.type = builder.type;
this.garageCapacity = builder.garageCapacity;
this.isGarageHeated = builder.isGarageHeated;
this.greeneryArea = builder.greeneryArea;
this.gardenType = builder.gardenType;
this.isPoolIndoor = builder.isPoolIndoor;
this.poolArea = builder.poolArea;
}
@Override
public String toString() {
return "房屋信息: \n\r" +
" 房屋类型: " + type + "\n\r" +
" 地址: " + address + "\n\r" +
" 房间数: " + numberOfRooms + "间\n\r" +
" 居住面积: " + livingArea + "平米\n\r" +
" 总面积: " + totalArea + "平米\n\r" +
" 价格: " + price + "元\n\r" +
" 建造年份: " + constructionYear + "\n\r" +
" 车库容量: " + garageCapacity + "\n\r" +
" 车库是否加热: " + isGarageHeated + "\n\r" +
" 绿植面积: " + greeneryArea + "\n\r" +
" 花园类型: " + gardenType + "\n\r" +
" 游泳池是否为室内: " + isPoolIndoor + "\n\r" +
" 游泳池面积: " + poolArea + "\n\r";
}
public static class Builder {
private String address;
private Integer numberOfRooms;
private Double livingArea;
private Double totalArea;
private Double price;
private String constructionYear;
private String type;
private Integer garageCapacity;
private Boolean isGarageHeated;
private Double greeneryArea;
private String gardenType;
private Boolean isPoolIndoor;
private Double poolArea;
public Builder address(String address) {
this.address = address;
return this;
}
public Builder numberOfRooms(Integer numberOfRooms) {
this.numberOfRooms = numberOfRooms;
return this;
}
public Builder livingArea(Double livingArea) {
this.livingArea = livingArea;
return this;
}
public Builder totalArea(Double totalArea) {
this.totalArea = totalArea;
return this;
}
public Builder price(Double price) {
this.price = price;
return this;
}
public Builder constructionYear(String constructionYear) {
this.constructionYear = constructionYear;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder garageCapacity(Integer garageCapacity) {
this.garageCapacity = garageCapacity;
return this;
}
public Builder isGarageHeated(Boolean isGarageHeated) {
this.isGarageHeated = isGarageHeated;
return this;
}
public Builder greeneryArea(Double greeneryArea) {
this.greeneryArea = greeneryArea;
return this;
}
public Builder gardenType(String gardenType) {
this.gardenType = gardenType;
return this;
}
public Builder isPoolIndoor(Boolean isPoolIndoor) {
this.isPoolIndoor = isPoolIndoor;
return this;
}
public Builder poolArea(Double poolArea) {
this.poolArea = poolArea;
return this;
}
public House build() {
return new House(this);
}
}
public static Builder builder() {
return new Builder();
}
}
public class HouseTest {
@Test
public void testGetHouseWithGarage() {
House houseWithGarage = new House.Builder()
.type("带有停车房的房屋")
.address("XXX省XXX市XXX区220号")
.numberOfRooms(4)
.livingArea(100D)
.totalArea(130D)
.price(1000000D)
.constructionYear("2025年2月10日")
.garageCapacity(2)
.isGarageHeated(false)
.build();
System.out.println(houseWithGarage.toString());
}
@Test
public void testGetHouseWithGreenery() {
House houseWithGreenery = new House.Builder()
.type("带有花园的房屋")
.address("XXX省XXX市XXX区220号")
.numberOfRooms(4)
.livingArea(100D)
.totalArea(130D)
.price(1000000D)
.constructionYear("2025年2月10日")
.greeneryArea(50D)
.gardenType("欧式花园")
.build();
System.out.println(houseWithGreenery.toString());
}
@Test
public void testGetHouseWithPool() {
House houseWithPool = House.builder()
.type("带有游泳池的房屋")
.address("XXX省XXX市XXX区220号")
.numberOfRooms(4)
.livingArea(100D)
.totalArea(130D)
.price(1000000D)
.constructionYear("2025年2月10日")
.poolArea(50D)
.isPoolIndoor(true)
.build();
System.out.println(houseWithPool.toString());
}
}
4.开源项目中的应用
Lombok 中的 @Builder 注解
Lombok 是一个流行的 Java 库,它通过注解简化代码。其中的 @Builder 注解就是建造者模式的典型应用。
使用 @Builder 注解
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private String name;
private int age;
private String email;
}
客户端代码
public class Client {
public static void main(String[] args) {
User user = User.builder()
.name("易元")
.age(30)
.email("eiyuan@example.com")
.build();
System.out.println(user); // 输出: User(name=易元, age=30, email=eiyuan@example.com)
}
}
长话短说
核心思想
- 分离构建过程与表示:将复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 逐步构建:通过一步一步地构建对象,最终得到一个完整的对象。
何时使用?
-
对象构造过程复杂:
- 当对象的构造过程涉及多个步骤,且这些步骤的顺序和逻辑复杂时。
-
对象属性多且可选:
- 当对象的属性很多,且某些属性是可选的时。
-
需要构建不同表示的对象:
- 当需要构建多个不同表示的对象,但构建过程相似时。
-
避免构造函数参数过多:
- 当构造函数参数过多,且某些参数是可选的时,使用建造者模式可以避免“构造函数爆炸”。
如何使用?
-
定义产品类:
- 定义最终要构建的复杂对象。
-
定义建造者接口:
- 定义构建产品的各个步骤的接口。
-
实现具体建造者:
- 实现建造者接口,完成具体产品的构建。
-
定义指挥者类(可选) :
- 负责调用建造者的方法,按照一定的顺序构建产品。
-
客户端使用建造者:
- 使用建造者和指挥者来构建产品。
2 3 4 步骤均可以省略,在产品类中定义内部类也可,指挥者类用于较多配置且配置间存在先后的顺序,在使用中可以直接通过定义静态内部类来使用