構造型パターンは、既存のクラスをどのように組み立て、それらの相互作用を設計して、特定の機能目的を達成するかを解決するためのものです。構造型パターンは、多くの問題の解決を包含しています。例えば:拡張性(ファサード、コンポジット、プロキシ、デコレーター)、カプセル化(アダプター、ブリッジ)。
オブジェクトの生成問題が解決された後、オブジェクトの構成やオブジェクト間の依存関係が開発者の関心の焦点となります。なぜなら、オブジェクトの構造、継承、依存関係の設計が、後続のプログラムの保守性、コードの堅牢性、結合度などに影響を与えるからです。
アダプターパターン#
アダプターパターン(Adapter Pattern)は、構造型設計パターンの一種で、その核心的な役割は、あるクラスのインターフェースをクライアントが期待する別のインターフェースに変換することです。これにより、インターフェースが互換性がないために協調して動作できないクラス間の協力問題を解決します。これは現実の「電源コンバーター」に似ており、中間層インターフェースを介して変換を行い、元々不一致なコンポーネントが協調して動作できるようにします。
主要構成要素#
- ターゲットインターフェース(Target):クライアントが期待するインターフェースで、呼び出す必要があるメソッドを定義します。
- 被アダプタ(Adaptee):適応が必要なインターフェースで、既存のクラスまたはインターフェースですが、そのインターフェースはターゲットと互換性がありません。
- アダプター(Adapter):ターゲットと被アダプタを接続する中間層で、継承またはコンポジションを通じてインターフェース変換を実現します。
実装#
-
クラスアダプター(継承方式):アダプターは被アダプタクラスを継承し、ターゲットインターフェースを実装します。柔軟性は低い
// ターゲットインターフェース interface Target { void request(); } // 被アダプタ class Adaptee { void specificRequest() { /*...*/ } } // アダプター class Adapter extends Adaptee implements Target { @Override void request() { specificRequest(); } }
-
オブジェクトアダプター(コンポジション方式):アダプターは被アダプタのオブジェクトインスタンスを保持し、委任を通じてメソッドを呼び出します。
class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override void request() { adaptee.specificRequest(); } }
-
インターフェースアダプター(デフォルトアダプター):ターゲットインターフェースのメソッドが多すぎて、クライアントが部分的な実装のみを必要とする問題を解決します。これは抽象クラスを中間層として使用し、インターフェースメソッドにデフォルト実装(通常は空または単純なロジック)を提供し、サブクラスが必要に応じて特定のメソッドをオーバーライドできるようにします。これにより、すべてのインターフェースメソッドを強制的に実装する冗長性を回避します。
// ターゲットインターフェース public interface MultiFunction { void print(); void scan(); void copy(); } // 抽象アダプタークラス public abstract class DefaultAdapter implements MultiFunction { @Override public void print() {} // デフォルトの空実装 @Override public void scan() {} @Override public void copy() {} } // 具体的なアダプタークラス public class PrinterAdapter extends DefaultAdapter { @Override public void print() { System.out.println("印刷機能が有効になりました"); } // scan()とcopy()はオーバーライドする必要はなく、デフォルトの空ロジックをそのまま使用 } // 呼び出し public class Client { public static void main(String[] args) { MultiFunction device = new PrinterAdapter(); device.print(); // 出力:印刷機能が有効になりました } }
ファサードパターン#
ファサードパターン(Facade Pattern)は、構造型設計パターンの一種で、その核心的な目標は、複雑なサブシステムに対して統一された簡素化されたインターフェースを提供し、システム内部の複雑さを隠し、クライアントとサブシステム間の結合度を低下させ、システムの使いやすさと保守性を向上させることです。
主要構成要素#
** ファサードクラス(Facade)** を通じて複数のサブシステムの相互作用ロジックをカプセル化し、高レベルのインターフェースを提供します。クライアントはファサードクラスとだけ対話し、複雑なサブシステムコンポーネントを直接呼び出す必要はありません。
- ファサードロール(Facade):サブシステムの呼び出しを統一的に調整し、外部に簡素化されたインターフェースを提供します。
- サブシステムロール(Subsystem Classes):具体的な機能を実装するモジュール。
- クライアント(Client)、
ブリッジパターン#
2. ブリッジパターン — Graphic Design Patterns
矩形、円、楕円、正方形を描画する必要があると仮定すると、少なくとも 4 つの形状クラスが必要ですが、描画する図形が異なる色(赤、緑、青など)を持つ必要がある場合、少なくとも次の 2 つの設計案があります:
- 第一の設計案は、各形状に対してさまざまな色のバージョンを提供することです。
- 第二の設計案は、実際のニーズに応じて形状と色を組み合わせることです。
ブリッジパターンは、継承関係を関連関係に変換し、クラス間の結合度を低下させ、コードの記述量を減少させます。抽象部分とその実装部分を分離し、両方が独立して変化できるようにします。
ブリッジパターンの構造#
実装例#
ブリッジパターン(Bridge Pattern)-(最もわかりやすい例)_ブリッジパターンの実例 - CSDN ブログ
コンポジットパターン#
この木を大きなコンテナとして理解し、その中には多くのメンバーオブジェクトが含まれます。これらのオブジェクトはコンテナオブジェクトでもあり、葉オブジェクトでもあります。しかし、コンテナオブジェクトと葉オブジェクトの機能上の違いにより、使用する際にコンテナオブジェクトと葉オブジェクトを区別する必要がありますが、これによりクライアントに不必要な手間がかかります。クライアントは、コンテナオブジェクトと葉オブジェクトを一貫して扱いたいと考えています。これがコンポジットパターンの設計動機です:コンポジットパターンは、コンテナオブジェクトと葉オブジェクトを再帰的に組み合わせる方法を定義し、クライアントが使用する際に区別する必要がなく、一貫して処理できるようにします。
コンポジットパターン(Composite Pattern)は、全体 - 部分(Part-Whole)パターンとも呼ばれ、単一のオブジェクト(葉ノード)と組み合わせオブジェクト(枝ノード)を同じインターフェースで表現することにより、クライアントが単一のオブジェクトと組み合わせオブジェクトを一貫して使用できるようにすることを目的としています。
コンポジットパターンの構造#
コンポジットパターンは、透明なコンポジットパターンと安全なコンポジットパターンに分かれます。この方式では、抽象コンポーネントがすべてのサブクラスのすべてのメソッドを宣言しているため、クライアントは葉オブジェクトと枝オブジェクトを区別する必要がなく、クライアントにとっては透明です。しかし、その欠点は、葉コンポーネントは本来 Add ()、Remove () および GetChild () メソッドを持たないのに、これらを実装しなければならない(空実装または例外をスローする)ため、安全性の問題を引き起こすことです。
透明モードの実装#
27 デザインパターン —— コンポジットパターン(詳細版) - 知乎
public class CompositePattern {
public static void main(String[] args) {
Component c0 = new Composite();
Component c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
//抽象コンポーネント
interface Component {
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public void operation();
}
//葉コンポーネント
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
public void add(Component c) {
}
public void remove(Component c) {
}
public Component getChild(int i) {
return null;
}
public void operation() {
System.out.println("葉" + name + ":アクセスされました!");
}
}
//枝コンポーネント
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Object obj : children) {
((Component) obj).operation();
}
}
}
安全モードの実装#
葉と枝には異なるインターフェースがあるため、クライアントは呼び出す際に葉オブジェクトと枝オブジェクトの存在を知っている必要があり、透明性が失われます。
Leaf クラスは、add、remove、および getChild のような自分にとって無用なメソッドを実装する必要がなく、これにより設計がより合理的で理解しやすくなります。また、このような設計は潜在的なエラーを回避します。
プロキシパターン#
オブジェクトへのアクセスを制御し、プロキシオブジェクトを導入してそのアクセスを管理します。プロキシパターンは、元のオブジェクトのインターフェースを変更することなく、追加の機能やアクセス制御を追加することを許可します。
主要構成要素#
- サブジェクト(目標インターフェース):
- プロキシクラスと実際のテーマクラスの共通インターフェースを定義します。
- これにより、クライアントはコードを変更することなく、プロキシまたは実際のテーマオブジェクトを使用できます。
- リアルサブジェクト(実際のテーマ):
Subject
インターフェースの具体的なビジネスロジックを実装します。- これはクライアントが実際にアクセスする必要があるオブジェクトです。
- プロキシ(代理):
Subject
インターフェースも実装し、Real Subject
への参照を保持します。- プロキシクラスは、クライアントと実際のテーマの間で調整を行い、リクエストの前後に追加の操作を実行することがあります。
静的プロキシ#
プロキシは特定のビジネス実装クラスにのみサービスを提供できます。
1. 目標インターフェースの定義
まず、プロキシする必要があるメソッドを宣言するインターフェースを定義します。
JAVApublic interface Subject {
void request();
}
2. 実際のテーマクラスの実装
上記のインターフェースを実装する具体的なクラスを作成し、そのクラスには実際のビジネスロジックが含まれます。
JAVApublic class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: リクエストを処理中");
}
}
3. プロキシクラスの作成
プロキシクラスも同じインターフェースを実装し、実際のテーマオブジェクトへの参照を保持します。プロキシクラスは、実際のテーマのメソッドを呼び出す前または後に追加の機能を追加できます。
JAVApublic class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
// 前処理操作を実行
System.out.println("Proxy: 前処理操作を実行中");
realSubject.request();
// 後処理操作を実行
System.out.println("Proxy: 後処理操作を実行中");
}
}
4. クライアントコードの作成
クライアントはインターフェースを介してプロキシクラスまたは実際のテーマオブジェクトと対話し、直接呼び出すかプロキシを介して呼び出すかを知る必要はありません。
JAVApublic class Client {
public static void main(String[] args) {
// 実際のテーマオブジェクトを作成
Subject realSubject = new RealSubject();
// プロキシオブジェクトを作成し、実際のテーマをプロキシに渡す
Subject proxy = new Proxy(realSubject);
// プロキシのメソッドを呼び出す
proxy.request();
}
}
動的プロキシ#
動的プロキシはリフレクションのメカニズムを使用して、実行時にインターフェースクラスのインスタンスを作成します。
//ビジネスインターフェース
interface DateService {
void add();
void del();
}
// 具体的なビジネスクラス
class DateServiceImplA implements DateService {
@Override
public void add() {
System.out.println("成功しました!");
}
@Override
public void del() {
System.out.println("削除に成功しました!");
}
}
// プロキシ
class ProxyInvocationHandler implements InvocationHandler {
private DateService service;
public ProxyInvocationHandler(DateService service) {
this.service = service;
}
// this.getClass().getClassLoader() 最初の引数はクラスローダーで、プロキシクラスをロードします。
// service.getClass().getInterfaces() 被プロキシクラスが実装しているすべてのインターフェースで、プロキシオブジェクトがターゲットオブジェクトと同じインターフェースビューを提供できるようにします。
// this 現在のインスタンス(すなわち、InvocationHandlerインターフェースを実装するオブジェクト)で、プロキシオブジェクトのメソッド呼び出しを処理します。
public Object getDateServiceProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
}
/* invoke メソッド:
オーバーライドされたinvokeメソッドは、プロキシオブジェクト上で任意のメソッドが呼び出されるとトリガーされます。3つの引数を受け取ります:
Object proxy:プロキシオブジェクト自体を表します。
Method method:呼び出されているターゲットメソッドを表します。
Object[] args:ターゲットメソッドに渡される引数リスト。
メソッド本体内では、最初にmethod.invoke(service, args);を使用してターゲットオブジェクトの実際のメソッドを呼び出し、結果をresult変数に保存します。
次に、プロキシオブジェクトが特定のメソッドを実行し、そのメソッド名と戻り値を記録したログ情報を印刷します。
最後に、ターゲットメソッドの実行結果を返します。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var result = method.invoke(service, args); // serviceがメソッドを呼び出し、メソッドの戻り値
System.out.println(proxy.getClass().getName() + "プロキシクラスが" + method.getName() + "メソッドを実行し、戻り値は" + result + "、ログを記録しました!");
return result;
}
}
//クライアント
public class Test {
public static void main(String[] args) {
DateService serviceA = new DateServiceImplA();
DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
serviceProxy.add();
serviceProxy.del();
}
}
デコレーターパターン#
デコレーターパターン(Decorator Pattern)-(最もわかりやすい例)_デコレーターパターンの使用例 - CSDN ブログ
デコレーターパターン(Decorator)は、オブジェクトに動的に追加の責任を付与するもので、機能を追加する際にはデコレーターパターンがサブクラスを生成するよりも柔軟です。デコレーターパターンは構造型パターンに属し、既存のクラスのラッパーとして機能します。
デコレーターパターンの主要な役割には以下が含まれます:
-
コンポーネント(抽象コンポーネント):装飾されるオブジェクトとデコレーターが共通のインターフェースを定義します。オブジェクトインターフェースを定義し、これらのオブジェクトに動的に責任を追加できます。
-
具体的なコンポーネント(ConcreteComponent):Component インターフェースを実装し、装飾されるオブジェクトです。具体的なオブジェクトを定義し、このオブジェクトに追加の責任を付与できます。
-
デコレーター(抽象デコレーター):Component オブジェクトの参照を保持し、Component と同じインターフェースを持つメソッドを定義して一貫性を保ちます。
-
具体的なデコレーター(ConcreteDecorator):デコレーターが定義したメソッドを実装し、装飾されるオブジェクトのメソッドを呼び出す前後に追加の操作を実行できます。
具体的な実装#
// 抽象コンポーネント
public interface INoodles {
public void cook();
}
// 具体的なコンポーネント
public class Noodles implements INoodles {
@Override
public void cook() {
System.out.println("の麺");
}
}
// 抽象デコレータクラス
public abstract class NoodlesDecorator implements INoodles {
private INoodles noodles; // INoodlesの参照を追加
public NoodlesDecorator(INoodles noodles){ // コンストラクタを通じてINoodlesを設定
this.noodles = noodles;
}
@Override
public void cook() {
if (noodles!=null){
noodles.cook();
}
}
}
// 具体的なデコレータクラス
public class EggDecorator extends NoodlesDecorator {
public EggDecorator(INoodles noodles) {
super(noodles);
}
/**
* 親クラスのcookメソッドをオーバーライドし、自分の実装を追加し、親クラスのcookメソッドを呼び出します。このcookメソッドは本クラスのコンストラクタ
* EggDecorator(INoodles noodles)を通じて渡されたnoodlesのcook操作です。
*/
@Override
public void cook() {
System.out.println("卵を追加しました");
super.cook();
}
}
// 具体的なデコレータクラス
public class BeefDecorator extends NoodlesDecorator {
public BeefDecorator(INoodles noodles) {
super(noodles);
}
@Override
public void cook() {
System.out.println("牛肉を1斤追加しました");
super.cook();
}
}
public class Client {
public static void main(String[] args) {
INoodles noodles1 = new Noodles();
INoodles noodles1WithEgg = new EggDecorator(noodles1);
noodles1WithEgg.cook();
}
}
デコレーターパターンとプロキシパターンの違い#
- 意図が異なる:デコレーターパターンはオブジェクトの機能を強化することに焦点を当てていますが、プロキシパターンはオブジェクトへのアクセスを制御することに重点を置いています。
- 実装の詳細:デコレーターパターンでは、デコレーターと装飾されるオブジェクトは同じインターフェースを実装する必要があります。これにより、デコレーションチェーン内のオブジェクトが相互に置き換え可能になります。一方、プロキシパターンでは、プロキシクラスとターゲットオブジェクトも同じインターフェースを実装することがよくありますが、これは主に一貫性を保つためであり、必須条件ではありません。
- 設計上の考慮:デコレーターパターンを使用する際、設計者は既存のコードを変更せずに機能を拡張する方法をより多く考慮します。一方、プロキシパターンを使用する際は、リソースへのアクセスを保護し、管理する方法をより多く考慮します。