本記事ではGoFのデザインパターンのプログラムの構造に関するパターンの一つである「Bridge」パターンを解説します。このパターンを一言で説明するならば、「機能と実装を分離して、それぞれで拡張することが可能になるパターン」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。
「Bridge」パターンとは
先述した通り「Bridge」パターンとは「機能と実装を分離して、それぞれで拡張することが可能になるパターン」です。直訳すると「橋」になります。「機能を拡張させるための階層」と「実装を拡張するための階層」を分離して、それぞれを独立して拡張することができます。
例えば、下記の例を考えてみましょう。(Bridgeパターンを利用しない悪い例です)
まず抽象クラスのClassAを実装するようにClassA1とClassA2を作り、methodAを実装しました。ここでClassAに新たに抽象メソッド(methodB)を追加しようと思います。ClassA1とClassA2にはmethodBが必要ないため、ClassAを継承するようにClassABを作り、抽象メソッドのmethodBを作りました。このClassABを継承してClassAB1とClassAB2を作りmethodAとmethodBをそれぞれ実装しました。さらに、ClassABに抽象メソッド(methodC)を追加しようと思いますが、ClassAB1とClassAB2にはmethodCは必要ないため、ClassABを継承してClassABCを抽象クラスとして作り、抽象メソッドとしてmethodCを作りました。このClassABCを継承してClassABC1とClassABC2を作り、methodAとmethodBとmethodCを実装しました。さらにClassABCに抽象メソッド(methodD)を追加しようと思いますが、、、、、、、
めっちゃめんどくさい!!!抽象クラスのClassAにmethodAとmethodBとmethodCを追加してしまうと、必要のないところで「何も処理をしないが、とりあえずオーバーライドだけしておく」みたいな無駄な実装が出てきてしまいます。ここからさらにメソッドを追加しようものなら、継承に継承を重ねていき、どんどん可読性が低くなり、実装の手間も増えていきます。
ここで上記の例にBridgeパターンを適用してみましょう。
抽象クラスのClassAはImplementor型のフィールドを配列で持っています。Implementorクラスはインターフェースで、このインターフェースのapplyFeatureをmethodAやmethodBの内容で実装します。このapplyFeatureメソッドを使うことでmethodAやmethodBを間接的に呼び出すことが可能です。そのためClassAを継承したClassA1やClassA2ではメソッドの実装を行う必要はありません。Implementorの配列に「機能を追加」するだけでよいため、継承が深くなったとしても実装が増えることはありません。
「Bridge」パターンの書き方
それではBridgeパターンを利用する例と利用しない例をみてみましょう。本記事の例では自動車の設計をコード上で行うときの例で考えます。自動車といっても多くの種類が存在しますが、全てに共通する「運転する」とは別にさまざまな機能を持った自動車があります。その自動車ごとに特徴を追加できるようにBridgeパターンを実装します。
✖︎ Bridgeパターンを使用しない例 ✖︎
まずはBridgeパターンを利用しない例からです。まず自動車の抽象クラスを用意します。その抽象クラスを継承して「軽自動車」と「普通車」のクラスを作成します。
// 自動車の抽象クラス
public abstract class Car {
public abstract void drive();
}
// 軽自動車
public class LightCar extends Car {
@Override
public void drive() {
System.out.println("軽自動車を運転します");
}
}
public class OrdinaryCar extends Car {
@Override
public void drive() {
System.out.println("普通車を運転します");
}
}
「運転する」抽象メソッドをそれぞれのクラスで実装しています。
ここで、装甲車を作って欲しいとの依頼がありました。装甲車は車体の重量が大きいためエンジンの出力を増やす必要があります。「運転する」という特徴は同じなためCarクラスを継承してHeavyCarクラスを抽象クラスとして定義し、そのクラスを継承してArmoredCarクラスを作ります。
// 重量級の車
public abstract class HeavyCar extends Car {
public abstract void upEngineOutput();
}
// 装甲車
public class ArmoredCar extends HeavyCar {
@Override
public void drive() {
System.out.println("装甲車を運転します");
}
@Override
public void upEngineOutput() {
System.out.println("エンジンの出力を上げます");
}
}
装甲車の「運転する」と「エンジンの出力を上げる」メソッドの実装ができました。
ここでさらに追加の注文が入ります。軽トラックとダンプカーを作って欲しいとのことです。どちらとも荷台があるため、「荷台をあげる」メソッドを追加しようと思うため、HeavyCarを継承してTruckクラスを抽象クラスとして作ります。この抽象クラスを継承してLightTruck(軽トラック)とDunpTruck(ダンプカー)のクラスを作成します。
// トラック
public abstract class Truck extends HeavyCar {
public abstract void upCarrier();
}
// 軽トラック
public class LightTruck extends Truck {
@Override
public void drive() {
System.out.println("軽トラックを運転します");
}
@Override
public void upEngineOutput() {
// 軽トラックにはエンジンの出力を常時上げる必要はないため何もしない
}
@Override
public void upCarrier() {
System.out.println("荷台を上げます");
}
}
// ダンプカー
public class DunpTruck extends Truck {
@Override
public void drive() {
System.out.println("ダンプカーを運転します");
}
@Override
public void upEngineOutput() {
System.out.println("エンジンの出力を上げます");
}
@Override
public void upCarrier() {
System.out.println("荷台を上げます");
}
}
ダンプカーでは「運転をする」「エンジンの出力を上げる」「荷台を上げる」の3つのメソッドを実装していますが、軽トラックの方ではエンジンの出力を常時上げておく必要はないので、何も実装していません。また「エンジンの出力を上げる」メソッドはどこでも同じ処理内容なのに、クラスを継承するたびに同じ実装をしているのがわかります。
これらのクラスを呼び出すと以下のようにな入ります。
public class Main {
public static void main(String[] args) {
// 普通車
Car ordinaryCar = new OrdinaryCar();
ordinaryCar.drive();
// 軽自動車
Car lightCar = new LightCar();
lightCar.drive();
// 装甲車
HeavyCar armoredCar = new ArmoredCar();
armoredCar.drive();
armoredCar.upEngineOutput();
// 軽トラック
Truck lightTruck = new LightTruck();
lightTruck.drive();
lightTruck.upEngineOutput(); // 何もしないのにメソッドは呼べる
lightTruck.upCarrier();
// ダンプカー
Truck dunpTruck = new DunpTruck();
dunpTruck.drive();
dunpTruck.upEngineOutput();
dunpTruck.upCarrier();
}
}
>> 実行結果
軽自動車を運転します
普通車を運転します
装甲車を運転します
エンジンの出力を上げます
軽トラックを運転します
荷台を上げます
ダンプカーを運転します
エンジンの出力を上げます
荷台を上げます
それぞれの自動車のインスタンスの型はCarではなくそれを継承した型になっており、型がバラバラです。Car型に入れることもできますが、その場合その子要素で追加したメソッドは型の違いで利用することができなくなります。非常に使いにくいです。
また軽トラックでは「エンジンの出力を上げる」メソッドを呼ぶことができてしまっています。これは誤解を生みやすく、バグの引き金になりかねません。
⭕️ Bridgeパターンを使用する例 ⭕️
それでは上記の車の機能の例をBridgeパターンを利用して実装します。まず、「Bridge」の根幹となる「機能」のインターフェースを作成し、それぞれのメソッドとして実装したクラスを作成します。
// 機能実装クラス
public interface Implementor {
// 機能
void applyFeature();
}
// エンジンの出力を上げる
public class UpEngineOutputImpl implements Implementor {
@Override
public void applyFeature() {
System.out.println("エンジンの出力を上げます");
}
}
// 荷台を上げる
public class UpCarrierImpl implements Implementor {
@Override
public void applyFeature() {
System.out.println("荷台を上げます");
}
}
それぞれの機能のクラスはImplementorを実装しています。これで機能をImplementor型のオブジェクトとして扱うことができるようになりました。
次にCarクラスを修正します。Carクラスには複数の機能を持つ可能性があるためImplementorを配列で持っています。またその配列内のImplementorのapplyFeature()メソッドを、格納されている分だけ実行するメソッドも持っています。これらは抽象メソッドではありません。
// 自動車の抽象クラス
abstract public class Car {
// 機能クラスをフィールドで持つ
private Implementor[] implementors;
public Car(Implementor... implementors) {
this.implementors = implementors;
}
// 運転することは全ての自動車に共通の仕様
public abstract void drive();
// 車種ごとの特徴を実行するメソッド
public void applyFeatures() {
if (this.implementors.length != 0) {
for (Implementor implementor : this.implementors) {
implementor.applyFeature();
}
}
}
}
Carクラスはコンストラクタの引数で複数のImplementorを指定可能になっています。またdrive()は全ての自動車に共通のメソッドですが、自動車ごとに運転する方法が違いますので、抽象メソッドとして継承することで実装できるようにしています。
このCarクラスを継承して、「軽自動車」「普通車」「装甲車」「軽トラック」「ダンプカー」を実装してみます。
// 軽自動車
public class LightCar extends Car {
@Override
public void drive() {
System.out.println("軽自動車を運転します");
}
}
// 普通車
public class OrdinaryCar extends Car {
@Override
public void drive() {
System.out.println("普通車を運転します");
}
}
// 装甲車
public class ArmoredCar extends Car {
public ArmoredCar(Implementor... implementors) {
super(implementors);
}
@Override
public void drive() {
System.out.println("装甲車を運転します");
}
}
// 軽トラック
public class LightTruck extends Car {
public LightTruck(Implementor... implementors) {
super(implementors);
}
@Override
public void drive() {
System.out.println("軽トラックを運転します");
}
}
// ダンプカー
public class DunpTruck extends Car {
public DunpTruck(Implementor... implementors) {
super(implementors);
}
@Override
public void drive() {
System.out.println("ダンプカーを運転します");
}
}
全てCarクラスを継承してdrive()メソッドのみ実装しています。このクラスにはまだ機能は追加されていません。これらのクラスを呼び出ししてみます。
public class Main {
public static void main(String[] args) {
// 軽自動車
Car lightCar = new LightCar();
lightCar.drive();
lightCar.applyFeatures();
// 普通車
Car ordinaryCar = new OrdinaryCar();
ordinaryCar.drive();
ordinaryCar.applyFeatures();
// 装甲車
Car armoredCar = new ArmoredCar(new UpEngineOutputImpl());
armoredCar.drive();
armoredCar.applyFeatures();
// 軽トラック
Car lightTruck = new LightTruck(new UpCarrierImpl());
lightTruck.drive();
lightTruck.applyFeatures();
// ダンプカー
Car dunpTruck = new DunpTruck(new UpEngineOutputImpl(), new UpCarrierImpl());
dunpTruck.drive();
dunpTruck.applyFeatures();
}
}
>> 実行結果
軽自動車を運転します
普通車を運転します
装甲車を運転します
エンジンの出力を上げます
軽トラックを運転します
荷台を上げます
ダンプカーを運転します
エンジンの出力を上げます
荷台を上げます
新しくオブジェクトを立てる時に、コンストラクタの引数にImplementor型のオブジェクトを入れて機能を追加しています。全てCar型で立てることができており、利用側では型を意識する必要はありません。またどのオブジェクトでもapplyFeatures()を実行することで、追加した機能を利用することができます。
まとめ
本記事で利用したサンプルケースのクラス図は下記になります。
「Bridge」パターンは機能と実装を分離して、それぞれで拡張することが可能になるパターンです。上手く使えば可読性の高いコードになり保守も楽になります。ぜひ使ってみてください。
本記事で利用したコードは下記で公開しております。
コメント