GoFデザインパターンは現代でも有用か(1) — OOPのパラダイムシフトとGoFの誕生
1990年代、OOP(オブジェクト指向プログラミング)は、手続き型言語からのパラダイムシフトでした。そして、OOPの設計ノウハウとしてGoFのデザインパターンが体系化されました。現代では「OOPというコンセプト自体が不要」という新たなパラダイムシフトが起きています。その証左がOOPの中核を意図的に排除したGo言語です。
手続き型からOOPへのパラダイムシフト、そしてOOPの不完全さから生まれたGoFデザインパターン。なぜGoogleはOOPを捨ててGo言語を作ったのか。その背景を整理します。
この記事は「GoFデザインパターンは現代でも有用か — Go言語から再考する」シリーズの第1回(全4回)です。
Table of Contents
自宅の本棚の整理をしていて、『Java言語で学ぶデザインパターン入門第2版』(結城浩著)1を見つけました。「今どきJava言語かね」 と思い調べてみると、5年程前に第3版が出版されていました。
「そう言えば、今どきの言語でデザインパターンってどうなんだ?」と考察していました。
本記事では、なぜ脱オブジェクト指向プログラミング(以降、OOP)のパラダイムシフトが起きたのか、そしてGoFの23のデザインパターンをGo言語ではどう扱うべきかを検討し考察してみます。
手続き型プログラミングの限界
1960年代から1980年代にかけて、ソフトウェア開発の主流は手続き型プログラミングでした。C言語やFortranに代表されるこのパラダイムでは、データと手続き(関数)は分離して管理されます。プログラムの規模が小さいうちは問題になりませんが、規模が大きくなるにつれて、どの関数がどのデータを操作するのかを追跡することが困難になっていきました。
当時仕事ではメインフレーム中心で私の職場ではPL/Iを使っていましたが、規模を大きくしないためかバッチ指向のデザインが好まれていました。 そのため1つ1つは短いプログラムですが、内部ではグローバル変数への依存、関数間の暗黙的なデータ受け渡しが行われていました。そして短いプログラムでも、結局ファイルを介してバッチストリーム全体がストレージ共有をしているので、変更の影響範囲が予測できないという問題が深刻化しました。
やがて、1990年代に入るとWindows/Macintoshに加えて、IBMのOS/2、SunやHPのワークステーションが業務で使えるレベルのシステムとなり、「ダウンサイジング」の波が業務アプリケーションの世界にも押し寄せました。この問題はさらに致命的になりました。
OOPというパラダイムシフト
OOP(オブジェクト指向プログラミング)は、手続き型プログラミングにおけるデータと手続きの分離がもたらす複雑さに対するパラダイムシフトでした。データとそれを操作する手続きを「オブジェクト」として一体化し、カプセル化によって内部状態を保護する。オブジェクト間はメッセージパッシングで協調し、継承によってコードを再利用する。Simula(1967年)で芽生え、Smalltalk(1980年)で確立されたこの思想は、1990年代にC++やJavaの普及とともに主流となりました。
OOPの普及を決定づけたのは、ダウンサイジングがもたらしたGUIクライアントの台頭です。Windowsの普及により、ソフトウェアはウィンドウ、ボタン、メニューといった視覚的な部品で構成されるようになりました。これらのUI部品は、状態を持ち、イベントに応答し、階層的に組み合わされる — まさにオブジェクトとしてモデル化するのに適した対象でした。GUIツールキットはOOPの実践的な成功例となり、OOPこそがソフトウェア開発の正しいアプローチであるという認識を広めました。
一方でOOPは、カプセル化、継承、ポリモーフィズムという新しい概念を持ち込んだために、手続き型言語で機能分割しかやって来なかった当時の開発者たちに「OOPではどうコードを書くべきなのか」と悩ませることになりました。
GoFデザインパターンの誕生
この状況の中で、1994年にGoF(Gang of Four)2が『Design Patterns: Elements of Reusable Object-Oriented Software』を出版しました。GoFはオブジェクト指向設計で繰り返し現れる23のパターンを、生成(Creational)5種、構造(Structural)7種、振舞(Behavioral)11種の3つに分類して体系化しました。これらのパターンは、当時の開発者にとって「OOPでよいコードを書くためのお手本」として受け入れられました。
| 分類 | パターン名 | 概要 |
|---|---|---|
| Creational(生成) | Abstract Factory | オブジェクトを具象クラスを指定せずに生成 |
| Builder | 複雑なオブジェクトを抽象化し段階的に生成 | |
| Factory Method | オブジェクトの生成をサブクラスに委ねる | |
| Prototype | 既存オブジェクトをコピーし新オブジェクトを生成 | |
| Singleton | インスタンスが1つだけであることを保証 | |
| Structural(構造) | Adapter | 互換性のないインターフェースのクラス同士を接続 |
| Bridge | 抽象部分と実装部分を分離。独立に変更可能 | |
| Composite | ツリー構造により個別オブジェクトと合成を同一視 | |
| Decorator | オブジェクトに動的に新しい責務を追加 | |
| Facade | サブシステムの複雑なインターフェースの統一的な窓口 | |
| Flyweight | 多数の細粒度オブジェクトを共有してメモリ使用量削減 | |
| Proxy | 他のオブジェクトへのアクセス制御する代理オブジェクト | |
| Behavioral(振舞) | Chain of Responsibility | リクエスト処理するオブジェクト連鎖を構成し処理を委譲 |
| Command | リクエストをオブジェクト化し、実行取消や再実行を可能 | |
| Interpreter | 言語の文法を定義し解釈するインタプリタを提供 | |
| Iterator | 内部構造を公開せずに、集合要素へアクセスする方法 | |
| Mediator | オブジェクト間の相互作用を仲介者に集約し疎結合 | |
| Memento | オブジェクトの内部状態を保存し、復元可能に | |
| Observer | 状態変化を他のオブジェクトに自動的に通知 | |
| State | オブジェクトの内部状態に応じて振舞を変更する | |
| Strategy | アルゴリズムをカプセル化し、実行時に切り替え可能 | |
| Template Method | アルゴリズムの骨格を定義、ステップをサブクラスに委譲 | |
| Visitor | データ構造と処理を分離し、新しい処理を追加しやすくする |
新たなパラダイムシフト — なぜGoogleはOOPを捨てたのか
GoFが出版されてから10年以上が経った2007年、Googleの中で新しい言語の設計が始まりました3。
新言語の開発に踏み切った背景には、GoogleのC++コードベースの深刻な問題がありました。数百万行規模のコードではビルドに数十分から数時間を要し、複雑な依存関係の管理は開発者の大きな負担になっていました。C++の持つ豊富な機能 — テンプレート、多重継承、演算子オーバーロード — は、大規模なチーム開発においてコードの可読性を損ない、新しいメンバーの参入障壁を高めていました。
Go言語の設計者たちが出した答えは、複雑さを足すのではなく引くことでした。2009年にオープンソースとして公開されたGo言語は、OOPの中核を意図的に排除し、代わりに以下の設計方針を採用しています。
- クラス継承を排除し、合成(composition)と暗黙的インターフェースで代替した
- 参照セマンティクスではなく値セマンティクスをデフォルトにした4
- 振る舞いもオブジェクトに包む制約を廃し、第一級関数とチャネル/ゴルーチンで直接扱えるようにした
確かに近年はC++/JavaのようなOOPでなく、次のような言語を選択するシーンが私自身も増えています。
- Python — クラスも書けるが、関数だけでも十分に実用的
- Ruby — 実は言語設計は徹底的にOOP(全てがオブジェクト)。ただし実際の使われ方は柔軟
- JavaScript — そもそもクラスベースではなくプロトタイプベース。ES6のclassは後付けの構文糖
- TypeScript — JavaScriptに型を足したもの。OOPと関数型のどちらでも自然に書ける
これらはOOPですがマルチパラダイムで、OOPじゃないパラダイムでもコードが書けます。
では、Go言語はOOPなのでしょうか。Go言語にはメソッドやインターフェースがあり、カプセル化も実現できます。しかし、クラスや継承はありません。Go言語のFAQでは「Yes and no」と回答されています。Go言語はOOPの有用な部分(カプセル化、多態性)を残しつつ、複雑さの元凶(継承、型階層)を切り捨てた言語です。上記のような言語からさらにOOPを薄めたものと言えます。
Go言語の登場は、OOPに続く新たなパラダイムシフトの象徴です。
ここで1つの疑問が浮かびます。GoFのデザインパターンは「OOPでよいコードを書くためのお手本」として受け入れられました。しかし、Go言語はそのOOPの中核 — 継承、型階層、参照セマンティクス — を排除しています。もしGoFのパターンが本当にOOPの仕組みに依存したものであるなら、Go言語ではそれらのパターンは成立しないはずです。逆に、パターンの中に言語やパラダイムに依存しない普遍的な設計原則が含まれているなら、Go言語でも形を変えて生き残るはずです。
次回は、GoFの23のパターンをGo言語の視点から1つずつ再検討し、この仮説を検証していきます。
-
このリンクはAmazonアソシエイトのリンクです。 ↩
-
Gang of Four(GoF)は、Erich Gamma、Richard Helm、Ralph Johnson、John Vlissidesの4人の通称。 ↩
-
主導したのはRob Pike(UNIXやPlan 9の開発者)、Ken Thompson(UNIXの共同開発者)、Robert Griesemerの3人です。 ↩
-
参照セマンティクス(reference semantics)とは、変数への代入がオブジェクトの参照(ポインタ)をコピーする振る舞いのこと。値セマンティクス(value semantics)とは、変数への代入がデータそのものをコピーする振る舞いのこと。 ↩