💡gntk.dev

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

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

post-cover

概要

「機能のクラス階層」と「実装のクラス階層」を橋渡しするためのパターン。

機能のクラス階層

あるクラスSomethingに新しい機能を追加するためにSomethingのサブクラスSomethingGoodクラスを作るときにできる階層構造。

  • スーパークラスは基本的な機能を持っている
  • サブクラスはスーパークラスに新しい機能を追加する

実装のクラス階層

ある抽象クラスAbstractClassの抽象メソッドを実装したサブクラスConcreteClassを作るときにできる階層構造。

  • スーパークラスは抽象メソッドによってインターフェースを規定している
  • サブクラスは具象メソッドによってそのインターフェースを実装する

クラス階層の分離

「機能のクラス階層」と「実装のクラス階層」とが混在している場合、クラス階層が複雑になり見通しが悪くなる。それぞれのクラス階層を独立させ、その橋渡しをするためのデザインパターンがBridgeパターン。

サンプルコード

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

渡した文字に枠を付けて表示するプログラムを、機能のクラス階層と実装のクラス階層に分離して実装している。

import { Display } from 'bridge/function/display';
import { StringDisplayImpl } from 'bridge/implementation/stringDisplayImpl';
import { CountDisplay } from 'bridge/function/countDisplay';

describe('bridge', () => {
  test('display', () => {
    const display: Display = new Display(
      new StringDisplayImpl('Hello, Japan.'),
    );
    expect(display.display()).toEqual(`+-------------+
|Hello, Japan.|
+-------------+`);
  });

  test('multi display', () => {
    const display: CountDisplay = new CountDisplay(
      new StringDisplayImpl('Hello, world'),
    );
    expect(display.multiDisplay(5)).toEqual(`+------------+
|Hello, world|
|Hello, world|
|Hello, world|
|Hello, world|
|Hello, world|
+------------+`);
  });
});

StringDisplayImplは渡された文字列を加工して別の文字列を作るという実装のクラスDisplayCountDisplayStringDisplayImplのインターフェースを使って意味のある文字列のかたまりを表示するという機能のクラス

実装のクラス

まずは、文字列を加工して別の文字列を作る実装のインターフェースを用意する。

displayImpl.ts

export abstract class DisplayImpl {
  public abstract rawOpen(): string;
  public abstract rawPrint(): string;
  public abstract rawClose(): string;
}

このインターフェースを使って、実装のクラスを作ったり、機能のクラスから利用したりする。実装のクラスは以下の通り。

stringDisplayImpl.ts

import { DisplayImpl } from 'bridge/implementation/displayImpl';

export class StringDisplayImpl extends DisplayImpl {
  private string: string;
  private width: number;
  public constructor(string: string) {
    super();
    this.string = string;
    this.width = string.length;
  }
  public rawOpen(): string {
    return `${this.printLine()}\n`;
  }
  public rawPrint(): string {
    return `|${this.string}|\n`;
  }
  public rawClose(): string {
    return this.printLine();
  }
  private printLine(): string {
    const string: string[] = [];
    string.push('+');
    for (let i = 0; i < this.width; i++) {
      string.push('-');
    }
    string.push('+');

    return string.join('');
  }
}

渡された文字列の長さに応じて枠の上辺と下辺を作るrawOpenrawClose、渡された文字列の左右にパイプをつけた文字列を作るrawPrintを実装している。

機能のクラス

実装のクラス階層のインターフェースを使って、機能を提供するクラス。

display.ts

import { DisplayImpl } from 'bridge/implementation/displayImpl';

export class Display {
  private impl: DisplayImpl;
  public constructor(impl: DisplayImpl) {
    this.impl = impl;
  }
  public open(): string {
    return this.impl.rawOpen();
  }
  public print(): string {
    return this.impl.rawPrint();
  }
  public close(): string {
    return this.impl.rawClose();
  }
  public display(): string {
    const string: string[] = [];
    string.push(this.open());
    string.push(this.print());
    string.push(this.close());
    return string.join('');
  }
}

rawOpenなどの実装のクラスをそのまま使うだけのopenなどのメソッドと、それらを順番に使って枠付きの文字を作るdisplayメソッドを用意している。

複数回表示する機能を用意する場合は以下のような機能のクラスを作る。

countDisplay

import { Display } from 'bridge/function/display';
import { DisplayImpl } from 'bridge/implementation/displayImpl';

export class CountDisplay extends Display {
  public constructor(impl: DisplayImpl) {
    super(impl);
  }
  public multiDisplay(times: number): string {
    const string: string[] = [];
    string.push(this.open());
    for (let i = 0; i < times; i++) {
      string.push(this.print());
    }
    string.push(this.close());
    return string.join('');
  }
}

機能や実装を追加する

ランダム回数表示する機能

  test('random count display', () => {
    // 毎回ランダムに表示されたらテストにならないので、テストのときは3回に固定
    jest.spyOn(randomModule, 'generateRandomNumber').mockReturnValue(3);

    const display: RandomCountDisplay = new RandomCountDisplay(
      new StringDisplayImpl('Hello, world'),
    );
    expect(display.randomDisplay(5)).toEqual(`+------------+
|Hello, world|
|Hello, world|
|Hello, world|
+------------+`);
  });

例えば以上のようなランダム回数表示する機能を追加するときは、機能のクラス階層にクラスを追加する。

randomCountDisplay.ts

import { CountDisplay } from 'bridge/function/countDisplay';
import { DisplayImpl } from 'bridge/implementation/displayImpl';
import { generateRandomNumber } from 'bridge/util/generateRandomNumber';

export class RandomCountDisplay extends CountDisplay {
  public constructor(impl: DisplayImpl) {
    super(impl);
  }
  public randomDisplay(times: number): string {
    const random = generateRandomNumber(times);
    return this.multiDisplay(random);
  }
}

バーを表示する機能

  test('print bar', () => {
    const display1: IncreaseDisplay = new IncreaseDisplay(
      new CharDisplayImpl('<', '*', '>'),
      1,
    );
    expect(display1.increaseDisplay(5)).toEqual(`<>
<*>
<**>
<***>
<****>
`);

    const display2: IncreaseDisplay = new IncreaseDisplay(
      new CharDisplayImpl('<', '-', '>'),
      3,
    );
    expect(display2.increaseDisplay(5)).toEqual(`<>
<--->
<------>
<--------->
<------------>
`);
  });

以上のように、長さと段数を指定してバーを表示させる機能を作る場合、DisplayImplを使った新しい実装のクラスとCountDisplayを使った新しい機能のクラスを用意すれば実現できる。

CharDisplayImpl.ts

import { DisplayImpl } from 'bridge/implementation/displayImpl';

export class CharDisplayImpl extends DisplayImpl {
  private head: string;
  private body: string;
  private foot: string;
  public constructor(head: string, body: string, foot: string) {
    super();
    this.head = head;
    this.body = body;
    this.foot = foot;
  }
  public rawOpen(): string {
    return this.head;
  }
  public rawPrint(): string {
    return this.body;
  }
  public rawClose(): string {
    return `${this.foot}\n`;
  }
}

StringDisplayImplが行単位でrawOpen/rawPrint/rawCloseを実装していたのに対して、こちらは文字単位でそれらを実装している。

IncreaseDisplay.ts

import { DisplayImpl } from 'bridge/implementation/displayImpl';
import { CountDisplay } from './countDisplay';

export class IncreaseDisplay extends CountDisplay {
  private step: number;
  public constructor(impl: DisplayImpl, step: number) {
    super(impl);
    this.step = step;
  }
  public increaseDisplay(level: number): string {
    const string = [];
    let count = 0;
    for (let i = 0; i < level; i++) {
      string.push(this.multiDisplay(count));
      count += this.step;
    }

    return string.join('');
  }
}

CountDisplayに、長さを指定して表示するメソッドincreaseDisplayを追加している。

機能・実装を追加していった結果

機能・実装を追加していった結果、以下のようなクラス図ができあがった。

bridgeクラス図

右側にある機能のクラス階層、左側に実装のクラス階層、そしてそれらをつなぐブリッジができていることがわかる。

感想・考察

このパターンはすごく役に立ちそうな気がしたので結構丁寧にやった。最後クラス図書いて見事にブリッジになってるのがよかった。

© 2021 gntk.dev