本記事ではGoFのデザインパターンのプログラムの構造に関するパターンの一つである「Adapter」パターンを解説します。このパターンを一言で説明するならば、「非互換性の2つのオブジェクトの間にアダプターを設置することで、関連性を持たせることが可能になるパターン」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。
「Adapter」パターンとは
先述した通り、「Adapter」パターンとは「非互換性の2つのオブジェクトの間にアダプターを設置することで、関連性を持たせることが可能になるパターン」です。直訳すると「適合させる」となります。「関連性を持たせる」というと曖昧ですが、本来全く関係のないオブジェクト同士を組み合わせて利用できるようにするパターンです。
このパターンを現実世界で例えるといろいろありますが、一例として「海外旅行で使うコンセント変換プラグ」が挙げられるでしょう。日本国内では壁にあるコンセントにそのまま挿して電源として利用することができます。しかし、海外に行くと日本の電源プラグは現地では全く利用できません。そこで利用するのが「コンセント変換プラグ」です。このプラグを海外のコンセントに挿して、日本の製品のプラグをコンセント変換プラグに挿すことで、海外のコンセントから日本の製品に給電することが可能になります。「Adapter」パターンとはこの「コンセント変換プラグ」に相当します。
このパターンを使うデメリットはないと言っても良いでしょう。このパターンのメリットを挙げると以下になります。
- 新システムと旧システムに互換性を持たすことが可能
- 既存のコードを修正する必要がない
- 既存のコードをテストする必要がない
- 新しいインターフェースを実装すれば良いためコードの柔軟性が向上する
「Adapter」パターンの書き方
「Adapter」パターンを利用する方法として、「継承」を利用した方法と「委譲」を利用した方法の2つに分けられます。これらの方法について実際の例を交えて解説します。
今回の例では任天堂Switchのコントローラーを利用するときの例を考えてみます。ご存知の通り、任天堂Switchには本体に付いているコントローラーの他に、さまざまなコントローラーを利用することができます。例えば、「GameCube」や「NINTENDO64」のコントローラーが挙げられます。「GameCube」のコントラーラーはあくまで「GameCube」で利用されることを想定されているため、Switch本体にコントローラーの端子を接続したり、無線での接続は想定されていません。
そこで利用するのが「接続タップ」です。この装置を利用することで、Switchとコントローラーの中継を行います。これで「GameCube」のコントローラーがSwitchで利用できるようになります。この関係をコードに起こしてみます。
「継承」を利用した「Adapter」パターン
まずは「継承」を利用した例です。Switchで利用するコントローラーの動作を定義したインターフェースを作成します。
public interface SwitchController {
public void buttonA();
public void buttonB();
public void buttonX();
public void buttonY();
}
Switchに送るコントローラの動作として、「A」「B」「X」「Y」のボタンの動作を示すメソッドを用意します。
実際の製品である「GameCubeのコントローラー」を示すクラスを定義します。
public class GameCubeController {
public void pressA() {
System.out.println("「A」が押されました");
}
public void pressB() {
System.out.println("「B」が押されました");
}
public void pressX() {
System.out.println("「X」が押されました");
}
public void pressY() {
System.out.println("「Y」が押されました");
}
}
それぞれのボタンを押したときに動く処理を簡略して作っています。
次にSwitchControllerとGameCubeControllerに関係を持たせるadapterクラスを作成します。
public class GameCubeControllerAdapter extends GameCubeController implements SwitchController {
@Override
public void buttonA() {
pressA();
}
@Override
public void buttonB() {
pressB();
}
@Override
public void buttonX() {
pressX();
}
@Override
public void buttonY() {
pressY();
}
}
GameCubeControllerAdapterクラスはGameCubeControllerを継承し、SwitchControllerを実装しています。SwitchControllerのメソッドを実装するときの処理の中身は、GameCubeControllerのボタンの処理を入れています。こうすることで、このAdapterはSwitchControllerとGameCubeControllerを中継することができます。また中継する際にデータの受け渡しがあって、コントローラーごとにデータのフォーマットが違う場合は、このAdapterクラスでデータの差異を吸収して中継します。
最後に、このAdapterの利用側になります。今回は簡略なのでMainで利用しています。
public class Main {
public static void main(String[] args) {
// GameCubeコントローラーの接続
SwitchController gameCubeAdapter = new GameCubeControllerAdapter();
gameCubeAdapter.buttonA();
gameCubeAdapter.buttonB();
gameCubeAdapter.buttonX();
gameCubeAdapter.buttonY();
// NINTENDO64のコントローラーの接続
SwitchController n64Adapter = new N64ControllerAdapter();
n64Adapter.buttonA();
n64Adapter.buttonB();
n64Adapter.buttonX();
n64Adapter.buttonY();
}
}
「A」が押されました
「B」が押されました
「X」が押されました
「Y」が押されました
「右」が押されました
「下」が押されました
「上」が押されました
「左」が押されました
GameCubeControllerAdapterをnewしていますが、型はSwitchControllerです。GameCubeControllerAdapterもN64ControllerAdapterもどちらもSwitchControllerを実装しているため、利用側はAdapterの型が何かを意識する必要はありません。adapterのそれぞれのボタンを押すだけで処理を中継して、それぞれのコントローラのボタンを押してくれます。「NINTENDO64」を利用するAdapterも作っています。このように利用側はそれぞれのAdapterをnewして利用することで、本来関連性のないコントローラーを利用することが可能になります。
「継承」を利用した「Adapter」パターンのクラス図
継承を利用したAdapterパターンのクラス図は下記になります。
「委譲」を利用した「Adapter」パターン
先述した通り、継承を利用した「Adapter」パターンで実装することができましたが、継承では実装できないこともあります。例えば、SwitchControllerクラスが、インターフェースではなく抽象クラスとして定義されている場合です。この場合、javaでは多重継承が禁じられているため、以下のように実装することはできません。
public class GameCubeControllerAdapter extends GameCubeController extends SwitchController {
:
// コンパイルエラー
このような場合に利用できるのが、「委譲」を利用した「Adapter」パターンです。実際にSwitchControllerクラスを抽象クラスに変更して、委譲のAdapterパターンを実装します。
public abstract class SwitchController {
public abstract void buttonA();
public abstract void buttonB();
public abstract void buttonX();
public abstract void buttonY();
}
まずはSwitchControllerをインターフェースから抽象クラスに変更します。GameCubeControllerクラスに変更はありません。
次にGameCubeControllerAdapterクラスを修正します。多重継承が禁止であるためGameCubeControllerクラスは継承しません。継承するのはSwitchControllerのみです。
public class GameCubeControllerAdapter extends SwitchController {
private GameCubeController controller;
public GameCubeControllerAdapter(GameCubeController controller) {
this.controller = controller;
}
@Override
public void buttonA() {
controller.pressA();
}
@Override
public void buttonB() {
controller.pressB();
}
@Override
public void buttonX() {
controller.pressX();
}
@Override
public void buttonY() {
controller.pressY();
}
}
注目して欲しいのが、フィールドとしてGameCubeControllerのオブジェクトを持っているということです。継承の時は、自身のオブジェクトがボタンを押すメソッドを継承して使っていましたが、「委譲」の時はフィールドとして持っているオブジェクトのメソッドを利用して、SwitchControllerをオーバーライドしたメソッド内で利用しています。このように、別のオブジェクトに処理を渡すため「委譲」と言われています。
この「委譲」のAdapterを利用すると下記のようになります。
public class Main {
public static void main(String[] args) {
// GameCubeコントローラーの接続
GameCubeController gameCubeController = new GameCubeController();
SwitchController gameCubeControllerAdapter = new GameCubeControllerAdapter(gameCubeController);
gameCubeControllerAdapter.buttonA();
gameCubeControllerAdapter.buttonB();
gameCubeControllerAdapter.buttonX();
gameCubeControllerAdapter.buttonY();
// NINTENDO64のコントローラーの接続
N64Controller n64Controller = new N64Controller();
SwitchController n64ControllerAdapter = new N64ControllerAdapter(n64Controller);
n64ControllerAdapter.buttonA();
n64ControllerAdapter.buttonB();
n64ControllerAdapter.buttonX();
n64ControllerAdapter.buttonY();
}
}
GameCubeControllerAdapterのコンストラクタの引数にGameCubeControllerオブジェクトを入れて初期化しています。もし、GameCubeControllerAdapterにはGameCubeControllerしか絶対に入らないのであれば、GameCubeControllerAdapterのコンストラクタ内でGameCubeControllerのオブジェクトを生成するのでも良いかもしれません。
委譲を利用したAdapterパターンでも実行結果は継承利用時と全く同じになります。
「委譲」を利用した「Adapter」パターンのクラス図
委譲を利用したAdapterパターンのクラス図は下記になります。
まとめ
最後に「Adapter」パターンとは「非互換性の2つのオブジェクトの間にアダプターを設置することで、関連性を持たせることが可能になるパターン」です。また「継承」と「委譲」の2パターンのAdapterがあります。
どちらを使えばいいか迷った時は下記を参考にしてみてください。
継承 | 既存のクラスの機能を新しいクラスに継承して再利用することが適している場合 |
委譲 | 既存のクラスを新しいインターフェースに合わせる必要がある場合 多重継承になってしまう場合 |
本記事で利用したコードはGit上で公開しております。是非参考にしてみてください。
コメント