💡gntk.dev

【Builder】GoFのデザインパターンを学ぶ

GoFのデザインパターンをTypeScriptで写経しました。今回はBuilderパターンです。

post-cover

概要

全体を構成するための各部品を定義・作成し、それらを段階を踏んで組み上げるためのパターン。まず部品の抽象クラスを用意し、それらを実装するクラスをいくつか作る。組み上げの段階では抽象クラスのみを知っているクラスによって部品の組み上げが行われる。

サンプルコード

結城浩先生の増補改訂版Java言語で学ぶデザインパターン入門のJavaプログラムをTypeScriptで写経した。

Builderパターンを使ってテキスト文書とhtml文書を組み上げるプログラム。登場するクラスは以下の通り。

  • 文書に含まれる部品を定義しているBuilderクラス
  • テキスト・htmlそれぞれにおけるBuilderクラスの実装としてTextBuilderとHTMLBuilderクラス
  • Builderクラスのインスタンスを受け取って文書の構成を組み上げるDirectorクラス

以下のテストのように上記のクラスを利用する。

import { Director } from 'builder/director';
import { TextBuilder } from 'builder/textBuilder';
import { HTMLBuilder } from 'builder/htmlBuilder';

describe('Builder', () => {
  test('textBuilder', () => {
    const textBuilder = new TextBuilder();
    const director = new Director(textBuilder);
    director.construct();
    const result = textBuilder.getResult();

    expect(result).toEqual(
      '====================\n「Greeting」\n■朝から昼にかけて\n・おはようございます。\n・こんにちは。\n■夜に\n・こんばんは。\n・おやすみなさい。\n・さようなら。\n====================\n',
    );
  });

  test('htmlBuilder', () => {
    const htmlBuilder = new HTMLBuilder();
    const director = new Director(htmlBuilder);
    director.construct();
    const result = htmlBuilder.getResult();

    expect(result).toEqual(
      '<html><head><title>Greeting</title></head><body><h1>Greeting</h1><p>朝から昼にかけて</p><ul><li>おはようございます。<li/><li>こんにちは。<li/></ul><p>夜に</p><ul><li>こんばんは。<li/><li>おやすみなさい。<li/><li>さようなら。<li/></ul></body></html>',
    );
  });
});

Builderクラス

文書に含まれる部品を定義しているBuilderクラスは以下の通り。

export abstract class Builder {
  public abstract makeTitle(title: string): void;
  public abstract makeString(str: string): void;
  public abstract makeItems(items: string[]): void;
  public abstract close(): void;
}

文書はタイトル・本文・リスト・締め、の組み合わせで構成されることを定義している。

Directorクラス

受け取ったBuilderクラスのインスタンスを使い、文書の構成を組み上げる。タイトル→文字列→リスト→文字列→リスト→締めで文書を構成することを定義している。

import { Builder } from 'builder/builder';

export class Director {
  private builder: Builder;
  public constructor(builder: Builder) {
    this.builder = builder;
  }
  public construct(): void {
    this.builder.makeTitle('Greeting');
    this.builder.makeString('朝から昼にかけて');
    this.builder.makeItems(['おはようございます。', 'こんにちは。']);
    this.builder.makeString('夜に');
    this.builder.makeItems([
      'こんばんは。',
      'おやすみなさい。',
      'さようなら。',
    ]);
    this.builder.close();
  }
}

TextBuilder・HTMLBuilderクラス

Builderクラスを実装しているTextBuilder・HTMLBuilderクラスは以下の通り。

TextBuilderクラス

import { Builder } from 'builder/builder';

export class TextBuilder implements Builder {
  private sentence: string[] = [];
  public makeTitle(title: string): void {
    this.sentence.push('====================\n');
    this.sentence.push(`${title}」\n`);
  }
  public makeString(str: string): void {
    this.sentence.push(`${str}\n`);
  }
  public makeItems(items: string[]): void {
    items.forEach((item) => this.sentence.push(`${item}\n`));
  }
  public close(): void {
    this.sentence.push('====================\n');
  }
  public getResult(): string {
    return this.sentence.join('');
  }
}

HTMLBuilderクラス

import { Builder } from 'builder/builder';

export class HTMLBuilder implements Builder {
  private html: string[] = [];
  public makeTitle(title: string): void {
    this.html.push(`<html><head><title>${title}</title></head><body>`);
    this.html.push(`<h1>${title}</h1>`);
  }
  public makeString(str: string): void {
    this.html.push(`<p>${str}</p>`);
  }
  public makeItems(items: string[]): void {
    this.html.push('<ul>');
    items.forEach((item) => {
      this.html.push(`<li>${item}<li/>`);
    });
    this.html.push('</ul>');
  }
  public close(): void {
    this.html.push('</body></html>');
  }
  public getResult(): string {
    return this.html.join('');
  }
}

感想・考察

  • BuilderをDirectorが受け取りDirectorの中でmakeXxxするという操作は、Builderインスタンスのプロパティに対して副作用を起こしているので、あまり好きではなかった
  • 上記の理由があるので、makeXxxは、インスタンスのプロパティにpushするんではなくて単純に文字列を返す作りにしたほうがいいんではないかと思った
  • サンプルコードではTextBuilderやHTMLBuilderがBuilderをextendsしたものになっていた

    • Builderにはabstractなメソッドしか定義されてないので、extendsではなくimplementsのほうが適切であると思ったので、そのようにした
© 2021 gntk.dev