GoFのデザインパターンについての個人的な見解

  • 2024.08.27
92
NO IMAGE

最近弊社の若手が自主的に集まってGoFのデザインパターンについて学んでおり、それは勿論素晴らしい事である反面、GoFのデザインパターンが作成されたのは1995年と今から30年近く昔であるため、当時と今ではかなり様相が異なる部分がある。そのため、私がGoFのデザインパターンについて思う事を個々のパターンについての見解という形で記載しようと思う。場合によってはコンセプトのサンプルコードも記載するが、そちらはTypeScriptで記述する事とする。

そして改めて見て思うのが、かなりのパターンが委譲による多態性の実現や、Inversion of Controlの考えの元で説明できる事だ。なのでまずそれらについて学んで、その実例としてパターンを学ぶという方が良いようにも思える。また、いくつかのパターンは今となってはアプリケーションフレームワークやDIコンテナで実現されているので、実装する機会は少ないと思われる。その場合でも、自分の使っているものの動作の仕組みや概念を知ることは有用と思われる。

生成に関するパターン

DIコンテナやフレームワークの発展により、これらのパターンを一から実装する事は特に少なくなっている気もする。言い方を変えれば、自分がそれらを開発する時にはフレームワークのユーザー向けに提供した方が良さそうなパターンともいえる。概念自体を知っておいて良いものは多い。

Abstract Factory

最終的に欲しいインスタンスを生成するためのファクトリークラスを切り替える事で異なるインスタンスを生成するパターンであり、GUIフレームワーク/ライブラリなどでは特に使われやすい傾向にあると思われる。例えばreactのダイアグラム図作成ライブラリであるreact-diagramsでは、下記のようにModelとModelを表示するWidgetのFactoryを登録するような機能を有している。

class ModelXFactory<X> extends AbstractReactFactory<X> {
  constructor() {
    super("ModelX")
  }

  generateModel(initialConfig: any): X {
    return new ModelX(initialConfig)
  }

  generateReactWidget(event: any): JSX.Element {
    return (
      <WidgetX
        engine={this.engine}
        node={event.model}
      />
    )
  }
}

class ModelYFactory<Y> extends AbstractReactFactory<Y> {
  constructor() {
    super("ModelY")
  }

  generateModel(initialConfig: any): Y {
    return new ModelY(initialConfig)
  }

  generateReactWidget(event: any): JSX.Element {
    return (
      <WidgetY
        engine={this.engine}
        node={event.model}
      />
    )
  }
}

function registerFactories(engine: DiagramEngine) {
  const factory = engine.getNodeFactories()
  factory.registerFactory(new NodeXFactory())
  factory.registerFactory(new NodeYFactory())
}

フレームワーク、あるいはそれに近いものを提供する時には有用なパターンと言える。とはいえ、Factoryクラスを使わずとも関数オブジェクトを使えば十分な事もボチボチ多いようにも思える。(これは他のGoFのデザインパターンにも言える事だが、OOPの文脈を超えてより本質的かつ抽象的な視点で見られると尚良いパターンが結構ある。)

また、Factory Methodに限らず生成に関するパターンは、これらの仕組みの提供者側と利用者側で制御の反転(Inversion of Control)が発生していることが少なからずある。上記のreact-diagramsの例はまさしくそれで、ファクトリーでどのようなモデルとウィジェットを作るのかという事はその実装者側が決めているが、どのような契機でそれらのオブジェクトが作られるのかというタイミングはreact-diagramsで制御されている。

Builderパターン

オブジェクトの生成手順自体をBuilderというオブジェクトで表現する。生成過程が複雑な不変オブジェクトに対して使う事が多いか。

class Product {
    // 兎に角プロパティが多くて相関チェックが必要なオブジェクトだと思ってください
    constructor(private readonly prop1: string, private readonly prop2: boolean...) {} 
}

class Builder {
    prop1?: string
    prop2?: boolean
    ...
    ...
    setPorp1(prop1: string) {
        this.prop1 = prop1
    }
    ...
    build(): Product {
        // プロパティの存在チェックやら相関チェックやら
        return new Product(this.prop1, this.prop2, ...)
    }
}

他の生成に関するパターンと組み合わせる事もある。

Factory Method

実は前のAbstract Factoryの例は半ばFactory Methodの例にもなっている。GoFのデザインパターンにおいては、後述のTemplate Method的に振る舞いをある程度規定し、その上で生成されるインスタンスの種類を変える事で最終的な振る舞いを変えるというパターンになっている。一方で単純にコンストラクタ以外のメソッドを利用してインスタンスを生成させるだけでもFactory Methodと呼ばれる事があり、どうにも概念が混乱している気がする。

GoFのFactory Method的な例は、Abstract Factoryのコードを以下のように変えると理解しやすいか。

class AbstractModelXFactory<X> extends AbstractReactFactory<X> {
  constructor(type: string) {
    super(type)
  }
  generateModel(initialConfig: any): X {
    return generateConcreteModel(initialConfig)
  }

  abstract generateConcreteModel(initialConfig: any): X

  generateReactWidget(event: any): JSX.Element {
    return (
      <WidgetX
        engine={this.engine}
        node={event.model}
      />
    )
  }
}

class ModelDerived1Factory exntends AbstractModelXFactory {
    constructor() {
        super("Derived1")
    }

    generateConcreteModel(initialConfig: any) {
        return new Derivied1(initialConfig)
    }
}

class ModelDerived2Factory exntends AbstractModelXFactory {
    constructor() {
        super("Derived2")
    }

    generateConcreteModel(initialConfig: any) {
        return new Derivied2(initialConfig)
    }
}

class ModelYFactory<Y> extends AbstractReactFactory<Y> {
  constructor() {
    super("ModelY")
  }

  generateModel(initialConfig: any): Y {
    return new ModelY(initialConfig)
  }

  generateReactWidget(event: any): JSX.Element {
    return (
      <WidgetY
        engine={this.engine}
        node={event.model}
      />
    )
  }
}

function registerFactories(engine: DiagramEngine) {
  const factory = engine.getNodeFactories()
  factory.registerFactory(new ModelDerived1Factory())
  factory.registerFactory(new ModelDerived2Factory())
  factory.registerFactory(new NodeYFactory())
}

これは表示するコンポーネントは共通だが、内部的な振る舞いが異なるインスタンスを生成したい、といった意図のコードである。この時、generateConcreteModelがFactory Methodとしてサブクラスで実装されるメソッドになるが、このメソッドはユーザーのコードで直に使われるものではなく、Template Method的にライブラリ内部で呼び出されるのみである。

とはいえ、これは必ずしも継承で行うようなものかと言われると微妙で、委譲によって行ったり、あるいは関数オブジェクトを渡して行ったりといった事で十分な事が多い気もする。また、こういった応用的なパターン以外は、DIコンテナの発展などでインスタンスの生成の制御がフレームワークに移りつつあることもあり、位置から実装して使う機会は減っている感がある。

Prototype

インスタンスからインスタンスを生成する、要するにオブジェクトのコピーに関するパターンだが、これはどのような形で表現すべきなのかが割とケースバイケースなので、Prototypeパターンとしてまとめるのには割と疑問という気がする。

オブジェクトのコピーが必要になるのはまずオブジェクトが不変でない場合であり(不変であればAbstract Factoryなど他の生成に関するパターンが先に挙がるし、今であればDIコンテナで済む事も多い)、そのような場合には単純にクローンを作るというより、コピー元から何を引き継ぐのか、そして引き継がなかった値に何を設定するのかを管理するManager的なものが必要になる事が多く、そのような観点で考察した方が良さそうに思える。

例えばreact-diagramsの場合、単純にModelをコピーするとIDが被っておかしなことになってしまうので、最低でもIDの付け替えを行う必要があり、また、画面の同じ位置にコピーを作成すると複数のオブジェクトがダブって表示されてしまうので位置の調整も必要になる。このようにオブジェクトのコピーについては単純に行かないケースがあり、その場合に問題なくコピーを作成するためのコンテキストを持ったオブジェクトが別途必要になる。そういう意味ではパターンとして捉えるべきはオブジェクトのコピーに必要なコンテキストの方にも思えるが、どうか。

Singleton

今となってはDIコンテナで済むことが多い気がする。実際GoFの一人はこのパターンを削除しようと思ったこともあったようだ。似たようなパターンかつ発展形のMultitionなどインスタンスの生成の制御のパターンは他にもあるので、概念自体は知っておいた方が良いとは思うが。

構造に関するパターン

今となってはプログラミングにおける常識になった感があるが、それ故にきちんと学ぶ価値が高いパターン群と思われる。

Adapter

インターフェースを揃えるためのラッパーという事で、常識的なOOPの使い方ではある。そして実際のところこれは委譲を用いた多態性の一種になるし、DDDにおける腐敗防止層の考えもここに関わってくる。また、AdapterとFacadeは結構近い所にあり、Decratorなどもかなり関連度が高い。

Adapterの実装例として多重継承を用いたパターンが存在するが、私はこれはあまり好ましく思っていない。というのも、Adapterを利用したいシーンというのはAdapteeのインターフェースが他の利用されるモジュールから都合が悪い場合であるため、そもそもその都合の悪いインターフェースを見せる必要があるのかという疑問がある。(元のインターフェースを残しつつ新たなインターフェースを付け加えるならそれはDecoratorだろう。)

Bridge

責務分割や委譲といった概念を学ぶ上で有用なパターンといえる。が、今となっては普通にinterfaceや委譲の事を説明しているだけかもしれない。

例えば何かゲームの開発をしているとして、Enemyクラスには「アルゴリズム」「点数計算」「アイテム排出計算」の3つのメソッドがあるとしよう。深く考えずに「弱くて点数が低くてアイテムがしょぼい敵」「強さも点数もアイテムもボチボチの敵」「強いが点数が高くてアイテムの期待値も高い敵」の3種類を実装したとする。

interface Enemy {
    algorithm(): void
    score(): number
    lottery(): Item | undefined
}

class Weak implements Enemy {
}

class Medium implements Enemy {
}

class Strong implements Enemy {
}

これで終了かと思いきや「弱いんだけどアイテムがおいしい敵がたまにボーナスで出るのも良くないか?」「強くてアイテムもしょぼいけど点数がさらに高い、スコアアタック用の敵を出すのも面白くないか?」などといったアイディアが出てしまったとする。そうなると、以下のようにどんどんサブクラスを嫌な形で増やすことになりかねない。


class WeakBonusItem extends Weak {
    lottery() // Strongのlotteryのコピペ
}

class StrongBonusScore extends Enemy {
    score() // 新規独自実装
    lottery() // Weakのlotteryのコピペ
}

勿論これは遅かれ早かれ破綻する。この場合、点数計算やアイテム抽選といった責務を分割して、別のオブジェクトに委譲する事が考えられる。

interface ScoreCalcurator {
    score(e: Enemy)
}

interface LoteryCalcurator {
    lottery(e: Enemy)
}

abstract class Enemy implements Enemy {
    scoreCalc: ScoreCalcurator
    lotteryCalc: LoteryCalcurator

    abstract algorithm()

    score() {
        return this.scoreCalc.score(this)
    }

    score() {
        return this.lotteryCalc.lottery(this)
    }
}

このように責務毎にオブジェクトを分けて処理を委譲する事で、それぞれのクラスを独立して拡張できる、というのがBridgeパターン。本当に超普通のOOPのやり方ではあるが、超普通ゆえに重要ではあるので一応説明しておいた。

Composite

再帰的な構造であり、特に末端のノードと中間のノードを統一的に扱いたい場合のパターン。ファイルシステム(あるいはそれを模したデータ構造)などが特に分かりやすいか。

Decorator

元のインスタンスに後付けで機能を追加する。通常の継承がコンパイル時の静的な機能拡張に対して、Decoratorは実行時の機能拡張になり得る。

先のゲームを例に出すと、一定の条件を満たすと一定期間、新たに出現した敵がボーナスアイテムを敵が落とすようになるとしよう。この場合、以下のように元のEnemyの派生クラスには手を加えずに、BonusTimeEnemyクラスを作り、Enemyのインスタンスに多くの処理を委譲してBonusTimeEnemy固有の処理を実装する事が考えられる。

class BonusTimeEnemy implements Enemy {
    constructor(delegate: Enemy) {}

    algorithm() {
        return this.delegate.algorithm()
    }

    score() {
        return this.delegate.score()
    }

    lottery() {
        // 独自実装
        const item = this.delegate.lottery()
        if (item === undefined) {
            return DEFAULT_ITEM // とりあえずボーナスタイムなので最低保証みたいな感じ
        }
        return this.greateItem(item) // 何かアイテムをグレードアップする処理
    }

}

Facade

複数のオブジェクトを用いた処理の受付窓口として分かりやすいメソッドを用意するのは良くあるパターン。例えば責務分割の結果、あるドメインの責務を他のモジュールに提供するインターフェースとしてFacadeを用意して、ドメイン内でさらに細かい責務分割を行い、それらの統合をFacadeに任せるといった形を取るなど。

Flyweight

省メモリのためのパターンで、キャッシュ機構を作る時など有用な場面は多々ある。とはいえ、これに相当する機能を他の生成に関するパターンとの合わせ技でフレームワークで提供されていることが多そうな気もする。

Flyweightパターンは特に不変オブジェクトと組み合わせて使う事が多いので、そちらについても学んだ方が良いか。

Proxy

リモートプロキシやら遅延ロードやらを筆頭に、中間層を置いて呼び出し先の(呼び出し元からするとどうでもいい)振る舞いの詳細を気にしなくて済むようにしたり、呼び出し先に必要な情報を集める口を用意したりするのは極めて一般的な手法。(後者はDecoratorやAdapterなどと関連する。)

振る舞いに関するパターン

玉石混交。定跡として押さえておくべきものもあれば、かなりどうでもいいものもある。

Chain of Responsibility

自身が処理できないオブジェクトを別のハンドラーに委譲、という事を繰り返すパターンであり、これはフレームワークで例外ハンドラー機構を作る時などが分かりやすいか。これも有用な場面は多いが、一から自分で作ることは少なくなっているかもしれない。また実際にこのパターンそのままで作らなくても、抽象的な発想としては同様のものに落ち着くことがある。

GoFのデザインパターンに割と近い形で実装すると、下記のような構造になるだろうか。

interface Message {
    ...
}

interface Result {
    ...
}

class Handler {
    next: Handler | null

    setNext(n: Handler) {
        if (this.next === null) {
            this.next = n
        } else {
            this.next.setNext(n)
        }
    }

    handle(message: Message): Result | null {
        const r = this.process(message)
        if (r !== null) {
            return r
        } else if (this.next !== null) {
            return this.next.handle(message)
        }
        return null
    }

    process(message: Message): Result | null
}

勿論これは問題ないのだが、別に下記のような実装でも良いことがあったりする。

class Handler {
    handlerFunctions: ((m: Message) => Result | null)[] = []

    addHandler(h: Handler) {
        this.handlerFunctions.push(h)
    }

    handle(message: Message): Result | null {
        for (const h in this.handlerFunctions) {
            const r = h(message)
            if (r !== null) {
            return r
            }
        }
        return null
    }
}

どちらにしろ実現したい事は同じ。このパターンに限らず、些末な実装詳細にはあまり拘らず、何を実現したいのかの本質的な部分に目を向けていくと良いと思われる。

Command

処理自体をオブジェクトにして、リクエスト処理の発行とそれによる実行を分離するパターン。これは応用範囲が広いパターンで、非同期処理やら各種UIライブラリやら履歴やロギングやら色々な所で出てくる。

Interpreter

流石にニッチ過ぎるので別に学ばなくても良いと思われる。というか構文解析は何度もやってきたものの、このパターンを念頭においた事は一度もない……。いや構文木とその評価はInterpreterなんだが、そんな一秒も考えることなく自明な事にパターン名を付けられても……。

Iterator

モダンなプログラミング言語は言語の機能として既に持っている事が殆ど。概念自体は有用なので、学ぶに越したことはない。

Mediator

オブジェクトの相互作用の際にMediatorを介する事で疎結合にするパターン。MediatorがGodクラスになるというアンチパターンにもなりかねないので、かなり応用編的なパターンにも思える。

例えば単一のMediatorの実装が肥大化することを防ぐ場合、Mediatorの実装を複数のクラスに分け、それらを統合するMediatorを置くことになるが、これはChain of Responsibilityとも言えるし、Compositeかもしれないし、そもそも相互作用がネットワークを介する場合にはProxyも関わってくるかもしれないし、インターフェースを揃えるためにAdapterが必要かもしれない。

などと考えると、他のパターンを学んだうえでの応用編とする方が良いかもしれない。

Memento

Undo/Redoの実装はそれこそ場合によりけりなので、パターンとしてまとめるものか?という気がしてならない。どちらかというとCommandの方が概念的に重要と思われる。

Observer

GUIライブラリで死ぬほど使う。既にライブラリやフレームワークで提供されている事が多く、近年はそこに関数オブジェクトを渡して終わりみたいなケースも多々あるように思える。勿論概念としては非常に有用なので、これは学ばなければならないパターンの一つ。

State

なんかゲームプログラミングで使った覚えがあるが、他では使いどころが難しい気もする。(下手にやると条件分岐を普通に書くよりも凝集度が下がってメンテしにくくなったりする。)

Strategy

アルゴリズムの動的切り替えというアイディアは依然として有用。とはいえ、関数オブジェクトで実現できる範囲も多い。

Template Method

予め処理の順序が規定された雛形があり、そこの実装詳細を埋めていくというのはフレームワークで特に良くあるパターン。先に出したFactory MethodやAbstract Factoryの使われ方もこれの一種と見做せることがある。

冒頭で出した氏江魚の反転の一番分かりやすい実例の一つがTemplate Methodで、むしろ制御の反転について学ぶ際に実例としてTemplate Methodを学ぶといった方が分かりやすい感もある。

Visitor

必要悪。……というのは言い過ぎかもしれないが、実際の所Visitorはダブルディスパッチによって実現する必要があるため、Visitorとそれをacceptする側が密結合になってしまい、そこが気持ち悪い事がある。汎用性を高めようとするとジェネリクスを使って型定義がどんどん複雑になっていくことがあるのも気にかかる。

実際の所、Visitorを使う場面というのはパターンマッチの使える言語であればそれで済ませたいところだったりする。例えば下記のような木構造の処理を考えてみよう。

class Branch {
    name: string
    nodes: Tree[]
}

class Leaf {
    name: string
}

type Tree = Leaf | Branch

これをVisitorパターンで書くとこうなる。

interface Entry {
    accept(visitor: Visitor)
}

class Branch implements Entry{
    name: string
    nodes: (Leaf | Branch) []
    accept(visitor: Visitor) {
        visitor.visitBranch(this)
    }
}

class Leaf implements Entry {
    name: string
    accept(visitor: Visitor) {
        visitor.visitLeaf(this)
    }
}

class Visitor {
    visitLeaf(leaf: Leaf)
    visitBranch(branch: Branch)
}

もう書き慣れたと言えば書き慣れたが、考えてみると少し複雑すぎる気がしなくもない。なんでこれが必要になっているかというと、パターンマッチのような構文がなく、かといってinstanceofのような型チェックだと静的に型チェックしきれないので処理する対象の型が増えた時にコンパイルエラーにならずにチェック漏れが起きることに起因する。

パターンマッチのある言語だったら、下記のようなコードで済むし静的型チェックが効く。(TypeScriptっぽい言語にそれっぽい構文が入ったと思って読んでくれ。)

class Branch {
    name: string
    nodes: Tree[]
}

class Leaf {
    name: string
}

type Tree = Leaf | Branch

function visitTree(tree: Tree) {
    match tree {
        when (leaf: Leaf): {
            console.log(leaf.name)
        }
        when (branch: Branch): {
            console.log(branch.name)
            branch.nodes.forEach(n => visitTree(n))
        }
    }
}

元となったTreeの構造を構築する時にVisitorの事を考えずにに済むという点で、パターンマッチ方式の方が概ねすっきりした定義になっていると思う。

実際Java21以降のJavaにはswitchで型の判定ができるようになっているので、上記のようなコードを書くことができるようになっているはずだ。