Java : Bridge パターン (図解/デザインパターン)
Bridge パターンとは、GoF によって定義されたデザインパターンの1つです。
複雑になる継承関係を、委譲を使ってシンプルにする設計です。
本記事では、Bridge パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Bridge パターンとは、
- 複雑になる継承関係を、委譲 を使ってシンプルにする
という設計です。
Bridge は、日本語的に発音すると「ブリッジ」となります。
意味は「橋」ですね。
【Bridge パターンのイメージ】
ベースとなる図形クラスがあり、そのサブクラスとして
- 長方形
- 円形
があります。
さらに、図形の色として
- 赤色
- 緑色
があるとしましょう。
今回、色についても継承で実現しています。
これは少し過剰だとは思いますが、あくまで例ということでご容赦ください…
さて、しばらくして「青色」を追加することになりました。
クラス図は次のようになります。
色を1つ追加するだけで、新しいクラスが 2つ 増えました。
さらに、図形として「三角形」を追加してみましょう。
今度は、図形を1つ追加するだけで新しいクラスが 4つ 増えました。
これでは、この図形クラスの 拡張性は悪い と言ってよいでしょう。
このような問題を解決するのが Bridge パターンです。
クラス数も減り、継承関係もすっきりしました。
継承関係は「図形」と「色」の2つに分離しています。
そして、その2つをつなぐ Bridge(橋) が 委譲 というわけですね。
構造は Strategy パターン とよく似ています。
… というかほぼ同じです。
個人的には、Bridge パターンと Strategy パターンはわざわざ別パターンにしなくてもよいのではないかな … とも思います。
厳密には違いはあるのでしょう。Bridge は 構造 に主眼を置き、Strategy は 振る舞い に主眼を置きます。
ただ、その線引きは難しいのかな、と思います。
Bridge パターンがいまいち理解できない場合は、Strategy パターンを先に学んでみるのもよいかもしれません。
関連記事:
クラス図とシーケンス図
上の図は、Bridge パターンの一般的なクラス図とシーケンス図です。
クラス関係はとてもシンプルですね。
それでは、このクラス図をそのままコードにしてみましょう。
まずは Implementor インタフェースと、その実装である ConcreteImplementor クラスです。
public interface Implementor {
void implementation();
}
public class ConcreteImplementor implements Implementor {
@Override
public void implementation() {
System.out.println("Implementation!");
}
}
次に Abstraction クラスです。
Implementor オブジェクトをフィールドに持つのがポイントですね。
public class Abstraction {
private final Implementor impl;
public Abstraction(Implementor impl) {
this.impl = impl;
}
public void function() {
System.out.print("function -> ");
impl.implementation();
}
}
最後に、Abstraction のサブクラスである RefinedAbstraction です。
もし RefinedAbstraction が必要なければ作らなくても問題ありません。
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor impl) {
super(impl);
}
public void refinedFunction() {
System.out.println("refinedFunction!");
}
}
それでは、これらのクラスを使ってみましょう。
final var abstraction = new RefinedAbstraction(new ConcreteImplementor());
abstraction.function();
abstraction.refinedFunction();
// 結果
// ↓
//function -> Implementation!
//refinedFunction!
結果も問題なしですね。
具体的な例
Bridge パターンを使わないケース
もう少し具体的な例も見てみましょう。
図形を描画する Shape クラスを考えてみます。
Shape クラスのサブクラスとして、具体的な図形である
- Rect クラス : 長方形
- Circle クラス : 円形
があります。
さらに、図形の色を表現するために
- RedRect, RedCircle クラス : 赤色
- GreenRect, GreenCircle クラス : 緑色
があります。
(継承で色を表現するのは少しやりすぎ感はありますが、あくまで例ということで…)
それでは Shape クラスから見てみましょう。
Shape クラスは draw メソッドで図形を描画します。
本来はキャンバスなどに描画したいのですが、今回は単純にテキストで出力します。
public class Shape {
public void draw() {
System.out.println("-- 図形描画 --");
}
}
次に、サブクラスとして
- Rect クラス : 長方形
- Circle クラス : 円形
を作ります。
public class Rect extends Shape {
@Override
public void draw() {
super.draw();
System.out.print("長方形");
}
}
public class Circle extends Shape {
@Override
public void draw() {
super.draw();
System.out.print("円形");
}
}
さらに、色を表現するサブクラスを作ります。
public class RedRect extends Rect {
@Override
public void draw() {
super.draw();
System.out.println(" : 赤色");
}
}
public class GreenRect extends Rect {
@Override
public void draw() {
super.draw();
System.out.println(" : 緑色");
}
}
public class RedCircle extends Circle {
@Override
public void draw() {
super.draw();
System.out.println(" : 赤色");
}
}
public class GreenCircle extends Circle {
@Override
public void draw() {
super.draw();
System.out.println(" : 緑色");
}
}
それでは、これらのクラスを使ってみましょう。
final var redRect = new RedRect();
redRect.draw();
// 結果
// ↓
//-- 図形描画 --
//長方形 : 赤色
final var greenRect = new GreenRect();
greenRect.draw();
// 結果
// ↓
//-- 図形描画 --
//長方形 : 緑色
final var redCircle = new RedCircle();
redCircle.draw();
// 結果
// ↓
//-- 図形描画 --
//円形 : 赤色
final var greenCircle = new GreenCircle();
greenCircle.draw();
// 結果
// ↓
//-- 図形描画 --
//円形 : 緑色
結果は問題なしですね。
ただし、このクラス構成はとても 拡張性が悪い です。
概要 のところでも述べましたが、例えば …
- 新しい図形を追加 (三角形など)
- 新しい色を追加 (青色など)
と拡張していくと、すごい勢いでクラスが増えていきます。
このクラス構成の問題は、
- 図形 : 長方形、円形
- 色 : 赤色、緑色
という 2つの構造 を、無理に 1つの継承関係 にまとめてしまったのが原因です。
ノート
- 多重継承が許されているプログラミング言語であれば、もう少しスマートな構成になるかもしれません。
ただし、その場合は多重継承特有の問題に注意する必要があります。 - Java 言語では多重継承が許されていないので、ここでは割愛します。
Bridge パターンを使うケース
それでは、Bridge パターンを使って、先ほどの構成 を改善してみましょう。
クラス構成を、
- Shape (図形) : Rect、Circle
- Color (色) : Red、Green
の2つに分離します。
そして、その2つをつなぐ Bridge(橋) が 委譲 ですね。
まずは Color インタフェースとその実装を見てみましょう。
public interface Color {
void drawColor();
}
public class Red implements Color {
@Override
public void drawColor() {
System.out.println("赤色");
}
}
public class Green implements Color {
@Override
public void drawColor() {
System.out.println("緑色");
}
}
次に、Shape クラスとそのサブクラスです。
Shape クラスのフィールドに、Color オブジェクトを持つのがポイントです。
public class Shape {
protected final Color color;
public Shape(Color color) {
this.color = color;
}
public void draw() {
System.out.println("-- 図形描画 --");
}
}
public class Rect extends Shape {
public Rect(Color color) {
super(color);
}
@Override
public void draw() {
super.draw();
System.out.print("長方形 : ");
color.drawColor();
}
}
public class Circle extends Shape {
public Circle(Color color) {
super(color);
}
@Override
public void draw() {
super.draw();
System.out.print("円形 : ");
color.drawColor();
}
}
それでは、これらのクラスを実行してみましょう。
final var redRect = new Rect(new Red());
redRect.draw();
// 結果
// ↓
//-- 図形描画 --
//長方形 : 赤色
final var greenCircle = new Circle(new Green());
greenCircle.draw();
// 結果
// ↓
//-- 図形描画 --
//円形 : 緑色
結果も問題なしですね。
拡張性についても、例えば …
- 新しい図形を追加 (三角形など)
- 新しい色を追加 (青色など)
としても、それぞれクラスを1つずつ追加するだけで済みます。
まとめ
Bridge パターンとは、
- 複雑になる継承関係を、委譲 を使ってシンプルにする
という設計です。
Strategy パターン ともよく似ているので、そちらも合わせて学習することをおすすめします。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン