パターンや設計パターンは開発者の仕事の中で見落とされがちな部分であり、そのためコードの保守や新しい要件への適応が困難になります。それが何なのか、そして JDK でどのように使用されるのかを確認することをお勧めします。当然のことながら、基本的なパターンはすべて、何らかの形で長い間私たちの周りにありました。このレビューでそれらを見てみましょう。
コンテンツ:
つまり、環境 (オペレーティング システム) に応じて、互換性のある要素を作成する特定のファクトリーを受け取ります。他の人を通じて作成するアプローチの代わりに、「プロトタイプ」パターンを使用できます。その本質は単純です - 新しいオブジェクトは、既存のオブジェクトのイメージと類似性の中で作成されます。彼らのプロトタイプによると。Java では、誰もがこのパターンに遭遇したことがあります。これはインターフェイスの使用です
詳細については、記事「Java AWT のパターン」を参照してください。構造パターンの中でも注目したいのが「ファサード」パターンです。その本質は、この API の背後にあるライブラリ/フレームワークを使用する複雑さを、便利で簡潔なインターフェイスの背後に隠すことです。たとえば、例として JPA の JSF または EntityManager を使用できます。他に「フライウェイト」と呼ばれるパターンもあります。その本質は、異なるオブジェクトが同じ状態を持つ場合、それを一般化して各オブジェクトではなく 1 か所に保存できることです。そして、各オブジェクトが共通部分を参照できるようになり、ストレージのメモリコストが削減されます。このパターンには、多くの場合、オブジェクトの事前キャッシュまたはプールの維持が含まれます。興味深いことに、私たちはこのパターンも最初から知っています。
同じ類推により、文字列のプールをここに含めることができます。このトピックに関する記事「フライウェイト デザイン パターン」を参照してください。
テンプレート
求人で最も一般的な要件の 1 つは「パターンの知識」です。まず第一に、「デザインパターンとは何ですか?」という単純な質問に答えることが重要です。パターンは英語から「テンプレート」と訳されます。つまり、これは私たちが何かを行う際の特定のパターンです。プログラミングでも同様です。一般的な問題を解決するための確立されたベスト プラクティスとアプローチがいくつかあります。すべてのプログラマーはアーキテクトです。少数のクラス、あるいは 1 つのクラスしか作成しない場合でも、変化する要件の下でコードがどれだけ長く存続できるか、他の人が使用するのにどれだけ便利かは、あなた次第です。ここでテンプレートの知識が役に立ちます。なぜなら... これにより、コードを書き直さずにコードを記述する最適な方法をすぐに理解できるようになります。ご存知のとおり、プログラマーは怠け者で、何度もやり直すよりも、すぐにうまく書くほうが簡単です) パターンもアルゴリズムに似ているように見えるかもしれません。しかし、彼らには違いがあります。アルゴリズムは、必要なアクションを記述する特定のステップで構成されます。パターンはアプローチを説明するだけであり、実装手順は説明しません。模様が違うので… さまざまな問題を解決します。通常、次のカテゴリが区別されます。-
原動力
これらのパターンは、オブジェクト作成を柔軟にするという問題を解決します。
-
構造的
これらのパターンは、オブジェクト間の接続を効果的に構築するという問題を解決します。
-
行動的
これらのパターンは、オブジェクト間の効果的な相互作用の問題を解決します。
創作パターン
オブジェクトのライフサイクルの始まり、つまりオブジェクトの作成から始めましょう。生成テンプレートはオブジェクトをより便利に作成するのに役立ち、このプロセスに柔軟性をもたらします。最も有名なものの 1 つは「ビルダー」です。このパターンを使用すると、複雑なオブジェクトを段階的に作成できます。Java での最も有名な例は次のとおりですStringBuilder
。
class Main {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("Hello");
builder.append(',');
builder.append("World!");
System.out.println(builder.toString());
}
}
オブジェクトを作成するもう 1 つのよく知られたアプローチは、作成を別のメソッドに移動することです。このメソッドは、いわばオブジェクト ファクトリになります。このパターンが「ファクトリーメソッド」と呼ばれる理由です。たとえば Java では、その効果は クラス で確認できますjava.util.Calendar
。クラス自体はCalendar
抽象クラスであり、それを作成するにはメソッドが使用されますgetInstance
。
import java.util.*;
class Main {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getTime());
System.out.println(calendar.getClass().getCanonicalName());
}
}
これは多くの場合、オブジェクト作成の背後にあるロジックが複雑になる可能性があることが原因です。たとえば、上記のケースでは、基本クラスにアクセスしCalendar
、クラスが作成されますGregorianCalendar
。コンストラクターを見ると、条件に応じて異なる実装が作成されていることがわかりますCalendar
。しかし、場合によっては、1 つのファクトリーメソッドだけでは十分ではありません。場合によっては、異なるオブジェクトを作成して互いに適合させることが必要になることがあります。これには別のテンプレート「 Abstract Factory 」が役立ちます。そして、さまざまな工場を 1 か所に作成する必要があります。同時に、実装の詳細は重要ではないという利点もあります。特定の工場を入手するかどうかは関係ありません。重要なことは、適切な実装を作成することです。超例:
java.lang.Cloneable
。
class Main {
public static void main(String[] args) {
class CloneObject implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return new CloneObject();
}
}
CloneObject obj = new CloneObject();
try {
CloneObject pattern = (CloneObject) obj.clone();
} catch (CloneNotSupportedException e) {
//Do something
}
}
}
ご覧のとおり、呼び出し元はclone
. つまり、プロトタイプに基づいてオブジェクトを作成するのは、オブジェクト自体の責任です。これは、ユーザーをテンプレート オブジェクトの実装に結び付けないため便利です。さて、このリストの最後にあるのは「Singleton」パターンです。その目的は単純で、アプリケーション全体にオブジェクトの単一インスタンスを提供することです。このパターンは、マルチスレッドの問題を示すことが多いため、興味深いものです。さらに詳しく知りたい場合は、次の記事をご覧ください。
構造パターン
オブジェクトを作成することで、それがより明確になりました。今こそ、構造パターンを検討する時期です。彼らの目標は、サポートしやすいクラス階層とその関係を構築することです。最初のよく知られたパターンの 1 つは「代理」(代理人) です。プロキシには実際のオブジェクトと同じインターフェイスがあるため、クライアントがプロキシ経由で動作するか直接動作するかに違いはありません。最も単純な例はjava.lang.reflect.Proxyです。import java.util.*;
import java.lang.reflect.*;
class Main {
public static void main(String[] arguments) {
final Map<String, String> original = new HashMap<>();
InvocationHandler proxy = (obj, method, args) -> {
System.out.println("Invoked: " + method.getName());
return method.invoke(original, args);
};
Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
original.getClass().getClassLoader(),
original.getClass().getInterfaces(),
proxy);
proxyInstance.put("key", "value");
System.out.println(proxyInstance.get("key"));
}
}
ご覧のとおり、この例ではオリジナルがあり、これはHashMap
インターフェイスを実装するものですMap
。HashMap
次に、クライアント部分の元のプロキシを置き換えるプロキシを作成します。プロキシはput
およびメソッドを呼び出しget
、呼び出し中に独自のロジックを追加します。ご覧のとおり、パターン内の相互作用はインターフェイスを通じて発生します。しかし、代替品では不十分な場合もあります。そして、「Decorator」パターンを使用できます。デコレータはラッパーまたはラッパーとも呼ばれます。プロキシとデコレータは非常に似ていますが、例を見ると違いがわかります。
import java.util.*;
class Main {
public static void main(String[] arguments) {
List<String> list = new ArrayList<>();
List<String> decorated = Collections.checkedList(list, String.class);
decorated.add("2");
list.add("3");
System.out.println(decorated);
}
}
プロキシとは異なり、デコレータは入力として渡されたものを自身でラップします。プロキシは、プロキシする必要があるものを受け入れることができ、また、プロキシされたオブジェクトの存続期間を管理することもできます (たとえば、プロキシされたオブジェクトを作成する)。もう 1 つの興味深いパターン、「アダプター」があります。これはデコレータに似ています。デコレータは 1 つのオブジェクトを入力として受け取り、このオブジェクトのラッパーを返します。違いは、目的は機能を変更することではなく、あるインターフェイスを別のインターフェイスに適応させることであるということです。Java にはこれに関する非常に明確な例があります。
import java.util.*;
class Main {
public static void main(String[] arguments) {
String[] array = {"One", "Two", "Three"};
List<String> strings = Arrays.asList(array);
strings.set(0, "1");
System.out.println(Arrays.toString(array));
}
}
入力には配列があります。次に、配列をインターフェイスに接続するアダプターを作成しますList
。これを操作するときは、実際には配列を操作することになります。したがって、要素を追加しても機能しません。元の配列は変更できません。この場合、 が得られますUnsupportedOperationException
。クラス構造を開発するための次の興味深いアプローチは、複合パターンです。興味深いのは、1 つのインターフェイスを使用する特定の要素セットが特定のツリー状の階層に配置されているという点です。親要素のメソッドを呼び出すと、必要なすべての子要素でこのメソッドが呼び出されます。このパターンの主な例は UI (java.awt または JSF) です。
import java.awt.*;
class Main {
public static void main(String[] arguments) {
Container container = new Container();
Component component = new java.awt.Component(){};
System.out.println(component.getComponentOrientation().isLeftToRight());
container.add(component);
container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
System.out.println(component.getComponentOrientation().isLeftToRight());
}
}
ご覧のとおり、コンテナにコンポーネントが追加されました。そして、コンテナにコンポーネントの新しい方向を適用するように依頼しました。そしてコンテナは、どのコンポーネントで構成されているかを認識して、このコマンドの実行をすべての子コンポーネントに委任します。もう 1 つの興味深いパターンは、「Bridge」パターンです。2 つの異なるクラス階層間の接続またはブリッジを記述するため、このように呼ばれます。これらの階層の 1 つは抽象化と見なされ、もう 1 つは実装と見なされます。抽象化自体はアクションを実行せず、この実行を実装に委任するため、これが強調表示されます。このパターンは、「コントロール」クラスと数種類の「プラットフォーム」クラス (Windows、Linux など) が存在する場合によく使用されます。このアプローチでは、これらの階層の 1 つ (抽象化) が別の階層 (実装) のオブジェクトへの参照を受け取り、主な作業をそれらの階層に委任します。すべての実装は共通のインターフェイスに従うため、抽象化内で交換できます。Java では、これの明確な例は次のとおりですjava.awt
。
行動パターン
そこで、オブジェクトを作成する方法と、クラス間の接続を組織する方法を考え出しました。残された最も興味深いことは、オブジェクトの動作を変更する際の柔軟性を提供することです。そして行動パターンがこれに役立ちます。最も頻繁に言及されるパターンの 1 つは「戦略」パターンです。ここから、「 Head First. Design Patterns 」という本のパターンの研究が始まります。「戦略」パターンを使用すると、アクションをどのように実行するかをオブジェクト内に保存できます。内部のオブジェクトには、コードの実行中に変更できる戦略が保存されます。これは、コンパレータを使用するときによく使用されるパターンです。import java.util.*;
class Main {
public static void main(String[] args) {
List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
Comparator<String> comparator = Comparator.comparingInt(String::length);
Set dataSet = new TreeSet(comparator);
dataSet.addAll(data);
System.out.println("Dataset : " + dataSet);
}
}
私たちの前に - TreeSet
。TreeSet
要素の順序を維持する動作があります。(SortedSet であるため) それらを並べ替えます。この動作には、JavaDoc に示されているデフォルトの戦略があります。それは、「自然な順序」でソートすることです (文字列の場合、これは辞書順です)。これは、パラメーターなしのコンストラクターを使用する場合に発生します。しかし、戦略を変更したい場合は、 を渡すことができますComparator
。この例では、セットを として作成するnew TreeSet(comparator)
と、要素を格納する順序 (格納戦略) がコンパレータで指定された順序に変更されます。興味深いことに、「 State 」と呼ばれるほぼ同じパターンがあります。「State」パターンは、このオブジェクトの状態に依存する何らかの動作がメイン オブジェクトにある場合、状態自体をオブジェクトとして記述し、状態オブジェクトを変更できることを示しています。そして、メインオブジェクトからの呼び出しを状態に委任します。Java 言語の基本を学習することで知られるもう 1 つのパターンは、「Command」パターンです。この設計パターンは、異なるコマンドを異なるクラスとして表すことができることを示唆しています。このパターンは、Strategy パターンと非常によく似ています。しかし、Strategy パターンでは、特定のアクションがどのように実行されるかを再定義していました (たとえば、 での並べ替えTreeSet
)。「コマンド」パターンでは、どのようなアクションが実行されるかを再定義します。pattern コマンドは、スレッドを使用するときに毎日使用します。
import java.util.*;
class Main {
public static void main(String[] args) {
Runnable command = () -> {
System.out.println("Command action");
};
Thread th = new Thread(command);
th.start();
}
}
ご覧のとおり、command は新しいスレッドで実行されるアクションまたはコマンドを定義します。「責任の連鎖」パターンも考慮する価値があります。このパターンも非常にシンプルです。このパターンは、何かを処理する必要がある場合にハンドラーをチェーン内に収集できることを示しています。たとえば、このパターンは Web サーバーでよく使用されます。入力時に、サーバーはユーザーからのリクエストを受け取ります。その後、このリクエストは処理チェーンを通過します。このハンドラーのチェーンには、フィルター (たとえば、IP アドレスのブラックリストからの要求を受け入れない)、認証ハンドラー (許可されたユーザーのみを許可)、要求ヘッダー ハンドラー、キャッシュ ハンドラーなどが含まれます。しかし、Java には、より単純でわかりやすい例がありますjava.util.logging
。
import java.util.logging.*;
class Main {
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName());
ConsoleHandler consoleHandler = new ConsoleHandler(){
@Override
public void publish(LogRecord record) {
System.out.println("LogRecord обработан");
}
};
logger.addHandler(consoleHandler);
logger.info("test");
}
}
ご覧のとおり、ハンドラーがロガー ハンドラーのリストに追加されます。logger.getHandlers
ロガーが処理するメッセージを受信すると、そのような各メッセージは、そのロガーの ( からの) ハンドラーのチェーンを通過します。私たちが毎日目にするもう 1 つのパターンは「Iterator」です。その本質は、オブジェクトのコレクション (つまり、データ構造を表すクラス。たとえば、List
) とこのコレクションの走査を分離することです。
import java.util.*;
class Main {
public static void main(String[] args) {
List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
Iterator<String> iterator = data.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
ご覧のとおり、イテレータはコレクションの一部ではありませんが、コレクションを横断する別のクラスによって表されます。イテレータのユーザーは、どのコレクションを反復処理しているのかさえ知らない可能性があります。彼はどのコレクションを訪れているのですか?「訪問者」パターンを検討する価値があります。ビジター パターンはイテレータ パターンと非常によく似ています。このパターンは、オブジェクトの構造をバイパスし、これらのオブジェクトに対してアクションを実行するのに役立ちます。それらはむしろ概念が異なります。イテレータはコレクションを走査するため、イテレータを使用するクライアントはコレクションの内容を気にせず、シーケンス内の要素のみが重要になります。訪問者とは、訪問するオブジェクトに特定の階層または構造があることを意味します。たとえば、個別のディレクトリ処理と個別のファイル処理を使用できます。Java には、このパターンのすぐに使える次の形式の実装がありますjava.nio.file.FileVisitor
。
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
public static void main(String[] args) {
SimpleFileVisitor visitor = new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
System.out.println("File:" + file.toString());
return FileVisitResult.CONTINUE;
}
};
Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
try {
Files.walkFileTree(pathSource, visitor);
} catch (AccessDeniedException e) {
// skip
} catch (IOException e) {
// Do something
}
}
}
場合によっては、一部のオブジェクトが他のオブジェクトの変化に反応する必要がある場合、「オブザーバー」パターンが役に立ちます。最も便利な方法は、一部のオブジェクトが他のオブジェクトで発生するイベントを監視し、応答できるようにするサブスクリプション メカニズムを提供することです。このパターンは、さまざまなイベントに反応するさまざまなリスナーやオブザーバーでよく使用されます。簡単な例として、JDK の最初のバージョンでのこのパターンの実装を思い出してください。
import java.util.*;
class Main {
public static void main(String[] args) {
Observer observer = (obj, arg) -> {
System.out.println("Arg: " + arg);
};
Observable target = new Observable(){
@Override
public void notifyObservers(Object arg) {
setChanged();
super.notifyObservers(arg);
}
};
target.addObserver(observer);
target.notifyObservers("Hello, World!");
}
}
もう 1 つ便利な行動パターン「メディエーター」 があります。これは、複雑なシステムでは、異なるオブジェクト間の接続を削除し、オブジェクト間のすべての対話を仲介者である何らかのオブジェクトに委任するのに役立つため、便利です。このパターンの最も印象的なアプリケーションの 1 つは、このパターンを使用する Spring MVC です。詳細については、「Spring: Mediator Pattern」を参照してください。例でも同じことがよく見られますjava.util.Timer
。
import java.util.*;
class Main {
public static void main(String[] args) {
Timer mediator = new Timer("Mediator");
TimerTask command = new TimerTask() {
@Override
public void run() {
System.out.println("Command pattern");
mediator.cancel();
}
};
mediator.schedule(command, 1000);
}
}
この例はコマンド パターンに似ています。そして、「Mediator」パターンの本質は「a」の実装に隠されていますTimer
。タイマーの内部にはタスク キューとTaskQueue
スレッドがありますTimerThread
。このクラスのクライアントとしての私たちは、オブジェクトと対話するのではなく、Timer
オブジェクトと対話します。オブジェクトは、そのメソッドへの呼び出しに応答して、仲介者である他のオブジェクトのメソッドにアクセスします。外見的には「ファサード」に非常に似ているように見えるかもしれません。ただし、違いは、ファサードが使用される場合、コンポーネントはファサードの存在を認識せず、相互に通信することです。「メディエーター」が使用される場合、コンポーネントは仲介者を認識して使用しますが、相互に直接連絡することはありません。「テンプレートメソッド」パターンはその名前からも明らかなので、検討する価値があります。肝心なのは、コードのユーザー (開発者) に何らかのアルゴリズム テンプレートが提供され、そのステップが再定義できるようにコードが記述されているということです。これにより、コード ユーザーはアルゴリズム全体を記述することなく、このアルゴリズムの 1 つまたは別のステップを正しく実行する方法だけを考えることができます。たとえば、Java には、AbstractList
反復子の動作を によって定義する抽象クラスがありますList
。ただし、イテレータ自体は、get
、set
、などのリーフ メソッドを使用しますremove
。これらのメソッドの動作は、子孫の開発者によって決定されますAbstractList
。したがって、 - の反復子は、AbstractList
シートを反復処理するためのアルゴリズムのテンプレートです。そして、特定の実装の開発者は、AbstractList
特定のステップの動作を定義することによって、この反復の動作を変更します。私たちが分析するパターンの最後は「スナップショット」(Momento) パターンです。その本質は、オブジェクトの特定の状態を保存し、その状態を復元する機能です。JDK の最も有名な例はオブジェクトのシリアル化です。java.io.Serializable
。例を見てみましょう:
import java.io.*;
import java.util.*;
class Main {
public static void main(String[] args) throws IOException {
ArrayList<String> list = new ArrayList<>();
list.add("test");
// Save State
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
out.writeObject(list);
}
// Load state
byte[] bytes = stream.toByteArray();
InputStream inputStream = new ByteArrayInputStream(bytes);
try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
List<String> listNew = (List<String>) in.readObject();
System.out.println(listNew.get(0));
} catch (ClassNotFoundException e) {
// Do something. Can't find class fpr saved state
}
}
}
結論
レビューからもわかるように、パターンは非常に多様です。それぞれが独自の問題を解決します。これらのパターンを知っておくと、柔軟性があり、保守しやすく、変更に強いシステムを作成する方法をやがて理解するのに役立ちます。最後に、さらに詳しく説明するためのリンクをいくつか示します。- パターンに関する最も素晴らしいリソース: refactoring.guru
- ビデオプレイリスト「オブジェクト指向プログラミングのデザインパターン」
- デザインパターンの演習
- パターンテスト
- Udemyのパターンコース
- Coursera のパターン コース
GO TO FULL VERSION