はじめに
Gofパターンの一つで構造に関するパターンである,
Adapterパターンについて見ていきます.
Adapterパターンについて
Adapterパターンは,互換性のないインタフェースを持つクラス同士をつなげて
協調動作させることです.
電源「Adapter」のように仲介する役割を持ったクラスが存在する
デザインパターンです.
こうすることにより,2つのオブジェクト間に関連性を持たせることができます.
また,Adapterパターンにはオブジェクト形式(委譲を利用) を利用したものと,
クラス形式(`継承を利用) を利用したものがあります.
Adapterパターンの実装例
Adapterパターンの実装例について考えていきたいと思います.
実装例1(Adapterパターン未適用)
まず,スイッチに対してONとOFFの指示を出してライトにONとOFFを通達するという設定の
以下のような設計を考えてみます.
クラス図
この場合のコードは以下のように記述します.
class Light{
public void turnOn(){
Sustem.out.println("Light is turned ON");
}
public void turnOff(){
System.out.println("Light is turned OFF");
}
}
class Switch{
private Light light;
public Switch(Light light){
this.light = light;
}
public void pressOn(){
light.turnOn();
}
public void pressOff(){
light.turnOff();
}
}
public class SwitchExample{
public static void main(String[] args){
Light light = new Light();
Switch lightSwitch = new Switch(light);
lightSwitch.pressOn();
lightSwitch.pressOff();
}
}
問題点
この場合,以下のような問題点が考えられます.
- Switchクラスが直接Lightクラスに依存し,柔軟性が欠如
(依存関係逆転の原則
・オープン・クローズドの原則
に反します.) - Lightに直接依存することで,異なる種類のデバイスを一貫した方法で扱えない
(別のデバイスでSwitchが必要なときに,Lightも一緒に連れていくことになります)
...この説明だけではわかりづらいので具体例もあげて考えてみます.
Fanクラスを追加
具体的にFanクラスを追加することを考えてみます.
Fanクラスを追加した場合,
クラス図とソースコードは以下のようになります.
クラス図
ソースコード
// Light.java
class Light {
public void turnOn() {
System.out.println("Light is turned on");
}
public void turnOff() {
System.out.println("Light is turned off");
}
}
// Fan.java
class Fan {
public void start() {
System.out.println("Fan is started");
}
public void stop() {
System.out.println("Fan is stopped");
}
}
// Switch.java
class Switch {
private Light light;
private Fan fan;
public Switch(Light light, Fan fan) {
this.light = light;
this.fan = fan;
}
public void turnOnLight() {
light.turnOn();
}
public void turnOffLight() {
light.turnOff();
}
public void turnOnFan() {
fan.start();
}
public void turnOffFan() {
fan.stop();
}
}
// SwitchExample.java
public class SwitchExample {
public static void main(String[] args) {
Light light = new Light();
Fan fan = new Fan();
Switch sw = new Switch(light, fan);
sw.turnOnLight();
sw.turnOnFan();
sw.turnOffLight();
sw.turnOffFan();
}
}
Fanクラスを追加した結果,発生する問題点は具体的には以下の通りになります.
- 新しいデバイスを追加するたび,Switchクラスに新しいコードを追加
(拡張性が低い) - SwitchがLightやFanの具体的なクラスに依存することで,Switchクラスに修正が必要
(結合度が高い) - LightとFanで電源をOn,Offにするメソッドが異なる可能性
(一貫性が欠如)
このように,あるクラス(Switch)が具体的なクラス(Light)に依存することで,
様々な問題点が生じてしまいました.
実装例2(Abstract Serverパターン)
Adapterパターンを適用せずとも,SwitchとLightの間にインタフェースを導入することで
実は依存関係逆転の原則
とオープン・クローズドの原則
は満たすことができます.
したがって,上記のような問題を解決することができます.
クラス図とソースコードは以下のようになります.
(以下のような構成をAbstract Serverパターンと呼ぶみたいです)
クラス図
ソースコード
interface Switchable {
void turnOn();
void turnOff();
}
class Light implements Switchable {
@Override
public void turnOn() {
System.out.println("Light is turned on");
}
@Override
public void turnOff() {
System.out.println("Light is turned off");
}
}
class Switch {
private Switchable device;
// コンストラクタでSwitchableデバイスを受け取る
public Switch(Switchable device) {
this.device = device;
}
public void turnOn() {
device.turnOn();
}
public void turnOff() {
device.turnOff();
}
}
public class SwitchExample2 {
public static void main(String[] args) {
// LightをSwitchableとして扱う
Switchable light = new Light();
// Switchでデバイスを操作
Switch lightSwitch = new Switch(light);
// Lightの操作
lightSwitch.turnOn(); // "Light is turned on"
lightSwitch.turnOff(); // "Light is turned off"
}
}
本当に問題が解決しているかどうかをFanクラスを追加して,
見ていきましょう.
Fanクラスの追加
以下がクラス図とソースコードになります.
クラス図
ソースコード
// Switchable.java
// 変更なし
interface Switchable {
void turnOn();
void turnOff();
}
// Light.java
// 変更なし
class Light implements Switchable {
@Override
public void turnOn() {
System.out.println("Light is turned on");
}
@Override
public void turnOff() {
System.out.println("Light is turned off");
}
}
// Fan.java
// Fanクラスを追加
class Fan implements Switchable {
@Override
public void turnOn() {
System.out.println("Fan is started");
}
@Override
public void turnOff() {
System.out.println("Fan is stopped");
}
}
// Switch.java
// 変更なし
class Switch {
private Switchable device;
// コンストラクタでSwitchableデバイスを受け取る
public Switch(Switchable device) {
this.device = device;
}
public void turnOn() {
device.turnOn();
}
public void turnOff() {
device.turnOff();
}
}
// SwitchExample2.java
// Fanクラスを追加
public class SwitchExample2 {
public static void main(String[] args) {
Switchable light = new Light();
Switchable fan = new Fan();
Switch lightSwitch = new Switch(light);
Switch fanSwitch = new Switch(fan);
lightSwitch.turnOn();
lightSwitch.turnOff();
fanSwitch.turnOn();
fanSwitch.turnOff();
}
}
Fanクラスを追加しても,新しいコードの追加や修正が最低限となり,
上記のような問題点が解決できたことを確認出来ました.
問題点...?
「Adapterパターンを適用せずとも問題が解決したし,これを用いればよいのでは?」
と思う方もいると思います.
...実はこれは単一責任の原則
に反する問題が発生しています.
LightとSwitchableには継承の関係があります.
LightとSwitchableの関係
この関係により,既存のクラスや外部のクラスを利用する際,
柔軟性に欠ける問題が発生します.
例えば,外注して手に入れたLightクラスへ,
新しいインタフェースを追加することはできません.
さらに,エアコンやテレビなど,Switchで制御したいほかの機器のクラスがあっても
それらに既に継承関係がある場合,直接Swichableから継承させることはできません.
(多重継承ができるなら関係ないかもしれませんが)
つまり,すでに決まった仕組みやルールがあるクラスに
Switchableインタフェースを直接追加することができないことを意味します.
実装例3(オブジェクト形式のAdapterパターン)
では,上記のような問題点を解決するため,
オブジェクト形式のAdapterパターンを適用してみましょう.
以下がクラス図とソースコードになります.
クラス図
ソースコード
interface Switchable {
void turnOn();
void turnOff();
}
class Light {
public void turnOn() {
System.out.println("Light is turned ON");
}
public void turnOff() {
System.out.println("Light is turned OFF");
}
}
class LightAdapter implements Switchable {
private Light light;
public LightAdapter(Light light) {
this.light = light;
}
@Override
public void turnOn() {
light.turnOn();
}
@Override
public void turnOff() {
light.turnOff();
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void pressOn() {
device.turnOn();
}
public void pressOff() {
device.turnOff();
}
}
public class ObjectAdapterExample {
public static void main(String[] args) {
Light light = new Light();
Switchable adapter = new LightAdapter(light);
Switch lightSwitch = new Switch(adapter);
lightSwitch.pressOn();
lightSwitch.pressOff();
}
}
このオブジェクト形式のAdapterパターンの特徴は以下のようになります.
- AdapterクラスがLightのインスタンスを委譲によって操作
- 柔軟に複数のクラスや構成に対応可能
Adapterパターンを適用することで,適切なAdaprterを作成するだけで,
ONとOFFできるオブジェクトであればどんなものでもSwitchは扱えます.
また,Switchableが持っているメソッドと同じ名前のメソッドを
オブジェクトが持つ必要もありません.
これは,Adapterがそのオブジェクトのインタフェースを
Switchableのインタフェースに変換してくれるためです.
Fanクラスの追加
では,(しつこいですが)こちらにもFanクラスを追加してみましょう.
以下がクラス図とソースコードになります.
クラス図
ソースコード
// Switchable.java
// 変更なし
interface Switchable {
void turnOn();
void turnOff();
}
// Light.java
// 変更なし
class Light {
public void turnOn() {
System.out.println("Light is turned ON");
}
public void turnOff() {
System.out.println("Light is turned OFF");
}
}
// LightAdapter.java
// 変更なし
class LightAdapter implements Switchable {
private Light light;
public LightAdapter(Light light) {
this.light = light;
}
@Override
public void turnOn() {
light.turnOn();
}
@Override
public void turnOff() {
light.turnOff();
}
}
// Fan.java
// Fanクラスの追加
// メソッド名がLightと違うことに注目
class Fan {
public void start() {
System.out.println("Fan is spinning");
}
public void stop() {
System.out.println("Fan is stopped");
}
}
// FanAdapter.java
// FanAdapterの追加
// FanクラスのメソッドをturnOnメソッドに変換
class FanAdapter implements Switchable {
private Fan fan;
public FanAdapter(Fan fan) {
this.fan = fan;
}
@Override
public void turnOn() {
fan.start();
}
@Override
public void turnOff() {
fan.stop();
}
}
// Switch.java
// 変更なし
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void pressOn() {
device.turnOn();
}
public void pressOff() {
device.turnOff();
}
}
// ObjectAdapterExample.java
// Fanクラスを追加
public class ObjectAdapterExample {
public static void main(String[] args) {
Light light = new Light();
Switchable adapter = new LightAdapter(light);
Switch lightSwitch = new Switch(adapter);
lightSwitch.pressOn();
lightSwitch.pressOff();
Fan fan = new Fan();
Switchable fanAdapter = new FanAdapter(fan);
Switch fanSwitch = new Switch(fanAdapter);
System.out.println("\nUsing the Fan:");
fanSwitch.pressOn();
fanSwitch.pressOff();
}
}
FanオブジェクトはSwitchableが持っているメソッドと同じ名前にする必要がないことや,
実装例2における問題を解決できることが確認できたと思います.
実装例4(クラス形式)
最後にクラス形式のAdapterパターンを適用してみます.
以下がクラス図とソースコードになります.
(Fanクラスを追加する場合は省略します)
クラス図
ソースコード
interface Switchable {
void turnOn();
void turnOff();
}
class Light {
public void turnOn() {
System.out.println("Light is turned ON");
}
public void turnOff() {
System.out.println("Light is turned OFF");
}
}
// extendsしていることに注目
class LightAdapter extends Light implements Switchable {
@Override
public void turnOn() {
super.turnOn();
}
@Override
public void turnOff() {
super.turnOff();
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void pressOn() {
device.turnOn();
}
public void pressOff() {
device.turnOff();
}
}
public class ClassAdapterExample {
public static void main(String[] args) {
// Lightインスタンスを作っていない
Switchable adapter = new LightAdapter();
Switch lightSwitch = new Switch(adapter);
lightSwitch.pressOn();
lightSwitch.pressOff();
}
}
このクラス形式のAdapterパターンの特徴は以下のようになります.
- AdapterクラスがLightを継承
- コードがシンプルだが,柔軟性が低い
- オブジェクト形式よりも効率と使い勝手がよくなる
Adapterパターンは高コスト
最後にAdapterパターンを適用する上での注意点を見てみます.
それは高コストであることです.
理由は以下のようなものが考えられます
- 新たなクラスを作成する必要性
- Adapterのインスタンスの作成と,オブジェクトの結合
- Adapterの起動によるメモリスペースの消費
そのため,今回の例でいうとSwitchが制御するオブジェクトがLight以外に
出てこない限り,実装例2(Abstract Serverパターン)で十分機能します.
最後に
前回までと比べると少し記事が長くなってしまいました.
ただ,発生しうる問題に対し,具体例を交えた説明をすることで,
デザインパターンを導入し,問題をどのように解決するかのイメージをすることができました.
そのため,今後の記事にも,この説明方法は取り入れていきたいと思います.