【TS】今さら聞けないビルダーパターン

5 min read読了の目安(約4900字

はじめに

今回はビルダーパターン(Builder Pattern)について解説します。
インスタンス生成方法を切り出すことで取り回しが効くようにするデザインパターンです。

ビルダーパターンとは?

TECHSCOREさんの解説があります。

以下、引用です。

例えば、家を建てることを考えてみます。完成する家がどのような家になるかというのは「家の構築過程」と「素材」大きく2つの要素で決定されると考えてみてください。「作成過程」とは、「どのような順番で、どこに何を配置していくか」というようなことであり、「素材」とは、「柱には何を使って、壁には何を使って・・・」ということであると考えてください。

このとき、「作成過程」には、"平屋を建てるための作成過程" や "2階建ての家を建てるための作成過程"、または "少し変わった平屋を建てるための作成過程" など様々なものが考えられます。同様に、「素材」にも、"柱は木で、壁は土壁、屋根は瓦" などのような和風の家を建てるための素材を用いることもあれば、"柱は鉄で壁と屋根はコンクリート" といった場合も考えられます。

これらをそれぞれ用意しておくことで、『 "ちょっと変わった平屋を建てる作成過程" で "柱は鉄で壁と屋根はコンクリート" の家を建ててください』という要望に柔軟に応えることができるようになります。

Builder パターンとは、このような、「作成過程」を決定する Director と呼ばれるものと「表現形式」を決定する Builder と呼ばれるものを組み合わせることで、オブジェクトの生成をより柔軟にし、そのオブジェクトの「作成過程」をもコントロールすることができるようにするためのパターンです。

例題

以下のように四角形を表すクラスがあるとします。
起点となる座標と縦横の幅は必須項目とし、塗り潰しやラベル等のオプション項目を持っています。

class Rect {
    x: number;
    y: number;
    width: number;
    height: number;
    fill: boolean = false;
    fillColor: string = 'transparent';
    lineColor: string = '#000';
    lineWidth: number = 1;
    label: string;
    labelColor: string = '#000';

    constructor(x: number, y: number, width: number, height: number) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    setFillColor = (color: string) => {
        this.fill = true;
        this.fillColor = color;
    }

    setLabel = (label: string, labelColor?: string) => {
        this.label = label;
        if(labelColor) this.labelColor = labelColor;
    }

    disp = () => {
        console.log(`position:`, `{${this.x},${this.y}}`);
        console.log(`fill:`,`${this.fill}`);
        console.log(`width:`,`${this.width}`);
        console.log(`height:`,`${this.height}`);
        console.log(`fill:`,`${this.fill}`);
        console.log(`fillColor:`,`${this.fillColor}`);
        console.log(`lineColor:`,`${this.lineColor}`);
        console.log(`lineWidth:`,`${this.lineWidth}`);
        console.log(`label:`,`${this.label}`);
        console.log(`labelColor:`,`${this.labelColor}`);
    }
}

この場合、「赤く塗り潰した、座標(40,20)width100,height400の四角形」インスタンスを生成する場合は以下のように記載します。

const rect: Rect = new Rect(40,20,100,400);
rect.fill = true;
rect.fillColor = 'red';
rect.disp();

出力

position: {40,20}
fill: true
width: 100
height: 400
fill: true
fillColor: red
lineColor: #000
lineWidth: 1
label: undefined
labelColor: #000

これと同じ四角形を「何個も」作る必要があったり、「色だけ差し替えたもの」や「さらにラベルをつけたもの」を作ることになった場合に「ビルダーパターン」が効果を発揮します。

ビルダーパターンで実装

まず、Rectインスタンスを生成するビルダーのインターフェースとなるIBuilderを作ります。

interface IBuilder {
    init():void;
    setInfo(x: number, y: number, width: number, height: number):void;
    setFillColor(color: string):void;
    setLabel(label: string):void;
    getResult():Rect;
}

そして、IBuilderを実装したRedRectBuilderを作ります。
その名の通り「赤い四角形」のRectインスタンスを生成します。

class RedRectBuilder implements IBuilder {
    private rect: Rect;

    constructor() {
        this.init();
    }

    init = () => {
        this.rect = new Rect(0,0,0,0);
	this.fill = true;
	this.fillColor = 'red';
    }

    setInfo = (x: number, y: number, width: number, height: number) => {
        this.rect.x = x;
        this.rect.y = y;
        this.rect.width = width;
        this.rect.height = height;
    }

    setFillColor = (color: string) => {
        this.rect.fill = true;
        this.rect.fillColor = color;
    }

    setLabel = (label: string) => {
        this.rect.label = label;
    }

    getResult = (): Rect => {
        const result = this.rect;
        this.init();
        return result;
    }
}

さらにこのRedRectBuilderを使用するDirectorクラスを作成します。
このDirectorは座標(0,0)で幅100の正方形を生成するものとします。

class Director {
    private _builder: IBuilder;
    constructor(builder: IBuilder) {
        this._builder = builder;
    }

    construct = () => {
        this._builder.setInfo(0,0,100,100);
    }
}

これらを使って「赤い正方形」を生成するコードは以下になります。

const redRectBuilder: RedRectBuilder = new RedRectBuilder();
const director: Director = new Director(redRectBuilder);
director.construct();
const result: Rect = redRectBuilder.getResult();
result.disp();

出力

position: {0,0}
fill: true
width: 100
height: 100
fill: true
fillColor: red
lineColor: #000
lineWidth: 1
label: undefined
labelColor: #000

この実装方法のメリットは、生成したいインスタンスの種類に合わせてBulderDirectorをカスタマイズすれば対応できる点です。
例えば青いラベル付きのRectインスタンスを生成したい場合は、RedRectBuilderを予め作成しておいたBlueLabelBuilderに変えるだけで済みます。
Builderconstructを修正して座標やラベルや塗りつぶし設定を変えることもできます。

このようにDirectorBuilderを組み合わせることで、Rectインスタンスを柔軟に生成できるようになります。

まとめ

今回はTypescriptでデザインパターンの一つである Builder Pattern について紹介しました。
このパターンを導入する目安となるのが、 「生成するインスタンスのプロパティの多さ」「類似したインスタンスを何個も生成する可能性の高さ」 だと思います。
何回も似たような書き方をしてちょっと違うインスタンスを生成しているな、と感じた時には導入するといいかもしれません。