Bridge
一言でいうと
Bridge (ブリッジ、 架け橋) は、 構造的パターンの一つです。 巨大なクラスや密接に関連したクラスの集まりを、 抽象部分と実装部分という、 二つの階層に分離し、 それぞれが独立して開発できるようにします。
問題
抽象と実装だって? 何だか怖そう? 心配無用。 ある簡単な例を考えていきましょう。
ええと、 幾何学的な形状を表す Shape
クラスとそのサブクラスとして Circle
(円) と Square
(正方形) があるとします。 色という要素を表現するために、 このクラス階層を拡張して、 Red
(赤) と Blue
(青) の形状クラスを作りたいとします。 しかしすでに二つのサブクラスがあるため、 この組み合わせである四つのサブクラスが必要となります。 BlueCircle
とか RedSquare
です。
クラス階層に形状の種類と色を加えるとそれは指数関数的に増加します。 たとえば、 三角形を追加するためには、 それぞれの色に一つずつ、 計二つのサブクラスを導入する必要があります。 その後、 色を一つ追加するには、 形状の種類一つにつき一つずつ、 計三つのサブクラスが必要となります。
解決策
この問題は、 形状クラスを、 形と色という二つの独立した次元で拡張しようとしているために発生します。 これはクラスの継承に関する非常に一般的な問題です。
Bridge パターンでは、 継承からオブジェクトの合成に切り替えることでこの問題を解決しようとします。 つまり、 次元のいずれか一つを別のクラス階層に抽出するということです。 一つのクラス内にすべての状態と振る舞いを持つ代わりに、 元のクラスは、 新しい階層のオブジェクトを参照するようにします。
このやり方に従うと、 色に関するコードは、 Red
と Blue
の二つのサブクラスを持つ専用のクラスに抽出します。 Shape
クラスには、 色オブジェクト二つのうち一つへの参照が付きます。 形状は、 色に関する作業を色オブジェクトに委任します。 この参照は、 Shape
クラスと Color
クラスのブリッジ=架け橋として機能します。 今後は、 色を追加しても形状階層を変更する必要はなく、 逆もまた可です。
抽象化と実装
「GoF 本 」 には、 抽象化 (Abstraction) と実装 (Implementation) という用語が、 Bridge の定義の中で使われています。 著者の意見では、 これらの用語は学術的に響きすぎ、 このパターンに実際よりも複雑な印象を与えます。 形状と色の簡単な例を読んだところで、 GoF 本の怖そううな言葉を解読していきましょう。
抽象化 (インターフェースとも) は、 ある項目の高レベル制御層です。 この層では、 実際の仕事は行わないことになっています。 仕事は、 実装層 (プラットフォームとも) へ移譲されます。
注意: ここではあるプログラミング言語の interface や abstract についてお話ししているわけではありません。 これらは同じものではありません。
ここで実際のアプリケーションについての話をすると、 抽象化はグラフィカル・ユーザー・インターフェース (GUI)、 そして実装は、 ユーザーの動作に応じて GUI 層が呼び出す基盤オペレーティングシステムのコード (API) と考えることもできます。
一般的に、 このようなアプリは、 二つの独立した方向に拡張することができます:
- いくつかの異なる GUI (たとえば、 通常の顧客向けに仕立てたり、 管理者向けに仕立てたり) を提供する。
- いくつかの異なる API をサポートする (たとえば、 Windows、 Linux、 macOS 下でアプリを起動を可能とするために)。
最悪の場合、 このアプリは大盛りスパゲッティーのように見えるかもしれません。 様々な API と様々な GUI を接続する何百もの条件文が、 コード全体に散らばっています。
特定のインターフェースとプラットフォームの組み合わせに関連するコードを個別のクラスに抽出することで、 この混乱的状況に秩序をもたらすことができます。 しかし、 すぐにこういうクラスが多数あることがわかります。 新しい GUI を一つ追加したり、 別の API を一つサポートするだけでも、 さらに多くのクラスを作成する必要があり、 クラス階層は指数関数的に増加します。
Bridge パターンでこの問題を解決しましょう。 このパターンに従うと、 クラスを二つの階層に分割することになります。
- 抽象化: アプリの GUI レイヤー
- 実装: オペレーティングシステムの API
抽象化層のオブジェクトは、 アプリケーションの外観を制御し、 実際の作業をリンクされた実装層のオブジェクトに委任します。 異なる実装は、 共通のインターフェースに従っている限り、 交換が可能です。 これにより、 Windows 下でも Linux 下でも同じ GUI が動作するようになります。
その結果、 API 関連のクラスに触れることなく、 GUI クラスを変更できます。 さらに、 新たなオペレーティングシステムをサポートするためには、 実装階層にサブクラスを作成するだけですみます。
構造
-
抽象化層 (Abstraction) は、 高レベルの制御ロジックを提供します。 実際の低レベルの作業は実装オブジェクトに任されています。
-
実装 (Implementation) は、 すべての具象実装に共通のインターフェースを宣言します。 抽象化層は、 ここで宣言されたメソッドを介してのみ実装オブジェクトと通信することができます。
抽象化は、 実装と同じメソッドを並べただけでもかまいません。 しかし通常、 抽象化は複雑な振る舞いを宣言します。 それらの振る舞いは、 実装によって宣言された多種多様な基本操作を使用します。
-
具象的実装 (Concrete Implementation) には、 プラットフォーム固有のコードが含まれています。
-
整った抽象化 (Refined Abstraction) は、 制御ロジックの変種を提供します。 親と同様に、 一般的な実装インターフェースを介して各種の異なる実装を使います。
-
通常、 クライアント (Client) は、 抽象化層とだけやりとりをします。 しかし、 抽象化層のオブジェクトと実装オブジェクトを結びつけるのは、 クライアントの仕事です。
擬似コード
この例では、 Bridge パターンが、 デバイスとそのリモコンを管理するアプリの一枚岩のコードを分割するのにどう役立つかを説明します。 Device
系のクラスは実装として機能し、 Remote
系のクラスは抽象化層です。
基底のリモコン・クラスは、 機器オブジェクトへ結びつけるための参照フィールドを宣言しています。 すべてのリモコンは、 機器とのやり取りを一般的な機器インターフェースを介して行い、 同じリモコンで複数の種類の機器をサポートできます。
リモコン・クラスの開発は、 機器クラスと独立して行えます。 行うべきことは、 新しいリモコンのサブクラスを作成することだけです。 たとえば、 基本的なリモコンには二つのボタンしかありませんが、 これを拡張して、 追加の電池やタッチスクリーンなどの機能を追加できます。
クライアント・コードは、 好みの種類のリモコンを特定の機器オブジェクトとリモコンのコンストラクターを介して結びつけます。
適応性
たとえば、 データベースなど、 機能にちょっとした違い変種がある場合、 一枚岩のコードを分割して組織するために、 Bridge パターンを使います。
クラスが増大するにつれ、 その動作を理解することが難しくなり、 変更に時間がかかるようになります。 機能の一つの変種に加えられた変更には、 クラス全体の変更を要するようになり、 誤りや重大な副作用への対処を欠くなどの弊害につながります。
Bridgeパターンでは、 一枚岩のクラスをいくつかのクラス階層に分割します。 その後は、 独立して各階層のクラスを変更できるようになります。 この方法により、 コードの保守が簡素化され、 既存のコードが動かなくなるリスクを最小限に抑えられます。
クラスをいくつかの直交的 (独立した) 次元で拡張する必要がある場合、 このパターンを使用します。
Bridge では、 各次元に対して個別のクラス階層を抽出することを勧めます。 元のクラスは、 すべてを自身で行うのではなく、 それぞれの階層に属するオブジェクトに関連作業を委任します。
実行時に実装を切り替える必要がある場合は、 Bridge を使用してください。
これは、 必ずしも必要ありませんが、 Bridge パターンでは、 実装オブジェクトを抽象化層内で置き換えることも許されています。 新しい値をフィールドに割り当てるのと同じくらい簡単です。
ちなみに、 この最後の項目のため、 多くの人々が Bridge を Strategy パターンと混同します。 覚えておいていただきたことは、 パターンはクラスに特定の構造を持たせる以上のものであるということです。 意図と解決すべき問題を伝えるためにも使用します。
実装方法
-
クラス内の直交する次元を特定します。 これらの独立した概念は、 たとえば抽象化とプラットフォーム (OS) だったり、 ドメインとインフラストラクチャーだったり、 フロントエンドとバックエンドだったり、 インターフェースと実装だったりします。
-
クライアントが必要とする操作は何であるかを考え、 抽象化層の基底クラスで定義します。
-
すべてのプラットフォームで利用可能な操作は何かを決定します。 抽象化層が必要とするものを一般的な実装インターフェースで宣言します。
-
サポートするすべてのプラットフォームに対して具体的な実装クラスを作成します。 すべて実装クラスはプラットフォームの実装インターフェースに従うようにしてください。
-
抽象化層のクラス内に、 実装型の参照フィールドを追加します。 抽象化層は、 このフィールドから参照される実装オブジェクトにほとんどの仕事を移譲します。
-
高レベルのロジックにいくつかの変種がある場合は、 各変種ごとに抽象化層の基底クラスを拡張して、 より洗練した抽象化層クラスを作成します。
-
クライアント・コードは、 抽象化層のクラスのコンストラクターに実装オブジェクトを渡して、 互いを関連づけます。 その後、 クライアントは実装のことは忘れ、 抽象化層のオブジェクトとだけやりとりをします。
長所と短所
- プラットフォーム非依存のクラスやアプリを作成できる。
- クライアント・コードは高レベルの抽象化層で動作。 プラットフォームの詳細に左右されない。
- 開放閉鎖の原則。 新規の抽象化層と実装を互いに独立して導入可。
- 単一責任の原則。 抽象化層では、 高レベルのロジックに、 実装層では、 プラットフォームの詳細に集中できる。
- 高度に密着したクラスに本パターンを適用すると、 コードが余計に複雑になる可能性あり。
他のパターンとの関係
-
Bridge は通常、 アプリケーションの部分部分を独立して開発できるように、 設計当初から使われます。 一方、 Adapter は、 既存のアプリケーションに対して利用され、 本来は互換性のないクラスとうまく動作させるために使われます。
-
Bridge、 State、 Strategy (と限られた意味合いでは、 Adapter も) は、 非常に似た構造をしています。 実際のところ、 これらの全てのパターンは、 合成に基づいており、 仕事を他のオブジェクトに委任します。 しかしながら、 違う問題を解決します。 パターンは、 単にコードを特定の方法で構造化するためのレシピではありません。 パターンが解決する問題に関して、 開発者同士がするコミュニケーションの道具でもあります。
-
Abstract Factory は、 Bridge と一緒に使用できます。 この組み合わせは、 Bridge によって定義された抽象化層のいくつかが特定の実装としか動作しない場合に便利です。 この場合、 Abstract Factory はこれらの関係をカプセル化し、 クライアント・コードから複雑さを隠すことができます。
-
Builder と Bridge を組み合わせることができます: ディレクター・クラスは抽象化層の役割を果たし、 ビルダーは実装です。