よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

「初心者のためのデザインパターン入門」シリーズ 第3回【Adapter - クラスの仲介役 - 】

前置き

引用元:Java言語で学ぶデザインパターン入門第3版

デザインパターンはクラスの再利用化を促進するものです。 再利用化を促進するとは、 クラスを部品として使えるようにするということであり、1つの部品を修正しても、他の部品の修正が少なくてすむ、ということなのです。

本記事は当ブログ管理人よしたろうによる『GoFデザインパターン入門シリーズ』です。本記事では、各デザインパターンの総合的理解ができることをコンセプトにしております。ピンポイントな情報ではなく、理解に必要な周辺知識をできる限り記述していきます。一記事あたりの文章量は多くなる傾向になります。あまりそういった記事が見当たらないこと、自身の学習のためにそういった形式にしております。ただし、他の記事で記述したものに関しては引用や紹介で済ませます。

デザインパターンって何?という方は過去記事

「Java言語で学ぶデザインパターン入門」シリーズ 第0回 【デザインパターンとは?】

をご覧下さい。

今回の記事の 『Adapter』 は「継承」を用いる場合と「委譲」を用いる場合の二種類があります。

adaptという単語には「適合させる」とか「なじませる」「適応させる」という意味があります。つまり、adapterとは「適合させるもの」という意味になります。Wrapper(ラッパー)パターンともよばれ、こちらの方がイメージしやすいかもしれませんね。

適用箇所として

  • 『既に提供されているもの』と『必要なもの』の間のずれを埋めたい場合

  • 「既に外部ライブラリ等で提供されている汎用的なクラスを、実装しているシステムで定義したインターフェースに合わせたい」

というような状況がよくあると思います。そういうケースでAdapterパターンは有効に作用するようです。

また継承による Adapter より委譲を用いる Adapter の方が主流だと思うので本記事では後者について話していきます。本質的にしていることは同じだと思いますが、以下の理由で委譲の Adapter を使用した方がいいかなと思います。

  1. 実装の継承自体のデメリット(後述)
  2. 委譲の場合、既存クラスのインスタンスをフィールドに持ち、 必要なメソッドを呼び出すため、継承のように他のクラスを意識して実装する必要がない。
  3. 多重継承の Adapter(クラス形式 Adapter) は Java では実装できない

ただし、継承の Adapter でないと実現できない場合はその限りではないと思いますが、どの場合かわからなかったので言及していません。 スーパとサブを明確に関連付けたい場合とか?など思いますが、それであれば Adpter をそもそも使う必要なくね?とか、インターフェイスの継承があるから型でくくれるしなー、などおもったり。生成コストの面かな?だれかわかる方がいれば教えてくださいませ.....

1. Template Method パターンのGoF本におけるカテゴライズ

プログラムの構造に関するパターン 使い所
Adapter
(一皮かぶせて再利用)
あるクラスのインタフェースを、クライアントが求める他のインタフェースへ変換する。Adapterパターンは、インタフェースに互換性のないクラス同士を組み合わせることができるようにする。 編集できないインタフェースを
求める形に変換したい時。
目的
生成 構造 振る舞い
範囲 クラス Factory MethodAdapter Interpreter / Template
オブジェクト Abstract Factory / Builder / Prototype / Singleton Adapter / Bridge / Composite / Decorator / Façade / Flyweight / Proxy Chain of Responsibility / Command / Iterator / Mediator / Memento / Observer / State / Strategy / Visitor

1-2. 2009年に再定義されたGoFデザインパターンでのカテゴライズ

該当なし(削除されました)

Core(重要) Creational(生成) Periphenal(あまり使用されない) Other(その他)
Composite / Strategy / State / Command / Iterator / Proxy /Template Method Factory / Prototype / Builder / Dependency Injection Abstract Factory / Visitor / Decorator / Mediator / Type Object / Null Object / Extension Object/Interface Flyweight / Interpreter

詳細は以下の過去記事に記述してますので気になればご覧ください

「継承」「合成と委譲」「インターフェイス」「抽象依存」について詳しく知りたい方は以下の過去記事をご覧ください。
かなり長いですのでご了承下さいませ。

『継承・インターフェイス・抽象クラス』シリーズ

2. Adapterパターンの概要

既存のクラス(Adaptee)を、Adaptee とは互換性のないインタフェース・抽象クラス(Target)に適合させる際に、既存のクラス(Adaptee)と異なるインターフェイス(Target)の間を取り持つのが Adapter です。 Adapter クラスを設けることで、既存のクラス(Adaptee)を変更することなく、新しいインタフェースを通じて、Adaptee の実装を再利用することができます。

また、前提条件として Main(Client)と Banner(Adaptee)は修正・変更できないと仮定しています。

2-1. 既存クラス(Adaptee)の再利用

既存クラスを再利用するために繋ぎ合わせる。そのクラス・メソッドが十分な品質(テスト・バグ・稼働実績)を保っているならば、そのクラスを部品として再利用したい。

Adapter(Wrapper)パターンは既存クラスに一皮かぶせて必要とするクラスを作り、必要とするメソッド群を素早く作ることができる。もしバグが出ていたとしても,既存クラス(Adaptee)はバグがないことがわかるため、Adapter(Wrapper)クラスを重点的に調べればよくなり,プログラムのチェックが楽になります。

既存のクラスを新しいインターフェイスに適合させたい際に、既存クラス(Adaptee)を修正してしまうと、動作テストが終わっている既存クラスを修正後にもう一度テストしないといけなくなります。この時に、既存クラスを一切修正することなく、新しいインターフェイス(Target)と既存クラス(Adaptee)を適合させることができるのが Adapter パターンです。

2-2. 外部の新しいAPIを、内部の既存インターフェイスに適合させる

外部の新しいAPIが今回はAdapteeになります。Adaptee は外部にある(信頼性のある)既存のクラスとも見れますし、(内部の)既存の実装に適合させたいもの、のとも言えます 新しいAPI(Adaptee)とは、例えばサードパーティ製のものでソースコードを持たない・こちらでは編集できない場合だとします。呼び出し先のソースに手を加えることができない状況です。

  • 他社の製品
  • そもそもソースがない。(実行ファイルだけもらっている)
  • すでにテストされていて手を加えるとテストが大変になる

新しい外部のAPI(Adaptee)を使用するにあたって、クライアント(Main)や内部の既存インターフェイス(Target)を変更するのは再利用に観点・変更コストの観点から避けたいですよね。

クライアントが使用しているインターフェイスと、外部のAPIを適合させるのが Adapter になります。下記にコードがありますが、Adapter は抽象型(インターフェイス・抽象クラス)から派生し実際の処理を Adaptee に委譲しています。あくまで Client は使用したいインターフェイスを使用していることしか知りませんが、実際の処理は Adaptee が行います。

合成と委譲につきましては以下の過去記事「1-5. 継承よりコンポジション(合成)、もしくは継承よりデリゲーション(委譲)」にて解説しています。

2-3. SRP(単一責任原則)違反の解消

もし Adapter が存在しない場合、インターフェイス(Target)と既存のクラス(Adaptee)は継承によって結合してします。SRPの定義は「クラスを変更する理由は一つ以上あってはならない」ですが、上記の二つが同じ理由で変更されるとは限らない場合も多々あります。また、使用したいインターフェイスから派生させることができない場合も Adapter が役立ちます。間に Adapter を入れることで本来であれば適合しないもの同士を繋げることができます。

Adapter は仲介役なんですね。

2-4. OCP(開放閉鎖原則)違反の解消

クライアント側は、 クライアントが使用したいインタフェースを介して Adapter とやりとりする限り、 Adaptee と結合されることがなくなります。 このおかげで、 新しい種類のアダプターをプログラムに導入しても既存のクライアント側コードには無関係であり、テストも不要で問題なく動作します。

拡張に対しては新しいクラスを追加することで解放しており、既存の実装を変更することに閉鎖していますね。

2-5. バージョンアップと互換性

古い版と新しい版を共存させて楽にメンテナンスをするために、Adapterパターンを用いることもできる様です。

3. サンプルプログラム

与えられた文字列を (Hello) のように表示したり,Helloのように表示するサンプルプログラムです。 後述の委譲を利用した版も同様です。 Printインターフェイスアブストラクト、Bannerクラスは変更できないと仮定、 それを用いる実装を行う為に新しく両者を結合できる様、PrintBannerクラスをAdapterとして用いている

継承(is-a)を利用したAdapter

委譲(has-a)を利用したAdapter

継承(is-a)を利用したAdapterのクラス図

委譲(has-a)を利用したAdapterのクラス図

3-1. 継承(is-a)を利用したAdapter

参考・引用元:Java言語で学ぶデザインパターン入門第3版

継承を利用したAdapterは、利用したいクラスのサブクラスを作成し、そのサブクラスに対して必要なインタフェースを実装することで実現される。元のクラスを継承してクラス単位でwrapする。

PrintBannerというAdapterを作成することで、既存クラス(Banner)クラスを修正することなく、 異なるインタフェースを持たせることができる。 このように、既存クラスを修正することなく、異なるインタフェースを持たせるということが、Adapter パターンの役割。

名前 解説
Bannerクラス:提供されているもの
(showWithParen, showWithAster)

Adaptee:適合される側の役
文字列をパレンティスで囲って表示する
文字列をアスタリスクで囲って表示する

Adapterクラスによってインターフェイス
変更される側のクラス。具体的な処理を提供
PrintBannerクラス:差異を埋めるもの

Adapter : 適合させる側の役
Adapter(Wrapper)役のクラス

Adapteeが提供するインターフェイス
Targetが提供するインターフェイスに変換するクラス
Print interface:要求されるもの

Target : 対象の役
文字列を弱く表示するprintWeak()
強く表示するprintStrong()

clientが要求するインターフェイスを提供
Mainクラス:呼び出す側

Client : 依頼者役
インスタンス生成・メソッド呼び出し
フロー制御

3-1-1. サンプルプログラムのクラス図

3-1-2. サンプルプログラムのコード

3-1-2-1. Main.class

/**
 * Client : 依頼者役
 */
public class Main {
    public static void main(String[] args) {
        // 提供されているものではなく Adapter 役を利用
        Print p = new PrintBanner("Hello");
        p.printWeak();
        p.printStrong();
    }
}

3-1-2-2. PrintBanner.class

/**
 * Adapter : 適合させる側の役
 */
public class PrintBanner extends Banner implements Print {

    public PrintBanner(String string) {
        super(string);
    }

    // showWithParen()を継承し,printWeak()に実装
    @Override
    public void printWeak() {
        showWithParen();
    }

    // showWithAster()を継承し,printStrong()に実装
    @Override
    public void printStrong() {
        showWithAster();
    }

}

3-1-2-3. Banner.class

/**
 * Adaptee:適合される側の役
 */
public class Banner {

    private String string;

    // コンストラクタ
    public Banner(String string) {
        this.string = string;
    }

    // パレンティスで囲って表示
    public void showWithParen() {
        System.out.println("(" + string + ")");
    }

    // アスタリスクで囲って表示
    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}

3-1-2-4. Print interface

/**
 * Target : 対象の役
 */
public interface Print {
    public abstract void printWeak();
    public abstract void printStrong();
}

3-2. 委譲(has-a)を利用したAdapter

参考・引用元:Java言語で学ぶデザインパターン入門第3版

継承版では自分の基底クラスから継承したshowWithParen(),showWithAster()を呼び出してましたが、今度はフィールド経由で呼び出してます。利用したいクラスのインスタンスを生成し、そのインスタンスを他クラスから利用することで実現されます。委譲を利用したAdapterは、元のクラスのインスタンスコンポジションで保持してオブジェクト単位でwrapする。

名前 解説
Bannerクラス:提供されているもの
(showWithParen, showWithAster)

Adaptee:適合される側の役
文字列をパレンティスで囲って表示する
文字列をアスタリスクで囲って表示する

Adapterクラスによってインターフェイス
変更される側のクラス。具体的な処理を提供
PrintBannerクラス:差異を埋める

Adapter : 適合させる側の役(仲介役)
Adapter(Wrapper)役のクラス

Adapteeが提供するインターフェイス
 Targetが提供するインターフェイスに変換するクラス 
Printインターフェイス:要求されるもの

Target : 対象の役
文字列を弱く表示するprintWeak()
強く表示するprintStrong()

clientが要求するインターフェイスを提供
Mainクラス:呼び出す側

Client : 依頼者役
インスタンス生成・メソッド呼び出し
使用するインターフェイス指定
フロー制御

3-2-1. サンプルプログラムのクラス図

3-2-2. サンプルプログラムのコード

3-2-2-1. Main.class

/**
 * Client : 依頼者役
 */
public class Main {
    public static void main(String[] args) {
        // 提供されているものではなく Adapter 役を型に指定する
        Print p = new PrintBanner("Hello");
        p.printWeak();
        p.printStrong();
    }
}

3-2-2-2. PrintBanner.class

/**
 * Adapter : 適合させる側の役
 */
public class PrintBanner extends Print {
    // インスタンスフィールドは private で包含する(委譲)
    private Banner banner;

    public PrintBanner(String string) {
        // 初期化の際にフィールドにAdapteeインスタンス代入
        this.banner = new Banner(string);
    }

    @Override
    public void printWeak() {
        // Adaptee に委譲
        banner.showWithParen();
    }

    @Override
    public void printStrong() {
        // Adaptee に委譲
        banner.showWithAster();
    }
}

3-2-2-3. Banner.class

/**
 * Adaptee:適合される側の役
 */
public class Banner {

    private String string;

    // コンストラクタ
    public Banner(String string) {
        this.string = string;
    }

    // パレンティスで囲って表示
    public void showWithParen() {
        System.out.println("(" + string + ")");
    }

    // アスタリスクで囲って表示
    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}

3-2-2-4. Print.class

/**
 * Target : 対象の役 インターフェイスでも可
 */
public abstract class Print {
    public abstract void printWeak();
    public abstract void printStrong();
}

3-4 それぞれの動き

  1. Main(クライアント) クラス

    • PrintBanner(Adapter) のインスタンス化(コンストラクタでインスタンスの初期化)
    • 参照型変数にインスタンスの参照先を格納
    • インスタンス(Adapter)が継承しているPrint(Target)のあ各メソッドを呼び出す
    • 型としてインターフェイスの Print 型を指定する
      • Print インタフェース(Target)のメソッドだけを用いるという点を強調しています。Print型の変数に代入してそれを使うことで、「PrintBanner クラスのメソッドを利用しているのではなく、 Print インタフェースのメソッドを利用しているのだ」という意図をはっきりさせています。
  2. 参照型変数 p から 各メソッドの呼び出し

    • PrintBanner(Adapter)クラスはインスタンスフィールドとして Banner(Adaptee) を集約している。
    • PrintBannerクラスで実装された各メソッドの実行。
    • 各メソッドの内部で集約しているインスタンスからメソッドを呼び出す。
  3. Adapter(PrintBanner) から Adaptee(Banner) を呼び出す

    • Adapter(PrintBanner)では、使用したいAdaptee(Banner) の実装しているメソッドが隠されています。
      • Client は Adapter を Target だと思って(Targetを実装しているので)使用するが、実際の処理は Adaptee が行なっている。
    • Adapter は委譲を用いて実際の処理である Adaptee の機能を利用しています。
    • また、インターフェイス(Target)と Adaptee は同名のメソッドを持つ必要はない。
      • Adapter が Adaptee を Target に変更してくれる。

4. このパターンの特徴

Adapterパターンは「委譲」「ポリモーフィズム」を利用したパターンです。 委譲を使ったAdapterパターンの場合、既存のクラス(Banner)を包み込むクラス(PrintBanner)を用意し、そのクラスで新しいAPI(Printインターフェイス) を提供します。ここで、具体的な処理自体は、包み込んだ既存のクラス(Banner)に任せてしまうような実装を行います。

クライアント側にはインターフェースを通じて、新しいAPI(Adaptee)を提供します。つまり、処理を実装したクラス(Adapter)は、クライアント側に提供されるインターフェース(Target)を実装しています。これにより、クライアント側からはインターフェース(Target)しか見えなくなり、その向こうにある処理の実装(Adaptee)を意識する必要がなくなります。

この「実装を意識することがなくなる」ため、実装を切り替える場合もアダプタに相当するクラスのみ修正するだけですみます。

また、Adapter クラスによりクライアントは、 Adaptee を Adapter クラスの派生元と同じように使用することができる(ポリモーフィズム

5. 注意点

5-1. かけ離れたクラスには使えない

Adaptee と Target がかけ離れていた場合、Adapter は複雑な実装になってしまう。Adaptee と Target の機能がかけ離れてる場合は、Adapterパターンは非推奨となる。

5-2. やはり、継承より委譲

継承のリスク

引用元:過去記事「継承について」

  1. サブクラスはスーパークラスの実装に強く依存しているので変更に弱い
  2. 継承の階層が深まるのは非常に危険だが、誰でも簡単にできてしまう
  3. リスコフの置換原則・スーパークラスとサブクラスの関係性「is - a」を破ってしまう様な継承(extends)が簡単に行える
  4. 階層が深くなりすぎたり関係性が破られてしまうと、可読性・保守性・拡張性・再利用性は大きく損なわれる
  5. 漫然と使用するといつでもどこでも上記が起こり得る
  6. 継承(extends)はクラスツリー内での実行に制限があるため「組み合わせ爆発」が起こりうる
  7. 多重継承問題
  8. カプセル化の破壊(ホワイトボックスな再利用)

5-3. コストが高い

引用先:アジャイルソフトウェア 開発の奥義

Adapter のコスト Adapter は高くつく。 新たにクラスを作成しなければならないし、 Adapter のインスタンスを作成し、処理を委譲するオブジェクトをそれに結合しなければならない。また、Adapter を起動するたびに時間と委譲に必要なメモリスペースとを浪費する。したがって、いつでも気軽に Adapter を使おうとは思わないだろう。 通常、 Abstract Server パターンで十分であることがほとんどだ。

5-4. 複雑性の増加

一連の新規のインターフェースとクラスを追加する必要があるため、 全体的なコードの複雑性が増加しやすい。

6. Decorator パターンとの類似点と違い

AdapterパターンはDecoratorパターンは、コンポジションによって目的とするクラスを内包するスタイル・構造が非常に似ているが、使用する目的が異なります。

Adapter
主目的:インターフェースの差異を吸収したい

インターフェイスの変更が難しいオブジェクトを別のインターフェイスに適合させる」ための仲介。インターフェイスの異なるクラス同士を結び付ける。

Decorator
主目的:拡張を動的にして柔軟にしたい

「既存のオブジェクトに機能や振る舞いを追加する」ための拡張。インターフェイスは同一のまま機能を拡張する。