春のセール

Bridge

別名:ブリッジ

一言でいうと

Bridge ブリッジ 架け橋 構造的パターンの一つです 巨大なクラスや密接に関連したクラスの集まりを 抽象部分と実装部分という 二つの階層に分離し それぞれが独立して開発できるようにします

Bridge デザインパターン

問題

だって 何だか怖そう 心配無用 ある簡単な例を考えていきましょう

ええと 幾何学的な形状を表す Shape クラスとそのサブクラスとして Circle Square 正方形 があるとします 色という要素を表現するために このクラス階層を拡張して Red Blue の形状クラスを作りたいとします しかしすでに二つのサブクラスがあるため この組み合わせである四つのサブクラスが必要となります Blue­Circle とか Red­Square です

Bridge パターンの問題

クラスの組み合わせ数は幾何学的な速さで増加する

クラス階層に形状の種類と色を加えるとそれは指数関数的に増加します たとえば 三角形を追加するためには それぞれの色に一つずつ 計二つのサブクラスを導入する必要があります その後 色を一つ追加するには 形状の種類一つにつき一つずつ 計三つのサブクラスが必要となります

解決策

この問題は 形状クラスを 形と色という二つの独立した次元で拡張しようとしているために発生します これはクラスの継承に関する非常に一般的な問題です

Bridge パターンでは 継承からオブジェクトの合成に切り替えることでこの問題を解決しようとします つまり 次元のいずれか一つを別のクラス階層に抽出するということです 一つのクラス内にすべての状態と振る舞いを持つ代わりに 元のクラスは 新しい階層のオブジェクトを参照するようにします

Bridge パターンの提案する解決策

クラス階層の膨張を防ぐために いくつかの関連する階層に変換する

このやり方に従うと 色に関するコードは RedBlue の二つのサブクラスを持つ専用のクラスに抽出します Shape クラスには 色オブジェクト二つのうち一つへの参照が付きます 形状は 色に関する作業を色オブジェクトに委任します この参照は Shape クラスと Color クラスのブリッジ=架け橋として機能します 今後は 色を追加しても形状階層を変更する必要はなく 逆もまた可です

抽象化と実装

GoF 本  には Abstraction Implementation という用語が Bridge の定義の中で使われています 著者の意見では これらの用語は学術的に響きすぎ このパターンに実際よりも複雑な印象を与えます 形状と色の簡単な例を読んだところで GoF 本の怖そううな言葉を解読していきましょう

インターフェースとも ある項目の高レベル制御層です この層では 実際の仕事は行わないことになっています 仕事は プラットフォームとも へ移譲されます

注意 ここではあるプログラミング言語の interfaceabstract についてお話ししているわけではありません これらは同じものではありません

ここで実際のアプリケーションについての話をすると 抽象化はグラフィカル・ユーザー・インターフェース GUI そして実装は ユーザーの動作に応じて GUI 層が呼び出す基盤オペレーティングシステムのコード API と考えることもできます

一般的に このようなアプリは 二つの独立した方向に拡張することができます

  • いくつかの異なる GUI たとえば 通常の顧客向けに仕立てたり 管理者向けに仕立てたり を提供する
  • いくつかの異なる API をサポートする たとえば Windows Linux macOS 下でアプリを起動を可能とするために

最悪の場合 このアプリは大盛りスパゲッティーのように見えるかもしれません 様々な API と様々な GUI を接続する何百もの条件文が コード全体に散らばっています

モジュール化されたコードでは変更管理が簡単にできる

一枚岩のコードベースには 簡単な変更を加えることすらかなり困難です なぜなら  を非常によく理解しなければならないからです 小さく 明確に定義されたモジュールに変更を加えるのは はるかに簡単です

特定のインターフェースとプラットフォームの組み合わせに関連するコードを個別のクラスに抽出することで この混乱的状況に秩序をもたらすことができます しかし すぐにこういうクラスがあることがわかります 新しい GUI を一つ追加したり 別の API を一つサポートするだけでも さらに多くのクラスを作成する必要があり クラス階層は指数関数的に増加します

Bridge パターンでこの問題を解決しましょう このパターンに従うと クラスを二つの階層に分割することになります

  • 抽象化 アプリの GUI レイヤー
  • 実装 オペレーティングシステムの API
プラットフォーム互換アーキテクチャー

プラットフォーム互換アプリケーションを構築する方法の一つ

抽象化層のオブジェクトは アプリケーションの外観を制御し 実際の作業をリンクされた実装層のオブジェクトに委任します 異なる実装は 共通のインターフェースに従っている限り 交換が可能です これにより Windows 下でも Linux 下でも同じ GUI が動作するようになります

その結果 API 関連のクラスに触れることなく GUI クラスを変更できます さらに 新たなオペレーティングシステムをサポートするためには 実装階層にサブクラスを作成するだけですみます

構造

Bridge デザインパターンBridge デザインパターン
  1. 抽象化層 Abstraction 高レベルの制御ロジックを提供します 実際の低レベルの作業は実装オブジェクトに任されています

  2. 実装 Implementation すべての具象実装に共通のインターフェースを宣言します 抽象化層は ここで宣言されたメソッドを介してのみ実装オブジェクトと通信することができます

    抽象化は 実装と同じメソッドを並べただけでもかまいません しかし通常 抽象化は複雑な振る舞いを宣言します それらの振る舞いは 実装によって宣言された多種多様な基本操作を使用します

  3. 具象的実装 Concrete Implementation には プラットフォーム固有のコードが含まれています

  4. 整った抽象化 Refined Abstraction 制御ロジックの変種を提供します 親と同様に 一般的な実装インターフェースを介して各種の異なる実装を使います

  5. 通常 クライアント Client 抽象化層とだけやりとりをします しかし 抽象化層のオブジェクトと実装オブジェクトを結びつけるのは クライアントの仕事です

擬似コード

この例では Bridge パターンが デバイスとそのリモコンを管理するアプリの一枚岩のコードを分割するのにどう役立つかを説明します Device 系のクラスは実装として機能し Remote 系のクラスは抽象化層です

Bridge パターン例の構造

元のクラス階層は device 機器 と remote リモコン の二つの部分に分割される

基底のリモコン・クラスは 機器オブジェクトへ結びつけるための参照フィールドを宣言しています すべてのリモコンは 機器とのやり取りを一般的な機器インターフェースを介して行い 同じリモコンで複数の種類の機器をサポートできます

リモコン・クラスの開発は 機器クラスと独立して行えます 行うべきことは 新しいリモコンのサブクラスを作成することだけです たとえば 基本的なリモコンには二つのボタンしかありませんが これを拡張して 追加の電池やタッチスクリーンなどの機能を追加できます

クライアント・コードは 好みの種類のリモコンを特定の機器オブジェクトとリモコンのコンストラクターを介して結びつけます

// 抽象化層は、二つのクラス階層の「Control」関係のインターフェースを定義。
// これは、「実装」階層のオブジェクトへの参照を維持し、実際の作業のすべ
// てをこのオブジェクトに委任。
class RemoteControl is
    protected field device: Device
    constructor RemoteControl(device: Device) is
        this.device = device
    method togglePower() is
        if (device.isEnabled()) then
            device.disable()
        else
            device.enable()
    method volumeDown() is
        device.setVolume(device.getVolume() - 10)
    method volumeUp() is
        device.setVolume(device.getVolume() + 10)
    method channelDown() is
        device.setChannel(device.getChannel() - 1)
    method channelUp() is
        device.setChannel(device.getChannel() + 1)


// 抽象化層のクラスは、Device 系クラスとは独立して拡張可能。
class AdvancedRemoteControl extends RemoteControl is
    method mute() is
        device.setVolume(0)


// 「実装」インターフェースは、全具象実装クラスに共通するメソッドを定義。
// 抽象層インターフェースと一致する必要なし。実際のところ、この二つはまっ
// たく異なってよい。典型的には、実装インターフェースは基礎的な操作を宣言
// し、抽象化層は基礎的操作に基づいて高レベルな操作を定義。
interface Device is
    method isEnabled()
    method enable()
    method disable()
    method getVolume()
    method setVolume(percent)
    method getChannel()
    method setChannel(channel)


// どの機器(Device)も同じインターフェースに準拠。
class Tv implements Device is
    // ……

class Radio implements Device is
    // ……


// クライアント・コードのどこかの様子。
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()

radio = new Radio()
remote = new AdvancedRemoteControl(radio)

適応性

たとえば データベースなど 機能にちょっとした違い変種がある場合 一枚岩のコードを分割して組織するために Bridge パターンを使います

クラスが増大するにつれ その動作を理解することが難しくなり 変更に時間がかかるようになります 機能の一つの変種に加えられた変更には クラス全体の変更を要するようになり 誤りや重大な副作用への対処を欠くなどの弊害につながります

Bridgeパターンでは 一枚岩のクラスをいくつかのクラス階層に分割します その後は 独立して各階層のクラスを変更できるようになります この方法により コードの保守が簡素化され 既存のコードが動かなくなるリスクを最小限に抑えられます

クラスをいくつかの直交的 独立した 次元で拡張する必要がある場合 このパターンを使用します

Bridge では 各次元に対して個別のクラス階層を抽出することを勧めます 元のクラスは すべてを自身で行うのではなく それぞれの階層に属するオブジェクトに関連作業を委任します

実行時に実装を切り替える必要がある場合は Bridge を使用してください

これは 必ずしも必要ありませんが Bridge パターンでは 実装オブジェクトを抽象化層内で置き換えることも許されています 新しい値をフィールドに割り当てるのと同じくらい簡単です

ちなみに この最後の項目のため 多くの人々が Bridge を Strategy パターンと混同します 覚えておいていただきたことは パターンはクラスに特定の構造を持たせる以上のものであるということです 意図と解決すべき問題を伝えるためにも使用します

実装方法

  1. クラス内の直交する次元を特定します これらの独立した概念は たとえば抽象化とプラットフォーム OS だったり ドメインとインフラストラクチャーだったり フロントエンドとバックエンドだったり インターフェースと実装だったりします

  2. クライアントが必要とする操作は何であるかを考え 抽象化層の基底クラスで定義します

  3. すべてのプラットフォームで利用可能な操作は何かを決定します 抽象化層が必要とするものを一般的な実装インターフェースで宣言します

  4. サポートするすべてのプラットフォームに対して具体的な実装クラスを作成します すべて実装クラスはプラットフォームの実装インターフェースに従うようにしてください

  5. 抽象化層のクラス内に 実装型の参照フィールドを追加します 抽象化層は このフィールドから参照される実装オブジェクトにほとんどの仕事を移譲します

  6. 高レベルのロジックにいくつかの変種がある場合は 各変種ごとに抽象化層の基底クラスを拡張して より洗練した抽象化層クラスを作成します

  7. クライアント・コードは 抽象化層のクラスのコンストラクターに実装オブジェクトを渡して 互いを関連づけます その後 クライアントは実装のことは忘れ 抽象化層のオブジェクトとだけやりとりをします

長所と短所

  • プラットフォーム非依存のクラスやアプリを作成できる
  • クライアント・コードは高レベルの抽象化層で動作 プラットフォームの詳細に左右されない
  • 新規の抽象化層と実装を互いに独立して導入可
  • 抽象化層では 高レベルのロジックに 実装層では プラットフォームの詳細に集中できる
  • 高度に密着したクラスに本パターンを適用すると コードが余計に複雑になる可能性あり

他のパターンとの関係

  • Bridge は通常 アプリケーションの部分部分を独立して開発できるように 設計当初から使われます 一方 Adapter 既存のアプリケーションに対して利用され 本来は互換性のないクラスとうまく動作させるために使われます

  • Bridge State Strategy と限られた意味合いでは Adapter 非常に似た構造をしています 実際のところ これらの全てのパターンは 合成に基づいており 仕事を他のオブジェクトに委任します しかしながら 違う問題を解決します パターンは 単にコードを特定の方法で構造化するためのレシピではありません パターンが解決する問題に関して 開発者同士がするコミュニケーションの道具でもあります

  • Abstract Factory Bridge と一緒に使用できます この組み合わせは Bridge によって定義された抽象化層のいくつかが特定の実装としか動作しない場合に便利です この場合 Abstract Factory はこれらの関係をカプセル化し クライアント・コードから複雑さを隠すことができます

  • BuilderBridge を組み合わせることができます ディレクター・クラスは抽象化層の役割を果たし ビルダーは実装です

コード例

Bridge を C# で Bridge を C++ で Bridge を Go で Bridge を Java で Bridge を PHP で Bridge を Python で Bridge を Ruby で Bridge を Rust で Bridge を Swift で Bridge を TypeScript で