Bridge
機能の階層と実装の改装を分ける
概要
継承よりコンポジションがいいよねっていうパターン
extendsによる複数概念とimplementsによる複数概念の掛け算的橋渡し
マンガでわかる Bridge
マンガでわかる Bridge #デザインパターン - Qiita
でざぱたんで覚える Bridge
ちびキャラは「ブリッジたん」。奇策よりも「小さな命令の手続を統一し、任せられる人材を選ぶ」ことで勝つ指揮官。命令する側(Abstraction)と実行する側(Implementor)を切り離し、部下の差し替えだけで無限の作戦を組む——継承の乱用でコピペ地獄に陥る代わりに、振る舞いを委譲へ逃がすBridgeの分割統治を軍隊で描く。同じ条件のif文が繰り返し現れたら適用を検討、という実践指針も。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.056〜)
メリット(用途)
継承には2種類の意味がある
- 機能のクラス階層: サブクラスに新しい機能を拡張追加する
- 実装のクラス階層: サブクラスに抽象メソッドの内容をオーバーライドする
継承は1段階ずつ行う都合、サブクラスを作ったとしてもそれがどちらの意味の文脈なのか判別しにくい。 そのサブクラスには機能を追加して抽象メソッドを実装して、をどっちもしないといけないのか
継承におけるこれら2種類の意味を分離するのが目的
- 機能の追加は、機能のクラス階層に行えば良い
- 実装のクラス階層は修正する必要なし
- 今追加した機能は全ての実装で利用できる
登場人物
- 抽象化(Abstraction): 「機能のクラス階層」の最上位クラス
- 改善した抽象化(RefinedAbstraction): 抽象化のサブクラス。新しい機能の追加
- 実装者(Implementor): 「実装のクラス階層」の最上位クラス。抽象メソッドしか持たない
- 具体的な実装者(ConcreteImplementor): 実装者を実装する
クラス図
このサイトの実装(Sample、ディスプレイの例)での対応関係:
classDiagram
class Display {
-impl DisplayImpl
+open()
+print()
+close()
+display()
}
class CountDisplay {
+multiDisplay(times)
}
class DisplayImpl {
<<abstract>>
+rawOpen()
+rawPrint()
+rawClose()
}
class StringDisplayImpl {
-string String
-width int
+rawOpen()
+rawPrint()
+rawClose()
}
Display <|-- CountDisplay
DisplayImpl <|-- StringDisplayImpl
Display o-- DisplayImpl
やり方
- 抽象化に実装者をプロパティで持たせる(委譲)
- 新しい機能を追加したかったら、改善した抽象化に持たせる
- 新しい実装を作成したかったら、具体的な実装者を新たに持たせる
ユースケース
ファイル形式と取引形式
たとえば、仕入れ先リストや顧客リストといった、取引先リストの派生モデルに関係するもの (What) と、HTML で表示したいか CSV でダウンロードしたいかといった、出力機能の派生モデルに関係するもの (How) を分ける。で、What が How を持つ形で組み合わさったインスタンスになる感じです。流行りの言葉でいえば、ドメインモデルとインフラストラクチャの組み合わせがユースケースだよって感じかな。
- 仕入先か
- 得意先か
Whatの軸(実装のクラス階層)と
- CSV出力か
- HTML表示か
Howの軸(機能のクラス改装)を混ぜる
2*2=4のマトリクスを生成
マルチOS
- Windows
- Mac
- Linux
といったOS依存の実装は「実装のクラス階層」に行う 何か機能を実装しても、それは
Java
public class StringDisplayImpl extends DisplayImpl {
private String string; // 表示するべき文字列
private int width; // バイト単位で計算した文字列の「幅」
public StringDisplayImpl(String string) { //コンストラクタで渡された文字列stringを、
this.string = string; //フィールドに記憶しておく。
this.width = string.getBytes().length; //それからバイト単位の幅もフィールドに記憶しておいて、後で使う。
}
public void rawOpen() {
printLine();
}
public void rawPrint() {
System.out.println("|" + string + "|"); // 前後に"|"をつけて表示
}
public void rawClose() {
printLine();
}
private void printLine() {
System.out.print("+"); // 枠の角を表現する"+"マークを表示する。
for (int i = 0; i < width; i++) { // width個の"-"を表示して、
System.out.print("-"); // 枠線として用いる。
}
System.out.println("+"); // 枠の角を表現する"+"マークを表示する。
}
}
public abstract class DisplayImpl {
public abstract void rawOpen();
public abstract void rawPrint();
public abstract void rawClose();
}
public class Display {
private DisplayImpl impl;
public Display(DisplayImpl impl) {
this.impl = impl;
}
public void open() {
impl.rawOpen();
}
public void print() {
impl.rawPrint();
}
public void close() {
impl.rawClose();
}
public final void display() {
open();
print();
close();
}
}
public class Main {
public static void main(String[] args) {
Display d1 = new Display(new StringDisplayImpl("Hello, Japan."));
Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World."));
CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe."));
d1.display();
d2.display();
d3.display();
d3.multiDisplay(5);
}
}
public class CountDisplay extends Display {
public CountDisplay(DisplayImpl impl) {
super(impl);
}
public void multiDisplay(int times) { // times回繰り返して表示する
open();
for (int i = 0; i < times; i++) {
print();
}
close();
}
}
Go
機能側(Display→CountDisplay)はGoに継承が無いので構造体埋め込みで表現。埋め込みフィールド名Displayとメソッド名が衝突するため、Java版のdisplay()はShow()にリネームしている。実装側(DisplayImpl)はinterfaceで橋渡し。
実行: go run ./GoF/patterns/Bridge/go
// 実行方法は main.go を参照(go run ./GoF/patterns/Bridge/go)。
package main
// DisplayImpl は「実装」側の階層のトップ。Display側(機能の階層)から
// 委譲で呼び出される、生の描画命令だけを規定する。
//
// Go の interface は暗黙実装(implements を書かない)。
// ここがJava版のabstract class DisplayImplとの一番わかりやすい違い
// (Strategy版Goの Strategy interface と同じ発想)。
type DisplayImpl interface {
RawOpen()
RawPrint()
RawClose()
}
// 実行方法は main.go を参照(go run ./GoF/patterns/Bridge/go)。
package main
import (
"fmt"
"strings"
)
// StringDisplayImpl は DisplayImpl の ConcreteImplementor。
// 文字列を"|"で前後を囲んだ枠の中に表示する、実装側の実体。
type StringDisplayImpl struct {
string string
width int
}
// NewStringDisplayImpl はコンストラクタ相当。
// 渡された文字列と、そのバイト単位の「幅」をフィールドに記憶しておく。
// Go の string は元々UTF-8のバイト列なので、len(s) が Java版の
// string.getBytes().length と同じくバイト長になる。
func NewStringDisplayImpl(s string) *StringDisplayImpl {
return &StringDisplayImpl{string: s, width: len(s)}
}
func (d *StringDisplayImpl) RawOpen() {
d.printLine()
}
func (d *StringDisplayImpl) RawPrint() {
fmt.Printf("|%s|\n", d.string) // 前後に"|"をつけて表示
}
func (d *StringDisplayImpl) RawClose() {
d.printLine()
}
// printLine は幅ぶんの"-"を"+"で挟んで枠線を表示する。
// Java版は1文字ずつループで表示するが、strings.Repeatで等価にまとめる。
func (d *StringDisplayImpl) printLine() {
fmt.Println("+" + strings.Repeat("-", d.width) + "+")
}
// 実行方法は main.go を参照(go run ./GoF/patterns/Bridge/go)。
package main
// Display は「機能」側の階層のトップ(Abstraction)。
// DisplayImplを持つ(has-a)ことで実装側と橋渡しする。継承ではなく委譲で
// 実装を差し替えられるのがBridgeパターンの肝。
// CountDisplayがこれを構造体埋め込みで拡張して「機能の階層」を伸ばす一方、
// DisplayImplの実装がどう差し替わっても、Display側のコードは一切変わらない。
type Display struct {
impl DisplayImpl
}
// NewDisplay はコンストラクタ相当。橋渡しするDisplayImplを受け取る。
func NewDisplay(impl DisplayImpl) *Display {
return &Display{impl: impl}
}
func (d *Display) Open() {
d.impl.RawOpen()
}
func (d *Display) Print() {
d.impl.RawPrint()
}
func (d *Display) Close() {
d.impl.RawClose()
}
// Show は Open→Print→Close をこの順に呼ぶ(Java版の display() 相当)。
// Java版は final で上書き禁止だが、CountDisplayはGoの構造体埋め込みで
// Displayを拡張するだけでこのメソッド自体は再定義しない(継承によるオーバーライドという
// 概念がGoに無いので、実質的に同じ制約になる)。
//
// 名前をdisplayではなくShowにしたのは、CountDisplay{ *Display }のように埋め込むと
// フィールド名も"Display"になり、メソッド名Displayとぶつかって曖昧になるため
// (embeddingを使う都合上の命名で、Java版との対応はコメントで補う)。
func (d *Display) Show() {
d.Open()
d.Print()
d.Close()
}
// 実行方法は main.go を参照(go run ./GoF/patterns/Bridge/go)。
package main
// CountDisplay は Display の RefinedAbstraction。
// 「機能の階層」を構造体埋め込み(Javaのextends相当)で拡張し、
// 繰り返し表示という新しい機能を追加する。DisplayImplには一切触れない —
// Bridgeパターンで「機能の拡張」と「実装の拡張」が互いに独立していることの実例になる。
type CountDisplay struct {
*Display
}
// NewCountDisplay はコンストラクタ相当。Displayと同じくDisplayImplを橋渡しする
// (Java版の super(impl) 呼び出しに相当)。
func NewCountDisplay(impl DisplayImpl) *CountDisplay {
return &CountDisplay{Display: NewDisplay(impl)}
}
// MultiDisplay は times回繰り返して表示する。
// Open/Print/Closeは埋め込んだ*Displayのメソッドがそのまま昇格(promotion)して使える。
func (c *CountDisplay) MultiDisplay(times int) {
c.Open()
for i := 0; i < times; i++ {
c.Print()
}
c.Close()
}
package main
// 実行:
//
// go run ./GoF/patterns/Bridge/go
//
// Java版Main.javaと同じ3パターンを再現する。
//
// Java版は `Display d2 = new CountDisplay(...)` のように、CountDisplayのインスタンスを
// 親クラスDisplay型の変数へ暗黙にアップキャストできる(d2からはmultiDisplayが見えなくなる)。
// Goの構造体埋め込みには暗黙のアップキャストが無いため、埋め込んだ*Displayフィールドを
// cd2.Display として直接取り出すことで、「Displayとしてしか見ない」構えを表現する。
func main() {
d1 := NewDisplay(NewStringDisplayImpl("Hello, Japan."))
cd2 := NewCountDisplay(NewStringDisplayImpl("Hello, World."))
d2 := cd2.Display // CountDisplayをDisplayとしてのみ扱う(MultiDisplayは呼べない)
d3 := NewCountDisplay(NewStringDisplayImpl("Hello, Universe."))
d1.Show()
d2.Show()
d3.Show()
d3.MultiDisplay(5)
}
PHP
<?php
/**
*
* 支持者と作業者を分割統治して柔軟な組み合わせを実現
* 多少の効率をロスしてでも全ての手続きを一貫した手順にまとめる
*
*
* 「異なる事柄を任せる人材」を選出
* 「小さな命令の手続きを統一」
* 「小さなことを任せる人材もち、自分はそれに強く気にかけない」
*/
/**
* Abstraction: inplementorを保持しImplementorを使った
* 基本的な機能が実装
* RefinedAbstraction: Abstractionの機能追加
* Implementor : Abstractionが使用するインタフェースを規定
* ConcreteImplementor : 具体的なImplementor
*/
// Abstraction
class Bridge
{
private $impl;
public function __construct($impl)
{
if (!($impl instanceof Implement)) {
throw new Exception("それは無理な相談");
}
$this->impl = $impl;
}
/**
* implementorのdoMethodを使う
* implementorのdoMethodを切り替えるだけで実装が切りか実装が切り替わる
*
*
* @return void
*/
public function doMethod()
{
return $this->impl->doMethod();
}
}
// RefindedAbstraction その1
class BridgeSub1 extends Bridge
{
public function doMethod()
{
return "Sub1 | " . parent::doMethod();
}
}
// RefinedAbstraction その2
class BridgeSub2 extends Bridge
{
public function doMethod()
{
// 言い換えれば
// 自分の処理+親からの伝言が実装できる
// 親が何をdoMethodするのかはImplementによる
return "Sub2 | " . parent::doMethod();
}
}
// implementor
interface Implement
{
public function doMethod();
}
// concrete implementor
class ImplementSample1 implements Implement
{
public function doMethod()
{
return "実装1 ";
}
}
class ImplementSample2 implements Implement
{
public function doMethod()
{
return "実装2 ";
}
}
function say($i)
{
print "$i <br>";
}
// 以下同じdoMethodを使っているが
// RefinedAbstractionで機能が拡張され
// concreteImplementを差し替えることにより
// 別の実装処理がされる
$parent = new Bridge(new ImplementSample2());
say($parent->doMethod());
$subclass1 = new BridgeSub1(new ImplementSample1);
say($subclass1->doMethod());
$subclass1_2 = new BridgeSub1(new ImplementSample2);
say($subclass1_2->doMethod());
$subclass2 = new BridgeSub2(new ImplementSample1);
say($subclass2->doMethod());
$subclass2_2 = new BridgeSub2(new ImplementSample2);
say($subclass2_2->doMethod());
TypeScript
TypeScriptはJava同様のクラス継承(class CountDisplay extends Display)がそのまま使える。Display d2 = new CountDisplay(...)のようなアップキャストによる可視メソッド制限もJava版通りに再現できる。
実行: npx tsx GoF/patterns/Bridge/typescript/main.ts
// 実行方法は main.ts を参照(npx tsx GoF/patterns/Bridge/typescript/main.ts)。
// DisplayImpl: 「実装」側の階層のトップ。Display側(機能の階層)から
// 委譲で呼び出される、生の描画命令だけを規定する。
// TypeScriptのinterfaceはJavaのinterfaceとほぼ同じ感覚で書ける
// (Builder版TSでの記法を踏襲。ConcreteImplementor側で`implements`を明示する)。
// Java版はDisplayImplがabstract classだが、抽象メソッドしか持たないのでinterfaceで表せる。
export interface DisplayImpl {
rawOpen(): void;
rawPrint(): void;
rawClose(): void;
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/Bridge/typescript/main.ts)。
import type { DisplayImpl } from "./display_impl";
// StringDisplayImpl: DisplayImplのConcreteImplementor。
// 文字列を"|"で前後を囲んだ枠の中に表示する、実装側の実体。
export class StringDisplayImpl implements DisplayImpl {
private readonly width: number;
constructor(private readonly str: string) {
// Java版はstring.getBytes().length、Go版はlen(s)でどちらもバイト長。
// TypeScriptのstring.lengthはUTF-16コード単位数でバイト長と一致しないため、
// Node.jsのBufferで明示的にバイト長を測って揃える(Prototype版TSと同じ対処)。
this.width = Buffer.byteLength(str);
}
rawOpen(): void {
this.printLine();
}
rawPrint(): void {
console.log(`|${this.str}|`); // 前後に"|"をつけて表示
}
rawClose(): void {
this.printLine();
}
private printLine(): void {
console.log("+" + "-".repeat(this.width) + "+");
}
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/Bridge/typescript/main.ts)。
import type { DisplayImpl } from "./display_impl";
// Display: 「機能」側の階層のトップ(Abstraction)。
// DisplayImplを持つ(has-a)ことで実装側と橋渡しする。継承ではなく委譲で
// 実装を差し替えられるのがBridgeパターンの肝。
// CountDisplayがこれをextendsして「機能の階層」を伸ばす一方、
// DisplayImplの実装がどう差し替わっても、Display側のコードは一切変わらない。
export class Display {
constructor(private readonly impl: DisplayImpl) {}
open(): void {
this.impl.rawOpen();
}
print(): void {
this.impl.rawPrint();
}
close(): void {
this.impl.rawClose();
}
// display: open→print→closeをこの順に呼ぶ。
// Java版はfinalで上書き禁止だが、TypeScriptにfinalメソッド修飾子は無いため、
// 「これはテンプレートなので上書きしないでね」という意図はコメントで伝えるにとどめる
// (FactoryMethod版TSのcreate()と同じ扱い)。
display(): void {
this.open();
this.print();
this.close();
}
}
// 実行方法は main.ts を参照(npx tsx GoF/patterns/Bridge/typescript/main.ts)。
import type { DisplayImpl } from "./display_impl";
import { Display } from "./display";
// CountDisplay: DisplayのRefinedAbstraction。
// 「機能の階層」をextendsで拡張し、繰り返し表示という新しい機能を追加する。
// DisplayImplには一切触れない — Bridgeパターンで「機能の拡張」と「実装の拡張」が
// 互いに独立していることの実例になる。
export class CountDisplay extends Display {
constructor(impl: DisplayImpl) {
super(impl);
}
// times回繰り返して表示する
multiDisplay(times: number): void {
this.open();
for (let i = 0; i < times; i++) {
this.print();
}
this.close();
}
}
// Bridge パターン: 文字列表示 (Java版と同じ題材)
//
// 実行:
// npx tsx main.ts
// (もしくはリポジトリルートから npx tsx GoF/patterns/Bridge/typescript/main.ts)
//
// 「機能の階層」Display -> CountDisplayと「実装の階層」DisplayImpl -> StringDisplayImplが
// 互いに独立に拡張できることを確認する (Java版Main.javaと同じ3パターン)。
//
// Java版は `Display d2 = new CountDisplay(...)` のように、CountDisplayのインスタンスを
// 親クラスDisplay型の変数へ暗黙にアップキャストできる(d2からはmultiDisplayが見えなくなる)。
// TypeScriptもクラスの継承をそのまま持つので、Go版のような埋め込みの回避策は不要で
// Java版の書き方をほぼそのまま再現できる。
import { CountDisplay } from "./count_display";
import { Display } from "./display";
import { StringDisplayImpl } from "./string_display_impl";
function main(): void {
const d1: Display = new Display(new StringDisplayImpl("Hello, Japan."));
const d2: Display = new CountDisplay(new StringDisplayImpl("Hello, World."));
const d3: CountDisplay = new CountDisplay(new StringDisplayImpl("Hello, Universe."));
d1.display();
d2.display();
d3.display();
d3.multiDisplay(5);
}
main();
Python
TypeScript同様、クラス継承(class CountDisplay(Display))でJava版をほぼそのまま移植できる。
実行: python3 GoF/patterns/Bridge/python/main.py
"""実行方法は main.py を参照(python3 GoF/patterns/Bridge/python/main.py)。
DisplayImpl: 「実装」側の階層のトップ。
Display側(機能の階層)から委譲で呼び出される、生の描画命令だけを規定する。
PythonにはJavaのinterfaceに相当する言語機能は無いため、Strategy/Builder版と同じく
抽象基底クラス(ABC, abcモジュール)で表す。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class DisplayImpl(ABC):
@abstractmethod
def raw_open(self) -> None: ...
@abstractmethod
def raw_print(self) -> None: ...
@abstractmethod
def raw_close(self) -> None: ...
"""実行方法は main.py を参照(python3 GoF/patterns/Bridge/python/main.py)。
StringDisplayImpl: DisplayImplのConcreteImplementor。
文字列を"|"で前後を囲んだ枠の中に表示する、実装側の実体。
"""
from __future__ import annotations
from display_impl import DisplayImpl
class StringDisplayImpl(DisplayImpl):
def __init__(self, string: str) -> None:
self._string = string
# Java版はstring.getBytes().lengthでバイト単位の幅を測る。
# Go版はlen(s)がそのままバイト長になるが、Pythonのstrはコードポイント列なので
# 明示的にUTF-8エンコードしてバイト長を測る(Prototype版Pythonと同じ対処)。
self._width = len(string.encode("utf-8"))
def raw_open(self) -> None:
self._print_line()
def raw_print(self) -> None:
print(f"|{self._string}|") # 前後に"|"をつけて表示
def raw_close(self) -> None:
self._print_line()
def _print_line(self) -> None:
print("+" + "-" * self._width + "+")
"""実行方法は main.py を参照(python3 GoF/patterns/Bridge/python/main.py)。
Display: 「機能」側の階層のトップ(Abstraction)。
DisplayImplを持つ(has-a)ことで実装側と橋渡しする。継承ではなく委譲で
実装を差し替えられるのがBridgeパターンの肝。CountDisplayがこれを継承して
「機能の階層」を伸ばす一方、DisplayImplの実装がどう差し替わっても、
Display側のコードは一切変わらない。
"""
from __future__ import annotations
from display_impl import DisplayImpl
class Display:
def __init__(self, impl: DisplayImpl) -> None:
self._impl = impl
def open(self) -> None:
self._impl.raw_open()
def print(self) -> None:
self._impl.raw_print()
def close(self) -> None:
self._impl.raw_close()
def display(self) -> None:
"""open→print→closeをこの順に呼ぶ。
Java版はfinalで上書き禁止だが、Pythonにfinalを強制する機構は無いため
(typing.finalは型チェッカーへの合図に過ぎず実行時には強制されない)、
「これはテンプレートなので上書きしないでね」という意図はコメントで伝えるにとどめる
(FactoryMethod版Pythonのcreateと同じ扱い)。
"""
self.open()
self.print()
self.close()
"""実行方法は main.py を参照(python3 GoF/patterns/Bridge/python/main.py)。
CountDisplay: DisplayのRefinedAbstraction。
「機能の階層」を継承で拡張し、繰り返し表示という新しい機能を追加する。
DisplayImplには一切触れない — Bridgeパターンで「機能の拡張」と「実装の拡張」が
互いに独立していることの実例になる。
"""
from __future__ import annotations
from display import Display
from display_impl import DisplayImpl
class CountDisplay(Display):
def __init__(self, impl: DisplayImpl) -> None:
super().__init__(impl)
def multi_display(self, times: int) -> None:
"""times回繰り返して表示する"""
self.open()
for _ in range(times):
self.print()
self.close()
"""Bridge パターン: 文字列表示 (Java版と同じ題材)
実行: python3 main.py
(もしくはリポジトリルートから python3 GoF/patterns/Bridge/python/main.py)
「機能の階層」Display -> CountDisplayと「実装の階層」DisplayImpl -> StringDisplayImplが
互いに独立に拡張できることを確認する (Java版Main.javaと同じ3パターン)。
Java版は `Display d2 = new CountDisplay(...)` のように、CountDisplayのインスタンスを
親クラスDisplay型の変数へ暗黙にアップキャストできる(d2からはmulti_displayが見えなくなる)。
Pythonも通常のクラス継承を持つので、Go版のような埋め込みの回避策は不要で
Java版の書き方をほぼそのまま再現できる(型注釈は型チェッカー向けで実行時には強制されない)。
"""
from __future__ import annotations
from count_display import CountDisplay
from display import Display
from string_display_impl import StringDisplayImpl
def main() -> None:
d1: Display = Display(StringDisplayImpl("Hello, Japan."))
d2: Display = CountDisplay(StringDisplayImpl("Hello, World."))
d3: CountDisplay = CountDisplay(StringDisplayImpl("Hello, Universe."))
d1.display()
d2.display()
d3.display()
d3.multi_display(5)
if __name__ == "__main__":
main()