デザインパターンBridgeについて勉強した

bridgeパターンとは

bridge(ブリッジ)は橋のことです。
機能と実装の橋渡しをします。

クラスの継承をするのには主に2つの理由があります。1つはメソッドを追加すること、もう一つはメソッドを書き換える(オーバライド)ことです。
メソッドを追加することによってできるクラス階層(クラスの親子関係)を機能の階層と呼びます。
オーバライドすることによってできるクラス階層を実装の階層と呼びます。
この機能の階層と実装の階層をごちゃごちゃにクラス階層を作ってしまうとどんどんクラス関係が複雑になってしまいます。
そこで、機能と実装を別々に作ることで全体の見通しや今後の拡張性を上げることができるようになります。

コード例

GreetingImpleインタフェース

public interface GreetingImple {

	String type();

	String morning();

	String noon();

	String night();
}

これは「実装の階層」になるクラスが実装するインタフェースです。
実装されていないので一見「機能の階層」と勘違いしそうですが、このインタフェースを実装したクラスのことを「実装の階層」とします。
実装の階層はこのインタフェースの直下のクラスだけと考えてください。
つまり、クラス1でtypeメソッドを実装した後、クラス1を継承したクラス2でtypeメソッドをオーバライドするようなことはしてはいけません。
typeメソッドの実装を変えたければ新たにGreetingImpleインタフェースを実装した別のクラスとして作るべきです。

Greetingクラス

public class Greeting {

	private GreetingImple imple ;

	public Greeting(GreetingImple imple) {
		this.imple = imple;
	}

	public String type() {
		return imple.type();
	}

	public String morning() {
		return imple.morning();
	}

	public String noon() {
		return imple.noon();
	}

	public String night() {
		return imple.night();
	}
}

「機能の階層」となるクラスです。

コンストラクタでは先ほど説明したGreetingImpleを受け取り、そのほかのメソッドでは全てGreetingImpleに処理を委譲しています。
全てのメソッドで処理を委譲している。つまり実装は全て「実装の階層」であるGreetingImpleに任せているのです。
ぼんやりと「機能の階層」と「実装の階層」の意味が見えてきましたか?

GreetingCatalogクラス

public class GreetingCatalog extends Greeting {

	public GreetingCatalog(GreetingImple imple) {
		super(imple);
	}

	public void greetingCatalog() {
		System.out.println(String.format("%sの挨拶は", type()));
		System.out.println(String.format("朝は「%s」,", morning()));
		System.out.println(String.format("昼は「%s」,", noon()));
		System.out.println(String.format("夜は「%s」です", night()));
	}
}

「機能の階層」側のサブクラスです。
新たにgreetingCatalogメソッドという「機能」を追加しました。
ぱっと見「実装の階層」に対する委譲が見当たらないので、このメソッドは「実装」側ではないかと思うかもしれませんが、ここで呼び出されているtype,morning,noon,nightメソッドは全てGreetingImpleへの移譲をしています。
しかし実際このメソッドが実装よりであることも間違いないのでどちらが正解とも言い切れません。

実装

Humanクラス

public class Human implements GreetingImple {

	private String morning ;
	private String noon;
	private String night;

	public Human(String morning,String noon,String night) {
		this.morning = morning;
		this.noon = noon;
		this.night = night;
	}

	@Override
	public String type() {
		return "人間";
	}

	@Override
	public String morning() {
		return morning;
	}

	@Override
	public String noon() {
		return noon;
	}

	@Override
	public String night() {
		return night;
	}
}

人間の挨拶を返すクラスです。
「実装の階層」ですね。
メソッドは全てインタフェースから実装したものです。
この例では非常に簡単な内容になっていますが実装の階層なのでいくらでも複雑な内容にできます。(読みづらいコードを書いてもいいという意味ではありません)

Gorillaクラス

public class Gorilla implements GreetingImple {

	public Gorilla(String morning,String noon,String night) {

	}

	@Override
	public String type() {
		return "ゴリラ";
	}

	@Override
	public String morning() {
		return "ウホウホ";
	}

	@Override
	public String noon() {
		return "ウホウホ";
	}

	@Override
	public String night() {
		return "ウホウホ";
	}
}

こちらも「実装の階層」です。
コンストラクタに引数はありますが、全く使っていませんね。
typeメソッド以外では全てウホウホ言っているだけです。

public class Main {

	public Main() {
		GreetingCatalog human_catalog = new GreetingCatalog(new Human("おはよう","こんにちは","こんばんは"));
		human_catalog.greetingCatalog();

		System.out.println("================");
		GreetingCatalog gorilla_catalog = new GreetingCatalog(new Gorilla("おはよう","こんにちは","こんばんは"));
		gorilla_catalog.greetingCatalog();
	}

	public static void main(String[] args) {
		new Main();
	}
}

それぞれのインスタンスを生成し、引数には朝昼夜の挨拶を渡しています。
greetingCatalogメソッドを呼ぶことで、人とゴリラそれぞれの挨拶について説明を始めます。

特徴

「機能の階層」を変更することなく人間とゴリラの2種類の実装を実現することができました。
今後新たに別の機能を追加するときにも、機能の階層を増やすだけで済みます。
機能と実装を分けなかった場合を想像してみましょう。

+Greeting
  +HumanGreeting
    +HumanGreetingCatalog
  +GorillaGreeting
    +GorillaGreetingCatalog

ここにアメリカ人を追加した場合、追加したいのはアメリカ人1つなのに2つのクラスを追加しないと全体の統一ができなくなります。

+Greeting
  +HumanGreeting
    +HumanGreetingCatalog
  +GorillaGreeting
    +GorillaGreetingCatalog
  +AmericanGreeting
    +AmericanGreetingCatalog

さらに朝昼夜の挨拶をランダムに出力するメソッドを追加してみましょう。(実用性があるかは目を瞑ってください)

+Greeting
  +HumanGreeting
    +HumanGreetingCatalog
      +HumanGreetingRandom
  +GorillaGreeting
    +GorillaGreetingCatalog
      +GorillaGreetingRandom
  +AmericanGreeting
    +AmericanGreetingCatalog
      +AmericanGreetingRandom

こうなるとどれがどのクラスか分かりにくいですし、HumanGreetingRandomクラスとAmericanGreetingRandomクラスは全く別クラスなので、同じメソッドを持っているのにコードの共通化ができないなどが発生してきます。
中身は全く同じrandom(HumanGreetingRandom random)メソッドとrandom(GorillaGreetingRandom random)メソッド、random(AmericanGreetingRandom random)メソッドを作らなくてはいけないなんて面倒ですし、あとで修正するときにも間違いや変更もれがありそうですよね。

ではこれをブリッジパターンに書き換えてみます。

+Greeting
  +GreetingCatalog
    +GreetingRandom
+GreetingImple
  +Human
  +Gorilla
  +American

ずいぶんシンプルになりましたね。
これならアメリカ人だけの処理をしたい時もrandom(GreetingRandom random)メソッドを作るときも困りません。

まとめ

今回の例では実装の階層もかなり単純な内容になっています。
実用的に考えると、例えばダイアログを表示する実装、ファイル出力をする実装、メールを送る実装など考えられます。

具体的な部分とそれの使い方でクラスを分けるという考え方はほとんどのデザインパターンに言えることです。