normcore.dev

GoFは古いのか? Podcastで聞いた「再整理案」を一次ソースから読み直す

よく聞いている技術系Podcastで GoF の「再整理案」の話を聞いて、最初は「今さらGoF?」と思いました。
でも辿ってみると、これは単なる懐古ではなく、変化に耐える設計をどう考えるかという、かなり今っぽい論点につながっていました。

この記事では、

  • Revised GoF は何だったのか
  • 何が面白くて、どこは少し言いすぎなのか
  • React / Express のような現代のWeb開発ではどう読むとしっくりくるのか

を整理してみます。


ただし、これは“公式な現代版GoF”ではない

TL;DR

  • GoF著者たちが2005年に行った「見直しメモ」は面白いが、正式な改訂版ではない。2009年のインタビューでも、Erich Gamma は draft state の notes だと明言している。 (InformIT)
  • 論点の中心は「継承は悪」ではなく、変化に耐える設計と、必要になってからのリファクタリングにある。著者たちは、patterns provide a target to do this と述べている。 (InformIT)
  • 再整理案は「削除リスト」ではなく、重要度に応じた再分類として読むほうが正確。Core / Creational / Peripheral / Other へ組み替えられ、Null Object や Dependency Injection などが追加されている。 (InformIT)
  • DI は重要だが、DIコンテナ必須とまでは言い切れない。Fowler は DI と Service Locator の両方に意義があるとし、より大事なのは configuration と use の分離だとしている。 (martinfowler.com)
  • Null Object も便利だが、常に正義ではない。「何もしない」が意味的に正しい場面でだけ強い。Fowler の Special Case も、その文脈で読むのが自然。 (martinfowler.com)
  • React / Express では GoF は消えたのではなく、composition / hooks / middleware / service 層に吸収されて見えにくくなった、と読むとしっくりくる。GoFは今も「設計意図を読む語彙」として役に立つ。 (InformIT)

技術系Podcastでデザインパターンの話題が出ていて、「今さらGoF?」と思いながら聞いてみたら、意外と面白かった。
特に引っかかったのは、GoF の著者たち自身が「いま見直すならどう整理するか」を語っていた、という話だ。

大事なのはここで語られている「Revised GoF」は、公式な新版カタログではない。InformIT のインタビューで Erich Gamma は、これは 2005 年に行った見直しのドラフトメモだと説明している。
つまり、読む価値は十分にあるが、「GoF が正式に現代版へ更新された」と受け取るのは正確ではない。 (InformIT)

継承を悪者にするより、「変化に耐える設計」を重視する

元記事の問題提起で印象的なのは、「継承は密結合の元凶」という感覚だ。これは実務的にはかなり共感できる。深い継承階層は、たしかに設計を硬直させやすい。

ただ、一次ソースの主張はもう少し広い。
著者たちは、再利用可能なソフトウェアの多くはライブラリやフレームワーク側へ移り、一般の開発者にとって重要なのは design for change だと述べている。そのうえで、多くの場合は最初から完成形を当てにいくより、必要になったときに設計をリファクタし、その目標としてパターンを使うほうがよいと話している。論点は「反・継承」より、むしろ反・過剰設計に近い。 (InformIT)

なので自分は、この話を「継承は悪」という標語ではなく、継承を再利用のデフォルト手段だと思わないこととして読むのが自然だと思う。
まずはシンプルに作る。変化が見えたら、そこで責務分割や委譲やパターンを導入する。その順番のほうが、現代の実務には合っている。

守るべきは「実装の再利用」より「利用側の安定性」

GoF を学ぶと、「どう再利用するか」に意識が向きがちだ。
でも今あらためて重要なのは、利用側を壊さないことだと思う。Iterator が分かりやすい例で、内部構造がどうであれ、呼び出し側が同じ規約で扱えることに価値がある。ここで守られているのは内部実装ではなく、利用コードの安定性だ。著者たち自身も、今の読者は「どのパターンを選ぶか」で悩む人より、「他人が選んだパターンを理解して使う人」に近いと述べている。 (InformIT)

この視点に立つと、パターンの価値は“実装技法”よりも、変更から利用側を守るための設計語彙にある。
だから GoF は、手品集ではなく共通言語として読むのがいちばん実務的だと思う。

パターンは「事前の計画」ではなく「進化の道標」

この再整理案の中で、いちばん腹落ちしたのはここだった。
パターンは設計開始時に当てにいくものではなく、リファクタリングの中で浮かび上がる目標として使う、という考え方だ。著者たちは実際に、patterns provide a target to do this と述べている。 (InformIT)

この読み方をすると、GoFを学ぶ意味も変わる。
23個を暗記することより、コードが育った先にどんな構造が現れるかを見抜くことのほうが重要になる。YAGNI や TDD と相性がいいのも、そのためだと思う。

15年後のGoF再整理案

InformIT のインタビューで示された再整理案は、単純な「削除リスト」ではない。
むしろ、重要度や出番の多さに応じてパターンを並べ直したものとして読むほうが自然だと思う。

Core

現代でも中心的で、頻度・汎用性ともに高いもの。

パターン ひとことで言うと よく使われるシーン
Composite 複数の要素を、ひとまとまりとして同じように扱えるようにする。 UI コンポーネントのネスト、ディレクトリ構造、メニューやレイアウト
Strategy 「やり方」だけを後から差し替えられるようにする。 ソート、バリデーション、料金計算、表示ルールの切り替え
State 状態ごとの分岐を整理して、状態ごとの振る舞いを分ける。 フォーム状態、注文ステータス、ワークフロー管理
Command 操作そのものをひとつの単位として扱えるようにする。 UI アクション、ジョブ実行、undo/redo、ユースケースの切り出し
Iterator 中の構造を知らなくても、順番に取り出せるようにする。 コレクション走査、ページネーション結果の処理、ストリーム読み取り
Proxy 本体の手前にひとつ挟んで、アクセスを制御する。 認証付きアクセス、キャッシュ、遅延ロード、API ラッパー
Template Method 大枠の流れは共通にして、一部だけ差し替えられるようにする。 共通フローを持つ処理、データ取得処理、Hook や基底ロジックの整理
Facade 複雑な仕組みを、使いやすい入口ひとつにまとめる。 service 層、API クライアント、複数ライブラリをまとめた Hook やラッパー

Creational

オブジェクトや依存関係の組み立てに関わるもの。

パターン ひとことで言うと よく使われるシーン
Factory 作り方を呼び出し側から隠して、生成方法の変更をしやすくする。 実装切り替え、設定に応じた生成、初期化処理の集約
Prototype 既存のものを元にして、新しいものを複製して作る。 設定済みオブジェクトの複製、テンプレート生成
Builder 複雑なものを、手順を分けて少しずつ組み立てる。 設定項目の多いオブジェクト生成、テストデータ構築
Dependency Injection 依存を使う場所と、依存を組み立てる場所を分ける。 service の wiring、DI コンテナ、Context や初期化関数による注入

Peripheral

依然として有用だが、適用場面はやや限定的なもの。

パターン ひとことで言うと よく使われるシーン
Abstract Factory 関連する部品一式を、まとめて作り分けられるようにする。 UI テーマごとの部品生成、環境差し替え、製品ファミリーの切り替え
Visitor データ構造はそのままに、あとから処理だけ増やしやすくする。 AST 処理、構文木の解析、複雑な構造への操作追加
Decorator 元の仕組みを変えずに、機能をあとから足す。 ログ追加、計測、権限チェック、UI のラップ
Mediator バラバラなやり取りを、仲介役に集めて整理する。 複数コンポーネント間の調停、イベントハブ、画面内の連携制御
Type Object 種類ごとの違いを、データとして持てるようにする。 ゲームデータ、商品種別、設定駆動の振る舞い管理
Null Object null の代わりに、「何もしない役」を用意する。 NullLogger、空の通知先、未設定時のデフォルト振る舞い
Extension Object もとの型を壊さずに、あとから拡張ポイントを足す。 プラグイン機構、拡張ポイントの後付け

Other

完全に不要ではないが、主要メンバーからは外れるもの。

パターン ひとことで言うと よく使われるシーン
Flyweight 共有できるものをまとめて、メモリ使用量を抑える。 大量要素の描画、文字情報、軽量オブジェクトの共有
Interpreter ルールや式を、プログラムとして解釈して実行する。 DSL、簡易クエリ言語、ルールエンジン

この整理を「不要になったパターンの一覧」だと読むとズレる。
むしろ、いまでも中心に置きたいもの、使いどころが限定されるもの、出番が少ないものを並べ直した地図として読むほうがしっくりくる。

以上が、インタビュー内で共有されたドラフトメモの分類である。 (InformIT)

ここで注意したいのは、これを「リストラ一覧」として読むとズレることだ。
一次ソースで明確なのは、Erich Gamma が Singleton はほぼ design smell なので自分は落としたい と言っていることと、全体としては重要なものを強調し、頻度の低いものを分けたいという意図だったことだ。主題は“追放”ではなく、重みづけの見直しとして読むほうが自然だと思う。 (InformIT)

DI の昇格は納得できる。けれど「DIコンテナ必須」までは言いすぎ

元記事が強く押していたのが、Dependency Injection の昇格だ。
これはかなり納得感がある。依存関係をどこで組み立てるか、利用側から生成責務をどう追い出すかは、現代の設計でかなり重要だからだ。 (InformIT)

ただし、「依存関係が複雑になるほど DI コンテナによる自動化が不可欠」とまで一般化すると、少し強すぎる。Fowler は DI と Service Locator の両方が、具体実装からアプリケーションコードを切り離すと説明している。そのうえで、choice between them is less important than the principle of separating configuration from use と述べている。さらに IoC は理解やデバッグのコストがあるので、自分は必要になるまで避けたい、とも書いている。 (martinfowler.com)

なので、ここはこう読むのがちょうどいい。
依存を使う場所と、依存を組み立てる場所を分けることが本質であり、その有力な実現手段が DI である。コンテナ導入の是非は、規模と複雑性とデバッグ容易性で決める。

Null Object も便利だが、「何もしない」が正しい場面に限る

Null Object が追加メンバーに入っているのも、今の感覚ではかなりわかりやすい。
if (obj != null) を散らさずに済む、という効能は実務的だ。 (InformIT)

ただ、ここも盛りすぎないほうがいい。
Fowler の Special Case は、null の代わりに、呼び出し側が期待する同じインターフェースを持つ特別なオブジェクトを返すという説明になっている。大事なのは、「nullチェックが消える」こと自体ではなく、“何もしない”や“空である”という振る舞いが意味的に正しいかどうかだ。ログを捨てる NullLogger や空コレクションのような場面では強いが、本来はエラーや未達を表すべき場面まで吸収すると、意味が曖昧になる。 (martinfowler.com)

React / Express のような現代のWeb開発では、GoFをどう読むか

ここまで読むと、次に出てくるのは「じゃあ React や Express では GoF をどう読むのか?」という疑問だと思う。

自分の感覚では、GoF の多くは不要になったのではなく、言語機能やフレームワークの作法の中に吸収された
InformIT のインタビューでも、Erich Gamma は JUnit 3 では Composite / Template Method / Command が見えていたのに、JUnit 4 ではアノテーションとテストランナーに置き換わって、表面上パターンが消えたように見える、と説明している。これは React / Express でもかなり近い話だと思う。 (InformIT)

Reactでは、継承よりも composition が先にある

React でまず強く感じるのは Composite だ。
Layout の中に SidebarContent を入れる、Dialog の中に Header / Body / Footer を組み合わせる、という構造は日常的に出てくる。ただし、それはクラス継承ではなく children と composition で表現される。
昔の感覚だと BaseDialog を継承して派生コンポーネントを増やしたくなるが、React ではたいてい、その発想より器は共通・中身は差し替えのほうが柔らかい。これはまさに「継承より合成を優先する」の現代版だと思う。

Strategyは、クラスではなく関数やpropsとして現れる

React でいちばん見つけやすいのは Strategy かもしれない。
ただし ConcreteStrategyA のようなクラスは出てこない。ソート関数を props で渡す、バリデーションルールを差し替える、表示方法を render prop や差し替えコンポーネントで変える。こういうものはどれも、振る舞いの変動点を外に逃がすという意味でかなり Strategy 的だ。
ここで大事なのは「Strategyクラスを書くこと」ではなく、分岐を内側に閉じ込めないことだと思う。

Template Methodは、Hookや共通フローに溶ける

React では古典的な Template Method は見えづらい。
でも考え方は消えていない。取得・ローディング・エラー処理の骨格は共通で、データ変換だけ差し替えたい custom hook や、送信フローは同じで成功後処理だけ違うフォーム処理は、かなり Template Method 的に読める。
つまり Template Method は消えたというより、Hook や高階関数に溶けたのだと思う。

Facadeは、むしろ今のほうが重要

Facade は、現代のほうがむしろ大事かもしれない。
React では API 呼び出し、state、loading、error、整形処理をコンポーネントへ直書きするとすぐ散らかる。だから useUserProfile() のような Hook や、社内UIコンポーネントの薄いラッパーが効く。Express 側でも、DB・外部API・認可・ログを handler に直接書かず、service や use case の窓口にまとめるのが効く。
派手ではないが、利用側に余計な事情を漏らさないという意味で、Facade はかなり実務的だ。

Expressのmiddlewareは、ひとつのパターンに決め打ちしないほうがいい

Express の middleware は、「これは何パターンか」とラベル付けしたくなる。
でも実際には、Chain of Responsibility 的でもあり、Proxy 的でもあり、少し Decorator 的でもある。だから、厳密なラベル当てよりも、どんな変更を handler の外へ追い出しているかを見るほうがいい。
GoF は分類ゲームではなく、設計意図を読むためのレンズとして使うほうが役に立つ。

FactoryやDIも、いまはもっと軽い形で現れる

Web の現場では、SomethingFactory というクラスをわざわざ作ることは少ない。
その代わり、repository と service と logger を束ねて handler を組み立てる関数や、環境に応じて実装を差し替える初期化処理のように、組み立て関数として Factory 的な役割が現れる。

DI も同じで、本質はコンテナではない。
大事なのは、依存を使う場所と、組み立てる場所を分けることだ。React なら props / Context / custom hook、Express なら起動時の wiring や module 初期化で十分なことも多い。ここでもやはり、「形式」より「責務分離」のほうが本質だ。 (martinfowler.com)

まとめ

この再整理案から受け取るべきメッセージは、「GoF は古いから捨てよう」ではないと思う。
むしろ、

  • すべてのパターンを同じ重さで覚える必要はない
  • 設計は最初から完成させるものではなく、変化に応じて育てるもの
  • パターンは“事前の正解”ではなく、“進化した構造を説明する語彙”である

ということのほうが本質に近い。著者たち自身も、patterns の relevance や weight は同じではないと述べている。 (InformIT)

そう考えると、GoF はいまでも十分に現役だ。
ただし、昔のクラス図をそのまま写経する教材としてではない。設計意図を見抜き、変更をどこに閉じ込めるかを話すための共通言語として読むのが、いちばんしっくりくる。

← Back to home