生成器模式 (Builder Pattern)

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了生成器模式 (Builder Pattern)相关的知识,希望对你有一定的参考价值。

生成器模式

生成器模式(Build Pattern) 是 GoF(Gang of Four design patterns) 设计模式中的一个,其本意是剥离复杂对象的构造方式,使其可以使用不同的实现方法构造出不同的对象,如:

这个案例中 builder 就属于抽象的对象(interface/abstract class),而 ConcreteBuilder 就是具体的实现方法,这张图上只有一个 ConcreteBuilder,不过实际案例中往往会出现多个 ConcreteBuilder,如:

最后的 Product 为实例出来的对象,而 Director 为调用 builder 的地方,有些地方也称之为 client:

builder pattern 的具体优势在于:

  • 可以微调一个实例的具体实现方式

    如第二张图的日程表安排,可以看到每一天的日程安排虽然相似(有一个具体的基类),但是具体的实现方式略有不同。

  • 封装了具体的实现方式

  • 提供流程化的实例过程

    这个下面的 JS 案例会具体说明

不过对应的也有一系列的劣势:

  • 每个被微调的实现方式都需要有一个对应的 ConcreteBuilder

  • 生成类必须可变(mutable)

    这也就违反了 SOLID 中的 OCP 原则

  • 可能会让依赖注入变得非常的麻烦

实现案例 1

假设这里实现一个 html 生成器,比较基础的实现如下:

const hello = 'hello';
let html = [];
html.push('<p>');
html.push(hello);
html.push('</p>');

console.log(html.join(''));

const words = ['hello', 'world'];
html = [];
html.push('<ul>\\n');
for (const word of words) 
  html.push(`  <li>$word</li>\\n`);

html.push('</ul>');

console.log(html.join(''));

这种比较基础的实现有一个挑战就是,当代码嵌套比较复杂的时候,如 html > body > table > tehad > ... 这样的结构,那么手写就会非常的累。这个时候就可以借助 builder pattern 简化问题:

class Tag 
  static get indentSize() 
    return 2;
  

  static create(name) 
    return new HtmlBuilder(name);
  

  constructor(name = '', text = '') 
    this.name = name;
    this.text = text;
    this.children = [];
  

  toStringImpl(indent) 
    let html = [];
    let i = ' '.repeat(indent * Tag.indentSize);
    html.push(`$i<$this.name>\\n`);
    if (this.text.length > 0) 
      html.push(' '.repeat(Tag.indentSize * (indent + 1)));
      html.push(this.text);
      html.push('\\n');
    

    for (const child of this.children) 
      html.push(child.toString());
    

    html.push(`$i</$this.name>\\n`);

    return html.join('');
  

  toString() 
    return this.toStringImpl(0);
  


class HtmlBuilder 
  constructor(rootName) 
    this.root = new Tag(rootName);
    this.rootName = rootName;
  

  addChild(childName, childText) 
    const child = new Tag(childName, childText);
    this.root.children.push(child);
  

  addChildFluent(childName, childText) 
    const child = new Tag(childName, childText);
    this.root.children.push(child);
    return this;
  

  clear() 
    this.root = new Tag(this.rootName);
  

  toString() 
    return this.root.toString();
  

  build() 
    return this.root;
  

与之对应的调用如下:

const builder = Tag.create('ul');

for (const word of words) 
  builder.addChild('li', word);


console.log(builder.build().toString());

builder.clear();
builder
  .addChildFluent('li', 'foo')
  .addChildFluent('li', 'bar')
  .addChildFluent('li', 'baz');

console.log(builder.toString());

因为 javascript 没有 interface,也没有 abstract class,所以这里没有使用二者去形成一个基类,比起上面的 UML 图来说就少了一个类,不过整体的流程是一致的:client 调用 create 去创建一个新的 builder,builder 负责实现具体的业务逻辑,既构建具体的 HTML tag。

⚠️ 最后的调用有点类似于 JQuery 的调用,这个是基于 Fluent interface, 流式接口 这一 API 设计模式 进行的实现。大部分的 builder pattern 都会基于 Fluent interface 进行实现。

简单的瞄了一下,JQuery 的 API 内部的确是遵从了 builder pattern 的规范,至少基于这本书是这么说的:

实现案例 2

这个案例会提供一个流程化的实现,即 builder A 调用 builder B 这样一个流程,代码如下:

class Person 
  constructor() 
    // address
    this.streetAddress = this.postcode = this.city = '';
    // employment info
    this.companyName = this.position = '';
    this.annualIncome = 0;
  

  toString() 
    return `Person lives at $this.streetAddress, $this.city, $this.postcode
    and works at $this.companyName as a $this.position earning $this.annualIncome`;
  


class PersonBuilder 
  constructor(person = new Person()) 
    this.person = person;
  

  get lives() 
    return new PersonAddressBuilder(this.person);
  

  get works() 
    return new PersonJobBuilder(this.person);
  

  build() 
    return this.person;
  


// all the sub-builders get access to the same obj
class PersonJobBuilder extends PersonBuilder 
  constructor(person) 
    super(person);
  

  at(companyName) 
    this.person.companyName = companyName;
    return this;
  

  asA(position) 
    this.person.position = position;
    return this;
  

  earning(annualIncome) 
    this.person.annualIncome = annualIncome;
    return this;
  


class PersonAddressBuilder extends PersonBuilder 
  constructor(person) 
    super(person);
  

  at(streetAddress) 
    this.person.streetAddress = streetAddress;
    return this;
  

  withPostcode(postcode) 
    this.person.postcode = postcode;
    return this;
  

  in(city) 
    this.person.city = city;
    return this;
  


const pb = new PersonBuilder();
let person = pb.lives
  .at('123 London Road')
  .in('London')
  .withPostcode('SW12BC')
  .works.at('Fabrikam')
  .asA('Engineer')
  .earning(123000)
  .build();

console.log(person.toString());

这里的一些实现基于了比较新的 JavaScript 语法,比如说 getter:

get lives() 
    return new PersonAddressBuilder(this.person);
  

这个 getter 就直接返回了一个新的 PersonAddressBuilder,子类的构造函数中也没有新建新的对象,因此三个 builders 修改的都是同一个实例化对象。

这就是一个相对而言比较复杂的 builder pattern,最上面三张图中的两个 Day Planner/Trip Planner 就可以基于这样的结构进行实现,既 Builder A 中调用 Builder B 或是 Builder C。

这样的嵌式调用的优势就在于动态化微调每一个实例,劣势也是同样如此,一旦每个操作都太过精细,那么就会让对象的结构太过于复杂,从而难以管理。

补充

再翻完了另一本书的 builder 篇章,发现 director 和 client 两个的概念还是不同的。

director 是对于 builder 一系列步骤的封装,以上面的 PersonBuilder 为例 假设这是一个员工的 builder,可以单独再实现一个 director 隐藏一些关键的实现信息,这样只要在 client 部分调用 factory.build(),就可以省略一些流式调用的过程。

build() 这个函数是很重要的,理论上来说,在调用 build() 之前,builder 正在构造的对象都属于不可实例的状态。

如果对于 builder pattern 还比较困惑,那么可以考虑一下车这个案例。对于大多数车来说都可以分类成 sedan、SUV、pickup truck,但是就算是分了子类,依旧会有很多不同的特性需要满足。

比如说倒车雷达、BSM(blindspot monitor)、加热座椅、加热方向盘、GPS 等功能,如果修改一个功能就需要新写一个 constructor,那么对于非 JavaScript 的其他编程语言来说,所需要的构造函数数量会大得惊人。这也是为什么 builder pattern 在这种情况下就特别的有用——它可以流式调用而并不需要写 N 个构造函数。

reference

以上是关于生成器模式 (Builder Pattern)的主要内容,如果未能解决你的问题,请参考以下文章

生成器模式(Builder Pattern)

设计模式Builder Pattern建造者模式

Java建造者模式(Builder pattern)

Builder生成器(创建型模式)

java设计模式创建模式Creational Pattern建造模式Builder Pattern

建造者模式(Builder Pattern)