デザインパターンの紹介、パート2:四大奇書の再考

デザインパターンを紹介するこの3部構成のシリーズのパート1では、デザインパターン:再利用可能なオブジェクト指向設計の要素について説明しました。この古典は、エーリヒガンマ、リチャードヘルム、ラルフジョンソン、ジョンブリシディーズによって書かれました。これらは、まとめて4人のギャングとして知られていました。ほとんどの読者が知っているように、デザインパターンは、パート1:作成、構造、および動作で説明されているカテゴリに適合する23のソフトウェアデザインパターンを示します。

JavaWorldのデザインパターン

David GearyのJavaデザインパターンシリーズは、JavaコードのGang ofFourパターンの多くを巧みに紹介しています。

デザインパターンはソフトウェア開発者にとっては標準的な読み物ですが、多くの新しいプログラマーはその参照形式と範囲に挑戦しています。 23のパターンのそれぞれは、13のセクションで構成されるテンプレート形式で詳細に説明されており、消化するのが大変です。新しいJava開発者にとってのもう1つの課題は、Gang of Fourパターンがオブジェクト指向プログラミングから生まれ、JavaコードではなくC ++とSmalltalkに基づいた例があることです。

このチュートリアルでは、Java開発者の観点から、一般的に使用される2つのパターン(戦略と訪問者)を展開します。ストラテジーはかなり単純なパターンであり、一般的にGoFデザインパターンで足を濡らす方法の例として役立ちます。訪問者はより複雑で中程度の範囲です。まず、Visitorパターンの重要な部分であるダブルディスパッチメカニズムをわかりやすく説明する例から始めます。次に、コンパイラのユースケースでVisitorパターンを示します。

ここでの私の例に従うと、他のGoFパターンを自分で調べて使用するのに役立つはずです。さらに、Gang of Fourの本を最大限に活用するためのヒントを提供し、ソフトウェア開発でデザインパターンを使用することの批評の要約で締めくくります。その議論は、プログラミングに不慣れな開発者に特に関係があるかもしれません。

開梱戦略

戦略パターンを使用すると、このような、テキスト合成、またはレイアウト管理をソートするために使用されるものなどのアルゴリズムの家族を定義できます。Strategyでは、各アルゴリズムを独自のクラスにカプセル化し、互換性を持たせることもできます。カプセル化された各アルゴリズムは、戦略と呼ばれます。実行時に、クライアントはその要件に適したアルゴリズムを選択します。

クライアントとは何ですか?

クライアントは、デザインパターンと相互作用ソフトウェアの任意の部分です。通常はオブジェクトですが、クライアントはアプリケーションのpublic static void main(String[] args)メソッド内のコードである場合もあります。

オブジェクトのスキンや外観の変更に焦点を当てるデコレータパターンとは異なり、ストラテジーはオブジェクトの内臓の変更、つまり変更可能な動作に焦点を当てています。ストラテジーでは、条件ブランチを独自のストラテジークラスに移動することで、複数の条件ステートメントの使用を回避できます。これらのクラスは、クライアントが特定の戦略と対話するために参照および使用する抽象的なスーパークラスから派生することがよくあります。

抽象的な観点から、戦略が必要Strategy、との種類。ConcreteStrategyxContext

戦略

Strategyサポートされているすべてのアルゴリズムに共通のインターフェースを提供します。リスト1はStrategyインターフェースを示しています。

リスト1.void execute(int x)は、すべての具体的な戦略によって実装する必要があります

public interface Strategy { public void execute(int x); }

具体的な戦略が共通のデータでパラメーター化されていない場合は、Javaのinterface機能を介してそれらを実装できます。それらがパラメーター化されている場合は、代わりに抽象クラスを宣言します。たとえば、テキストの配置を右揃え、中央揃え、および両端揃えにする戦略は、テキストの配置を実行するの概念を共有しています。したがって、このを抽象クラスで宣言します。

ConcreteStrategy x

それぞれが共通のインターフェースを実装し、アルゴリズムの実装を提供します。リスト2は、特定の具体的な戦略を説明するためにリスト1のインターフェースを実装しています。ConcreteStrategyxStrategy

リスト2.ConcreteStrategyAは1つのアルゴリズムを実行します

public class ConcreteStrategyA implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy A: x = "+x); } }

void execute(int x)リスト2のメソッドは、特定の戦略を識別します。このメソッドは、特定の種類の並べ替えアルゴリズム(バブルソート、挿入ソート、クイックソートなど)や特定の種類のレイアウトマネージャー(フローレイアウト、境界線レイアウトなど)など、より便利なものの抽象化と考えてください。グリッドレイアウト)。

リスト3は、2番目のStrategy実装を示しています。

リスト3.ConcreteStrategyBは別のアルゴリズムを実行します

public class ConcreteStrategyB implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy B: x = "+x); } }

環境

Context具体的な戦略が呼び出されるコンテキストを提供します。リスト2とリスト3は、メソッドパラメーターを介してコンテキストからストラテジーに渡されるデータを示しています。一般的な戦略インターフェースはすべての具体的な戦略で共有されているため、一部の戦略ではすべてのパラメーターが必要なわけではありません。無駄なパラメーターを回避するために(特に、いくつかの具体的な戦略にのみ多くの異なる種類の引数を渡す場合)、代わりにコンテキストへの参照を渡すことができます。

メソッドへのコンテキスト参照を渡す代わりに、それを抽象クラスに格納して、メソッド呼び出しをパラメーターなしにすることができます。ただし、コンテキストは、コンテキストデータに均一にアクセスするためのコントラクトを含むより広範なインターフェイスを指定する必要があります。その結果、リスト4に示すように、戦略とそのコンテキストの間の緊密な結合が実現します。

リスト4.コンテキストはConcreteStrategyxインスタンスで構成されています

class Context { private Strategy strategy; public Context(Strategy strategy) { setStrategy(strategy); } public void executeStrategy(int x) { strategy.execute(x); } public void setStrategy(Strategy strategy) { this.strategy = strategy; } }

Contextリスト4のクラスは、作成時にストラテジーを格納し、その後ストラテジーを変更するメソッドを提供し、現在のストラテジーを実行する別のメソッドを提供します。コンストラクタへの戦略を通過させる以外は、このパターンは、そのjava.awtの.Containerクラスに見ることができるvoid setLayout(LayoutManager mgr)void doLayout()方法レイアウトマネージャストラテジを指定して実行します。

StrategyDemo

以前のタイプを示すためにクライアントが必要です。リスト5は、StrategyDemoクライアント・クラスを示しています。

リスト5.StrategyDemo

public class StrategyDemo { public static void main(String[] args) { Context context = new Context(new ConcreteStrategyA()); context.executeStrategy(1); context.setStrategy(new ConcreteStrategyB()); context.executeStrategy(2); } }

具体的な戦略はContext、コンテキストが作成されるときにインスタンスに関連付けられます。その後、コンテキストメソッド呼び出しを介してストラテジーを変更できます。

これらのクラスをコンパイルして実行StrategyDemoすると、次の出力が表示されます。

executing strategy A: x = 1 executing strategy B: x = 2

ビジターパターンの再考

ビジターは、デザインパターンに表示される最終的なソフトウェアデザインパターンです。この行動パターンはアルファベット順の理由で本の最後に示されていますが、複雑さのために最後に来るべきだと考える人もいます。ビジターの初心者は、このソフトウェアデザインパターンに苦労することがよくあります。

デザインパターンで説明されているように、訪問者はクラスを変更せずにクラスに操作を追加できます。これは、いわゆるダブルディスパッチ手法によって促進されるちょっとした魔法です。ビジターパターンを理解するために、最初にダブルディスパッチを消化する必要があります。

ダブルディスパッチとは?

Javaおよび他の多くの言語動的ディスパッチと呼ばれる手法を介してポリモーフィズム(多くの形状)をサポートします。この手法では、実行時にメッセージが特定のコードシーケンスにマップされます。動的ディスパッチは、単一ディスパッチまたは複数ディスパッチのいずれかに分類されます。

  • シングルディスパッチ:各クラスが同じメソッドを実装する(つまり、各サブクラスが前のクラスのメソッドのバージョンをオーバーライドする)クラス階層が与えられ、これらのクラスの1つのインスタンスが割り当てられた変数が与えられると、タイプを把握できます。実行時のみ。たとえば、各クラスがメソッドを実装するとしprint()ます。これらのクラスの1つが実行時にインスタンス化され、その変数が変数に割り当てられているとしますa。 Javaコンパイラがを検出するとa.print();aの型にprint()メソッドが含まれていることのみを確認できます。どのメソッドを呼び出すかはわかりません。実行時に、仮想マシンは変数内の参照を調べますa適切なメソッドを呼び出すために、実際のタイプを把握します。実装が単一のタイプ(インスタンスのタイプ)に基づいているこの状況は、単一ディスパッチと呼ばれます。
  • 多重ディスパッチ:単一の引数がその名前のどのメソッドを呼び出すかを決定する単一のディスパッチとは異なり、多重ディスパッチはそのすべての引数を使用します。つまり、動的ディスパッチを一般化して、2つ以上のオブジェクトを処理します。(シングルディスパッチの引数は、通常、呼び出されるメソッド名の左側にピリオド区切り記号を付けて指定されます(a inなどa.print())。)

最後に、ダブルディスパッチは、2つのオブジェクトの実行時型が呼び出しに関与する多重ディスパッチの特殊なケースです。Javaはシングルディスパッチをサポートしていますが、ダブルディスパッチを直接サポートしていません。しかし、それをシミュレートすることはできます。

ダブルディスパッチに頼りすぎていませんか?

ブロガーのDerekGreerは、ダブルディスパッチの使用は設計上の問題を示している可能性があり、アプリケーションの保守性に影響を与える可能性があると考えています。詳細については、Greerの「ダブルディスパッチはコードの臭いです」ブログ投稿と関連するコメントをお読みください。

Javaコードでのダブルディスパッチのシミュレーション

ダブルディスパッチに関するウィキペディアのエントリは、関数のオーバーロード以上のものであることを示すC ++ベースの例を提供します。リスト6に、同等のJavaを示します。

リスト6.Javaコードでのダブルディスパッチ

public class DDDemo { public static void main(String[] args) { Asteroid theAsteroid = new Asteroid(); SpaceShip theSpaceShip = new SpaceShip(); ApolloSpacecraft theApolloSpacecraft = new ApolloSpacecraft(); theAsteroid.collideWith(theSpaceShip); theAsteroid.collideWith(theApolloSpacecraft); System.out.println(); ExplodingAsteroid theExplodingAsteroid = new ExplodingAsteroid(); theExplodingAsteroid.collideWith(theSpaceShip); theExplodingAsteroid.collideWith(theApolloSpacecraft); System.out.println(); Asteroid theAsteroidReference = theExplodingAsteroid; theAsteroidReference.collideWith(theSpaceShip); theAsteroidReference.collideWith(theApolloSpacecraft); System.out.println(); SpaceShip theSpaceShipReference = theApolloSpacecraft; theAsteroid.collideWith(theSpaceShipReference); theAsteroidReference.collideWith(theSpaceShipReference); System.out.println(); theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference); } } class SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class ApolloSpacecraft extends SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class Asteroid { void collideWith(SpaceShip s) { System.out.println("Asteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("Asteroid hit an ApolloSpacecraft"); } } class ExplodingAsteroid extends Asteroid { void collideWith(SpaceShip s) { System.out.println("ExplodingAsteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("ExplodingAsteroid hit an ApolloSpacecraft"); } }

Listing 6 follows its C++ counterpart as closely as possible. The final four lines in the main() method along with the void collideWith(Asteroid inAsteroid) methods in SpaceShip and ApolloSpacecraft demonstrate and simulate double dispatch.

Consider the following excerpt from the end of main():

theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference);

The third and fourth lines use single dispatch to figure out the correct collideWith() method (in SpaceShip or ApolloSpacecraft) to invoke. This decision is made by the virtual machine based on the type of the reference stored in theSpaceShipReference.

内からcollideWith()inAsteroid.collideWith(this);単一のディスパッチを使用して、目的のメソッドを含む正しいクラス(AsteroidまたはExplodingAsteroid)を見つけますcollideWith()AsteroidExplodingAsteroidオーバーロードのため、collideWith()引数のタイプthisSpaceShipまたはApolloSpacecraft)はcollideWith()、呼び出す正しいメソッドを区別するために使用されます。

そしてそれで、ダブルディスパッチを実現しました。要約すると、最初collideWith()SpaceShipまたはを呼び出しApolloSpacecraft、次にその引数を使用して、またはthiscollideWith()メソッドの1つを呼び出しました。AsteroidExplodingAsteroid

を実行DDDemoすると、次の出力が表示されます。