読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:デザインパターン

[学習記録] デザインパターン

第9章:Bridgeパターン

概要
  • 2つの場所を結びつける役割を果たす
    • 「機能のクラス階層」と「実装のクラス階層」の橋渡し
  • 新しい機能を追加したいとき
    • クラスSomethingに新しい機能(具体的には、新しいメソッドなど)を追加したいとき
    • SomethingクラスのサブクラスとしてSomethingGoodクラスを作成
    • 更に新たな機能をつけるため、SomethingGoodのサブクラスとしてSomethingBetterクラスを…
    • 「機能のクラス階層」、スーパクラスは基本的な機能を持ち、サブクラスで機能を追加する
    • ※一般に、クラス階層はあまり深くしすぎない方がよいと言われている
  • 新しい実装を追加したいとき
    • 抽象クラスAbstractClassの実装を行うとき
    • AbstractClassの抽象メソッドを実装したサブクラスConcreteClassを作成
    • 別の実装をしたければ、AbstractClassのサブクラスAnotherConcreteClassを作成
    • 「実装のクラス階層」、スーパクラスは抽象メソッドによりAPIを規定し、サブクラスで実装する
  • クラス階層の混在と分離
    • サブクラスを作るとき、機能を追加しようとしているのか?実装を行おうとしているのか?
    • クラス階層が1つだと、機能と実装のクラス階層が混在して複雑になってしまう
    • そこで、機能と実装のクラス階層を2つの独立したクラス階層に分ける
    • 単に分けただけではバラバラなので、2つのクラス階層の間に橋渡しが必要
サンプルプログラム
階層 名前 解説
機能 Display 表示するクラス
機能 RandomDisplay ランダム回数表示する機能を追加したクラス
実装 DisplayImpl 表示するクラス
実装 NumberDisplayImpl 数字を使って表示するクラス
Main テスト用クラス
  • Display
    • 抽象的な「何か表示するもの」
    • 機能のクラス階層の最上位にあるクラス
    • implフィールドに実装を表すインスタンスを格納
    • コンストラクタで実装を表すクラスのインスタンスを受け取り、implフィールドに格納
    • implは今後の処理で使われる=このフィールドが2つのクラス階層の「橋」になる
public class Display {
	private DisplayImpl impl;
	public Display(DisplayImpl impl) {
		this.impl = impl;
	}
	public void before() {
		impl.rawBefore();
	}
	public void print() {
		impl.rawPrint();
	}
	public void after() {
		impl.rawAfter();
	}
	public final void display() {
		before();
		print();
		after();
	}
}
  • RandomDisplay
    • 機能のクラス階層
    • Displayクラスにランダム回数表示する機能を追加したもの
public class RandomDisplay extends Display {
	public RandomDisplay(DisplayImpl impl) {
		super(impl);
	}
	public void randomDisplay() {
		before();
		int times = (int) (Math.random() * 10);
		for (int i = 0; i < times; i++) {
			print();
		}
		after();
	}
}
  • DisplayImpl
    • 実装のクラス階層の最上位
    • クラス自体は抽象クラスで、Displayクラスのbefore, print, afterに対応するメソッドを持つ
public abstract class DisplayImpl {
	public abstract void rawBefore();
	public abstract void rawPrint();
	public abstract void rawAfter();
}
  • NumberDisplayImpl
    • 本当の実装、数字を表示するkルアス
public class NumberDisplayImpl extends DisplayImpl {
	private int num;
	private int width;
	
	public NumberDisplayImpl(int num) {
		this.num = num;
		this.width = String.valueOf(num).getBytes().length;
	}
	
	@Override
	public void rawBefore() {
		printBar();
	}
	@Override
	public void rawPrint() {
		System.out.println("|" + num + "|");
	}
	@Override
	public void rawAfter() {
		printBar();
	}
	private void printBar() {
		System.out.print("*");
		for (int i = 0; i < width; i++) {
			System.out.print("=");
		}
		System.out.println("*");
	}

}
  • Mainクラス
    • 上記4クラスを組合わせて数字の表示を行う
public class Main {
	public static void main(String[] args) {
		Display dp1 = new Display(new NumberDisplayImpl(12345));
		Display dp2 = new RandomDisplay(new NumberDisplayImpl(12345));
		RandomDisplay dp3 = new RandomDisplay(new NumberDisplayImpl(9999999));
		
		dp1.display();
		dp2.display();
		dp3.display();
		dp3.randomDisplay();
	}
}
*=====*
|12345|
*=====*
*=====*
|12345|
*=====*
*=======*
|9999999|
*=======*
*=======*
|9999999|
|9999999|
|9999999|
*=======*
まとめと補足
  • 登場人物
    • Abstraction役:機能のクラス階層の最上位クラス、Implementor役を保持
    • RefinedAbstraction役:Abstractionに機能を追加した役
    • Implementor役:実装のクラス階層の最上位クラス、Abstraction役のAPIを実装するためのメソッドを規定
    • ConcreteImplementor役:具体的にImplementor役のAPIを実装する役
  • 分けることで拡張が楽になる
    • Bridgeパターンの特徴は、機能と実装のクラス階層を分けること
    • これにより、それぞれのクラス階層を独立に拡張することができる
    • 機能を追加したければ機能のクラス階層にクラスを追加
    • 実装のクラス階層は全く修正する必要はなく、追加した機能は全ての実装で利用できる
    • OSに例えると、OS依存の部分を実装のクラス階層で表現
    • こうしておけば、機能のクラス階層でいくら機能を追加しても、実装のクラス階層でそれぞれ対応できる
  • 継承と委譲
    • 継承はクラスを拡張するのに便利だが、クラス間の結びつきを固定してしまう
    • 必要に応じてクラス間の関係を切り替えたいときには、委譲を使う
    • printを実行するときにはimpl.rawPrintを呼んでいる箇所が委譲にあたる