한국어 | English | 日本語
Webアプリケーションエンジニア (経験8.8年)
技術・開発
engineering
ウェブフロントエンドと バックエンド開発を扱います

化粧で見抜くデコレーター、変装で見抜くアダプター

構造型デザインパターンに属するデコレーター、アダプター、ファサードパターンは、いずれもクラス内部に別のオブジェクトを持つ「コンポジション」方式を採用している点で共通しています。しかし、その目的が機能追加なのか、インターフェースの適合なのか、それとも複雑な構造を単純にまとめることなのかによって、明確に区別されます。
クラスの本質を保ちながら機能を追加する「化粧」(デコレーター)と、クライアントの要求に合わせてインターフェースを完全に変更する「変装」(アダプター)、そして複数のクラスを一つのインターフェースで便利にまとめる「ファサード」パターンについて、その動作原理と実装の違いを比較しながら学習します。

変装術

三匹のこぶたの物語には、羊の皮をかぶり、白い粉で手を塗って羊に変装したオオカミが登場します。もちろん、あまりにもお粗末なため末っ子の子ぶたに笑われますが、こぶたたちの家に入るためには「変装」が必要だったのです。今回お話しする内容は、化粧と変装についてです。ここで、その違いを少し見てみましょう。

化粧

化粧は、現在の自分の姿を少し誇張した自分の姿に飾り付けることです。 自分自身はそのまま、難しく言えば本質を損なわない範囲で、その上に何かを追加で飾り付けたものです。

変装

変装は、現在の自分の姿を完全に別のものの姿に飾り付けることです。 自分自身ではない、完全に別の何かとして飾り付けたものです。

この章で学ぶのは、化粧に該当するデコレーターパターンと、変装に該当するアダプターパターンです。最後に、これまでの二つのパターンとは異なり、複数のクラスを一つのクラスに単純にまとめるファサードパターンについて触れて終わりにします。

アダプターパターン = 変装

先ほど、オオカミは三匹のこぶたの家に入るために、おとなしい羊に変装しました。恐ろしい爪を白い粉で可愛い手のように変え、唸り声をおとなしい羊のように「メー」と真似しようとします。これをクラスで表現すると、非常に理解が容易になります。

class Wolf {
    public String getClaw() {
        return "Sharp Claw";
    }
    public String getGrowl() {
        return "Grrrrrrr";
    }
}
class WolfWantsToBeSheep implements Sheep {
    public Wolf wolf; // アダプティー (Adaptee)
    public String getHand() {
        return wolf.getClaw().replace("Sharp Claw", "White Hand");
    }
    public String getSound() {
        return wolf.getGrowl().replace("Grrrrrrr", "Baaaaaaa");
    }
}

これでオオカミは、羊が入れる場所ならどこでも行けるようになりました。羊しか入れない三匹のこぶたの家に入ってみましょう。

public void WelcomeToPigHouse(Sheep sheep);
WelcomeToPigHouse(new Sheep());
WelcomeToPigHouse(new WolfWantsToBeSheep(new Wolf()));

あるクラスや関数をクライアントと見なすと、クライアントは特定のターゲットインターフェースにのみ合うように実装されています。この制約のため、たとえそのクライアントで別のクラスを使用したいと思っても、そのクラスがターゲットインターフェースの実装体でなければ使用できません。上記の例のように、生まれたときからオオカミだったが、三匹のこぶたの家に行くためにはおとなしい羊にならなければならない状況のようなものです。一般的なビジネスにおいても、このようにあるクラスをクライアントの目的に合ったクラスとして使用しなければならないという突然の要求が発生することがあります。

オブジェクトアダプター

上記のオオカミと羊の例のように、アダプターパターンはアダプターというターゲットインターフェースに対する新しい実装クラスを生成し、その中にターゲットインターフェースに変装したい外部クラスをオブジェクトとして内部に持ちます。このように、既存のオブジェクトを一度別のインターフェースでラップした実装をアダプターと呼び、ラップされる元のオブジェクトを「アダプティー」と呼びます。アダプティーの元の関数やプロパティを活用して、ターゲットインターフェースの各関数を実装すればよいのです。

これをより具体的には**「オブジェクトアダプター」**と呼ぶのは、アダプターがアダプティーをオブジェクトとして持っているためです。これを私たちは「コンポジション」と学びましたね。以下のコードを見ると、AdapterAdapteeをオブジェクトとして持っています。クラス図が理解を少し助けてくれるでしょう。

public void Client(TargetInterface interface);

class Adapter implements TargetInterface {
    private Adaptee adaptee;
    // ... adapteeの関数を活用してTargetInterfaceの関数を実装します。
}
this.Client(new Adapter(new Adaptee()));

アダプティーはアダプターの助けを借りて、TargetInterfaceのみを使用するクライアントに注入可能になりました。

Class Diagram for understanding Adaptor Pattern

では、**「クラスアダプター」**とは何でしょうか? AdapterがAdapteeをオブジェクトの形で「コンポジション」(has)するのではなく、クラスの形で「継承」(extends)すればよいのです。

クラスアダプター

クラスアダプターはむしろ単純です。以下のコードとクラス図を見ると、オブジェクトアダプターと二つの違いがあります。

オブジェクトアダプターとクラスアダプターの違いを擬似コードで簡単に理解してみましょう。

class Adapter implements TargetInterface {
    private Adaptee adaptee;
    // ... adapteeの関数を活用してTargetInterfaceの関数を実装します。
}

Class Diagram of Object Adaptor

class Adapter extends Target, Adaptee { // 注:これは説明のための擬似コードです
    // ... adapteeの関数を活用してTargetの関数を拡張します。
}

Class Diagram of Class Adaptor

上記のコードを見て、ひるんだ方もいるかもしれませんが、Javaではクラスに対する多重継承をサポートしていません。クラスアダプターのコードを見ると、2つのクラスを1つのアダプタークラスで拡張して使用していることがわかりますが、一つは拡張したいターゲットのクラスで、もう一つは拡張する対象クラスであるアダプティーのクラスです。もちろん、このような形でのJavaのクラス多重継承はサポートされていないため、このロジックはJavaでは使用できません。また、この構造自体が柔軟性を損なうため、使用を推奨する方式でもありません。

多重アダプター

多重アダプターは、既存の単一のターゲットインターフェースだけでなく、複数のターゲットインターフェースすべてをサポートすることを意味します。一つのアダプティークラスを、こちらのインターフェースだけでなく、あちらのインターフェースでも使用したい場合、TargetOneInterfaceTargetTwoInterfaceを一つのアダプタークラスに接続し、両方のインターフェースのすべてを実装すればよいのです。オブジェクトアダプターではなくクラスアダプターであれば、2つのクラスTargetOne, TargetTwoを継承(extends)すればよいことになります。

Javaではクラスに対する多重継承はサポートしていませんが、インターフェースに対する多重継承はサポートしているため、implements A, Bのような文法は十分に利用可能になります。

public void ClientOne(TargetOneInterface interface1);
public void ClientAnother(TargetTwoInterface interface2);

class Adapter implements TargetOneInterface, TargetTwoInterface {
    private Adaptee adaptee;
    // ... adapteeの関数を活用してTargetOne/TwoInterfaceの関数をすべて実装します。
}

デコレーターパターン - 化粧

デコレーターパターンは、クラスに無数の追加機能を加えても、そのクラスは本来のクラスの機能を維持する「化粧」に該当します。デコレーターパターンをアダプターパターンの次に扱う理由は、実は原理がアダプター-アダプティーの概念と同じだからです。アダプターがAdapteeTargetInterfaceに**「変装」させたとしたら、デコレーターはDecorateeDecoratee自分自身に「化粧」**させる形になります。

class Adapter implements TargetInterface {
    private Adaptee adaptee;
}
class Decorator extends Decoratee { // DecoratorもDecorateeである
    private Decoratee decoratee; // 別のDecorateeをラップする
}

デコレーターパターンは一度だけ化粧するために使用されるわけではありません。自分自身に再帰的に化粧を施し続けることができます。どれほど多様なDecoratorADecoratorBを作成して飾り付けても、結局Decorateeクラスであるため、既存のクライアントは特に気にすることなく、以前と同じように使用すればよいのです。

デコレーターパターンは、DecoratorクラスがDecorateeをDecorateeとして化粧させるものです。 DecoratorはDecorateeを継承するため、それ自身もDecorateeになることができます。 したがって、Decoratorは再帰的にDecorateeの位置に置くことができ、無限に化粧を施すことができます。

class Decoratee {
    // ...
}
class Decorator extends Decoratee { // Decorateeから継承して型を維持する
    private Decoratee decoratee; // 実際のDecorateeインスタンスをラップする
    // ... decorateeの関数を活用して、より改善されたdecoratee関数に拡張します。
}

上記のような単純なコードでも、非常に基本的な飾り付けを望むだけであれば十分です。しかし、それでも以下のように**「抽象デコレーター」「具象デコレーター」**に分けることをお勧めする理由は、以下の利点があるためです。

「実装よりもインターフェースを使え」というデザインパターン第一原則を覚えていますか?実装ではなくインターフェース(または抽象クラス)の利点は、必要な具象クラスを付けたり外したりできる有用性と再利用性でした。例えば、具象デコレーターをリストやセットにまとめて管理したい場合、抽象デコレーター型のリストやセットを作成して使用できますね。

ファサードパターン - まとめ

最後に学ぶパターンはファサードパターンです。アダプターとデコレーターパターンは、それぞれ一つのアダプティーまたはデコレーティーを持つという共通点があり、違いはアダプターが他のクラスに「変装」するのに対し、デコレーターは同じデコレーター(実質的にはデコレーティー)に「化粧」を施すという点でした。この章でファサードパターンを扱うということは、これらとの共通点があるということでしょう。何が同じなのでしょうか?

ファサードパターンは、アダプター、デコレーターパターンと同じ共通点を持っています。アダプティー、デコレーティーのように活用するためのクラスを内部に持っています。ただし、アダプター、デコレーターがアダプティー、デコレーティーを一つずつしか持たなかったのに対し、ファサードは非常に多くのクラスを持ちます。そして、アダプターとデコレーターの違いが「変装」か「化粧」かという違いだったのに対し、ファサードはただそれ自体が新しいクラスになります。

アダプターとデコレーターパターンは、なりたいインターフェースやクラスを継承することで、なりたい姿を持っていましたが、ファサードパターンは、単に目的を達成するためのロジックを作るために、必要なオブジェクトを自由に詰め込むだけです。ある意味、アダプターとデコレーターを説明する際にファサードパターンに言及するのが適切かどうか疑問に思うかもしれませんが、理解を深めるのに役立つのではないかと思い含めてみました。

class Adapter implements TargetInterface {
    private Adaptee adaptee;
    // ... adapteeの関数を活用してTargetInterfaceの関数を実装します。
}
class Facade {
    private ClassA classA;
    private ClassB classB;
    private ClassC classC;
    // ... ClassA, B, Cを活用した新しい関数を作成します。
}

ファサードには、その後ろにextendsimplementsといったものは一切存在しません。単に複数のクラスを一つにまとめるクラスなのです。


いつも長い記事を読んでいると、最後のあたりで集中力が途切れてしまうことがあります。以下に3行要約をまとめてみました。


アダプターパターン

一つのクラス(アダプティー)を、別のクラス(ターゲットインターフェース)に**「変装」**させます。

class Adapter implements TargetInterface {
    private Adaptee adaptee;
    // ... adapteeの関数を活用してTargetInterfaceの関数を実装します。
}

デコレーターパターン

一つのクラス(デコレーティー)を、その一つのクラス(デコレーティー)として**「化粧」**します。

class Decorator extends Decoratee {
    private Decoratee decoratee;
    // ... decorateeの関数を活用して、より改善されたdecoratee関数に拡張します。
}

上記のサンプルコードは、理解を助けるためにシンプルなデコレータークラスとして記述しました。本文で説明した通り、抽象/具象デコレーターとして利用することをお勧めします。

ファサードパターン

複数のクラス一つの一つの異なるクラスまとめます

class Facade {
    private ClassA classA;
    private ClassB classB;
    private ClassC classC;
    // ... ClassA, B, Cを活用した新しい関数を作成します。
}

化粧で見抜くデコレーター、変装で見抜くアダプター
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
同じカテゴリーの関連記事
最新記事
LLMフィルターが奪う会話の筋肉とコミュニケーション様式
会話における無礼さを濾過し、洗練された回答を生成するLLMツールが日常化した現代において、私たちは本当に思慮深い会話をしているのだろうか?リアルタイムのコミュニケーションにおける数多くの失敗を通じて磨かれるべき会話能力が、外部ツールに依存することで退化している現象と、それがもたらす社会的な不安や世代間の行動様式の変化について考察する。
シニア採用における年俸交渉の最適なタイミングと戦略
年俸交渉は単なる数字の交換ではなく、心理的な駆け引きとタイミングが重要です。本稿では、企業側にとって、候補者が計算的な態度を取りがちな最終合格後よりも、採用プロセスの初期段階から段階的に交渉を進めることが、なぜより効率的であり、率直な情報の共有に繋がるのかを考察します。
法治主義の限界と人間の多様性
全ての人間の行為を単一の法体系で規制できるという信念は、傲慢であるかもしれない。この記事は、中世の階層的な統制から脱却し、現代の無限の自由を手に入れた人類が直面する法治主義の逆説と、多様性という名のもとに深化する社会的強制力と他者への悪魔化現象を鋭く分析する。
토스트 예시 메세지