【Design Pattern】Creational - Builder

Posted by 西维蜀黍 on 2018-11-11, Last Modified on 2022-12-10

动机

无论在现实世界还是软件系统中,都存在一些复杂的对象,它们拥有多个组成部分。比如汽车,它包括变速箱、发动机、轮毅等各种部件。

而对于大多数用户而言,当使用汽车时,无须了解这些部件的生产细节,而将汽车作为一个整体来使用。有时,汽车的制造商允许用户指定某些部件的型号,以提供定制化服务。


在软件开发中,也存在大量类似汽车的复杂对象,它们拥有一系列成员属性。在这些复杂对象初始化中,可能存在一些限制条件,如:

  • 在某些属性被赋值之前,复杂对象不能作为一个完整的产品被使用
  • 部分属性的赋值必须按照某个指定的顺序,如在某个属性被赋值之前,另一个属性可能无法赋值
  • 部分属性是必须的(Mandatory),而部分属性是可选的(Optional)

但是对于调用者来说,在它们对复杂对象进行初始化时,调用者并不想了解复杂对象的这些内部细节(等同于用户不想知道汽车的某个组件的工作原理),只希望以最简单的方式完成该复合对象的初始化(等同于用户在定制化汽车时,只需要指定坐垫材质、发动机排量等简单的参数即可)。


复杂对象相当于一辆有待组组合的汽车,而对象的属性相当于汽车的部件,初始化对象的过程就相当于选择汽车部件的过程。当对象初始化完成后,就可以得到一个可以被调用的对象实例;相当于当汽车组件组合完成后,汽车就可以被使用了。

由于对复合对象进行初始化(组合部件)的过程较复杂。为了方便调用者,复合对象的初始化(部件的组合)过程往往被“外部化”到一个称作建造者的对象(Builder)里。

从而,用户只需要调用建造者对象中暴露的指定接口(并传入参数),即可完成部件组合过程。最终,建造者对象返回给调用者的是一个已经建造完毕的完整产品对象。这就是建造者模式的模式动机。

简单来说,对于初始化过程较复杂(需为其属性赋值,且还需要遵循一定的约定)的对象,可以采用建造者模式,以为调用者简化初始化过程。

定义

建造者模式(Builder Pattern)将一个复杂对象的构建与它的表示分离,使得同样的构建过程(但传入的参数不同)可以创建不同的表示。

构成

  • 抽象建造者(Builder):可以是一个接口或抽象类,声明了复杂对象产品中各个零件的装配方法;
  • 具体建造者(Concrete Builder):实现了Builder接口,具体来实现产品中各个组件的具体装配方法,同时提供一个方法(getResult())以返回创建好的复杂产品对象;
  • 导演者(Director):负责定义复杂对象产品中零件的装配顺序,以完成对复杂对象的组装;
  • 复杂对象产品(Product):被构建的复杂对象,包含多个组成部件,具体建造者负责创建该产品,并定义它的组装过程。

一般来说,复杂对象产品(Product)中包含的零件是数目与抽象建造者接口中声明的复杂对象产品中各个零件的装配方法的数目相符。

而且, 导演者角色是与客户端打交道的角色。导演者角色将客户端创建产品的请求划分为对各个零件的构造请求,再将这些请求委派给具体建造者角色。具体建造者角色是做具体建造工作的,但是却不为客户端所知。

UML图

导演者类(Director)

class Director{   
  private AbstractBuilder builder;   
      
  public Product construct(AbstractBuilder builder) {   
  	this.builder = builder;   
    this.builder.buildPartA();   
    this.builder.buildPartb();   
    this.builder.buildPartc();   
    return this.builder.getResult();   
  }   
}   

抽象建造者类(Builder)

abstract class AbstractBuilder{   
  public abstract void buildPartA();   
  public abstract void buildPartB();   
  public abstract void buildPartC();   
  public abstract Product getResult();   
}  

具体建造者类(Concrete Builder)

class ConcreteBuilder extends AbstractBuilder{   
  private Product product;
      
  public ConcreteBuilder(){
    this.product = new ConcreteProductA();   
  }
  public void buildPartA() {   
		... 
  }   
  public void buildPartB() {   
		... 
  }  
  public void buildPartC() {   
		... 
  }  
  public Product getResult() {   
    return this.product;   
  }   
}   

测试类

public class Client {   
  public static void main(String[] args) {   
    AbstractBuilder builder = new ConcreteBuilder();  
    
    Director director = new Director(builder);   
    Product p = director.construct();   
  }
}

示例

以去肯德基点餐为例,我们需要选择三明治、辅菜(比如薯条、炸鸡翅),选择饮料等等,以生成所谓的“套餐”:

  • MealBuilder接口声明了装配产品(准备一顿饭)所需要的各个方法
  • SandwichMealBuilder类实现了MealBuilder接口
  • MealDirector类中根据准备这顿饭的指定步骤,调用了指定方法(选择三明治、增加辅菜,选择饮料等等)
  • Meal类表示装配产品(一顿饭)

UML图

实现思路

Meal.java

public class Meal {
    public String sandwich;
    public String sideOrder;
    public String drink;
    public String offer;
    public double price;

    @Override
    public String toString() {
        return "Sandwich=" + sandwich + " Side Order=" + sideOrder + " Drink="
                + drink + " Offer=" + offer + " Price=" + price;
    }
}

MealBuilder.java

public interface MealBuilder {    
    public abstract void addSandwich(String sandwich);
    public abstract void addSides(String sides);
    public abstract void addDrink(String drink);
    public abstract void addOffer(String coupon);
    public abstract void setPrice(double price);
    public abstract Meal getMeal();
}

SandwichMealBuilder.java

public class SandwichMealBuilder implements MealBuilder {
    private Meal _meal = new Meal();

    @Override
    public void addSandwich(String sandwich) {
        _meal.sandwich = sandwich;
    }

    @Override
    public void addSides(String sides) {
        _meal.sideOrder = sides;
    }

    @Override
    public void addDrink(String drink) {
        _meal.drink = drink;
    }

    @Override
    public void addOffer(String coupon) {
        _meal.offer = coupon;
    }

    @Override
    public void setPrice(double price) {
        _meal.price = price;
    }

    @Override
    public Meal getMeal() {
        return _meal;
    }
}

MealDirector.java

public class MealDirector {
    MealBuilder mealBuilder;
    public MealDirector(MealBuilder mealBuilder){
      	this.mealBuilder = mealBuilder;
    }
  	
    public Meal makeMeal(String sandwich, String sides, String drink, String offer) {
        mealBuilder.addSandwich(sandwich);
        mealBuilder.addSides(sides);
        mealBuilder.addDrink(drink);
        mealBuilder.addOffer(offer);
        mealBuilder.setPrice(5.99);
      
      	return this.mealBuilder.getMeal();
    }
}

Client.java

public class Client {
    public static void main(String[] args) {        
        MealBuilder sandwichBuilder = new SandwichMealBuilder();
        
        MealDirector director = new MealDirector(sandwichBuilder);
        Meal meal = director.makeMeal("Hamburger", "Fries", "Coke", "Weekend Bonanza");
        System.out.println(meal.toString());
    }
}

适用环境

在以下情况下可以使用建造者模式:

  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

优缺点

优点

  • 调用者不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦
  • 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”

缺点

  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制
  • 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大

分析

建造者模式过渡到模板方法模式

准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。

不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式。

有意思的是,这个特殊的建造模式与模板方法有相似之处:construct()方法就相当于一个模板方法,这个方法调用其他的建造方法,如 buildPart1()、buildPart2()等基本方法。

因此,这使得此系统与模板方法模式相同。

建造者模式与工厂模式区别

我们可以看到,建造者模式与工厂模式是极为相似的,总体上,建造者模式仅仅只比工厂模式多了一个“导演类”的角色。在建造者模式的类图中,假如把这个导演类看做是最终调用的客户端,那么图中剩余的部分就可以看作是一个简单的工厂模式了。

与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——导演类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给导演类。由导演类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。

Reference