设计模式-建造者模式

174 阅读9分钟

写在前面

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.核心角色

  1. 产品(Product)

    • 最终要构建的复杂对象。
    • 例如:一辆汽车、一份套餐、一个文档。
  2. 建造者(Builder)

    • 定义构建产品的各个步骤的接口。
    • 例如:定义如何安装引擎、如何安装车轮。
  3. 具体建造者(Concrete Builder)

    • 实现建造者接口,完成具体产品的构建。
    • 例如:具体实现如何安装引擎、如何安装车轮。
  4. 指挥者(Director)

    • 负责调用建造者的方法,按照一定的顺序构建产品。
    • 例如:指挥先安装引擎,再安装车轮。

3.使用场景

  1. 对象构造过程复杂

    • 当一个对象的构造过程涉及多个步骤,且这些步骤的顺序和逻辑复杂。
    • 例如:构建一辆汽车需要安装引擎、车轮、座椅等。
  2. 对象属性多且可选

    • 当对象的属性很多,且某些属性是可选的时。
    • 例如:构建一份套餐,可以选择是否加饮料、是否加甜点。
  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)
    }
}

长话短说

核心思想

  • 分离构建过程与表示:将复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
  • 逐步构建:通过一步一步地构建对象,最终得到一个完整的对象。

何时使用?

  1. 对象构造过程复杂

    • 当对象的构造过程涉及多个步骤,且这些步骤的顺序和逻辑复杂时。
  2. 对象属性多且可选

    • 当对象的属性很多,且某些属性是可选的时。
  3. 需要构建不同表示的对象

    • 当需要构建多个不同表示的对象,但构建过程相似时。
  4. 避免构造函数参数过多

    • 当构造函数参数过多,且某些参数是可选的时,使用建造者模式可以避免“构造函数爆炸”。

如何使用?

  1. 定义产品类

    • 定义最终要构建的复杂对象。
  2. 定义建造者接口

    • 定义构建产品的各个步骤的接口。
  3. 实现具体建造者

    • 实现建造者接口,完成具体产品的构建。
  4. 定义指挥者类(可选)

    • 负责调用建造者的方法,按照一定的顺序构建产品。
  5. 客户端使用建造者

    • 使用建造者和指挥者来构建产品。

2 3 4 步骤均可以省略,在产品类中定义内部类也可,指挥者类用于较多配置且配置间存在先后的顺序,在使用中可以直接通过定义静态内部类来使用