コアJavaの創造的デザインパターン
1. 序章
デザインパターンは、ソフトウェアを作成するときに使用する一般的なパターンです。 これらは、時間をかけて開発された確立されたベストプラクティスを表しています。 これらは、コードが適切に設計され、適切に構築されていることを確認するのに役立ちます。
作成パターンは、オブジェクトのインスタンスを取得する方法に焦点を当てたデザインパターンです。 通常、これはクラスの新しいインスタンスを構築する方法を意味しますが、場合によっては、使用できるようにすでに構築されたインスタンスを取得することを意味します。
この記事では、いくつかの一般的な作成デザインパターンを再検討します。 それらがどのように見えるか、JVMまたは他のコアライブラリ内のどこにあるかを確認します。
2. ファクトリメソッド
ファクトリメソッドパターンは、インスタンスの構築を、構築しているクラスから分離するための方法です。 これは、正確な型を抽象化して、代わりにクライアントコードがインターフェイスまたは抽象クラスの観点から機能できるようにするためです。
class SomeImplementation implements SomeInterface {
// ...
}
public class SomeInterfaceFactory {
public SomeInterface newInstance() {
return new SomeImplementation();
}
}
ここで、クライアントコードは SomeImplementation について知る必要はなく、代わりにSomeInterfaceに関して機能します。 ただし、これ以上に、ファクトリから返されるタイプを変更でき、クライアントコードを変更する必要はありません。 これには、実行時にタイプを動的に選択することも含まれます。
2.1. JVMでの例
おそらく、このパターンの最もよく知られている例は、 singleton()、 singletonList()などのCollectionsクラスのコレクション構築メソッドです。 singletonMap()。これらはすべて、適切なコレクションのインスタンスを返します– Set 、 List 、または Map – 、ただし正確タイプは無関係です。 さらに、 Stream.of()メソッドと新しい Set.of()、 List.of()、および Map.ofEntries( )メソッドを使用すると、より大きなコレクションでも同じことができます。
これには他にもたくさんの例があります。たとえば、 Charset.forName()は、要求された名前に応じて Charset クラスの異なるインスタンスを返します。また、[X196X ] ResourceBundle.getBundle()。指定された名前に応じて異なるリソースバンドルをロードします。
これらのすべてが異なるインスタンスを提供する必要もありません。 いくつかは、内部の仕組みを隠すための単なる抽象化です。 たとえば、 Calendar.getInstance()と NumberFormat.getInstance()は常に同じインスタンスを返しますが、正確な詳細はクライアントコードとは無関係です。
3. 抽象ファクトリ
Abstract Factory パターンは、これを超えるステップであり、使用されるファクトリにも抽象ベースタイプがあります。 次に、これらの抽象型の観点からコードを記述し、実行時に何らかの方法で具象ファクトリインスタンスを選択できます。
まず、実際に使用したい機能のインターフェースといくつかの具体的な実装があります。
interface FileSystem {
// ...
}
class LocalFileSystem implements FileSystem {
// ...
}
class NetworkFileSystem implements FileSystem {
// ...
}
次に、ファクトリが上記を取得するためのインターフェイスといくつかの具体的な実装があります。
interface FileSystemFactory {
FileSystem newInstance();
}
class LocalFileSystemFactory implements FileSystemFactory {
// ...
}
class NetworkFileSystemFactory implements FileSystemFactory {
// ...
}
次に、実際のインスタンスを取得できる抽象ファクトリを取得するための別のファクトリメソッドがあります。
class Example {
static FileSystemFactory getFactory(String fs) {
FileSystemFactory factory;
if ("local".equals(fs)) {
factory = new LocalFileSystemFactory();
else if ("network".equals(fs)) {
factory = new NetworkFileSystemFactory();
}
return factory;
}
}
ここでは、2つの具体的な実装を持つFileSystemFactoryインターフェイスがあります。 実行時に正確な実装を選択しますが、それを使用するコードは、実際に使用されるインスタンスを気にする必要はありません。 これらはそれぞれ、 FileSystem インターフェイスの異なる具体的なインスタンスを返しますが、ここでも、コードはこのインスタンスがどれであるかを正確に気にする必要はありません。
多くの場合、上記のように、別のファクトリメソッドを使用してファクトリ自体を取得します。 この例では、 getFactory()メソッド自体が、FileSystemの構築に使用される抽象FileSystemFactoryを返すファクトリメソッドです。
3.1. JVMでの例
JVM全体で使用されるこのデザインパターンの例はたくさんあります。 最も一般的に見られるのは、XMLパッケージの周りです。たとえば、 DocumentBuilderFactory 、 TransformerFactory、、XPathFactoryなどです。 これらはすべて、コードが抽象ファクトリのインスタンスを取得できるようにする特別なnewInstance()ファクトリメソッドを持っています。
内部的には、このメソッドは、システムプロパティ、JVMの構成ファイル、サービスプロバイダーインターフェイスなど、さまざまなメカニズムを使用して、使用する具体的なインスタンスを正確に決定しようとします。 これにより、必要に応じて代替XMLライブラリをアプリケーションにインストールできますが、これは実際にそれらを使用するコードに対して透過的です。
コードがnewInstance()メソッドを呼び出すと、適切なXMLライブラリからファクトリのインスタンスが作成されます。 次に、このファクトリは、同じライブラリから使用する実際のクラスを構築します。
たとえば、JVMのデフォルトのXerces実装を使用している場合、 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl のインスタンスを取得しますが、代わりに使用したい場合は別の実装の場合、 newInstance()を呼び出すと、代わりに透過的に返されます。
4. ビルダー
Builderパターンは、より柔軟な方法で複雑なオブジェクトを作成する場合に役立ちます。 これは、複雑なオブジェクトを構築するために使用する別のクラスを用意し、クライアントがより単純なインターフェイスでこれを作成できるようにすることで機能します。
class CarBuilder {
private String make = "Ford";
private String model = "Fiesta";
private int doors = 4;
private String color = "White";
public Car build() {
return new Car(make, model, doors, color);
}
}
これにより、 make 、 model 、 doors 、および color の値を個別に指定し、をビルドするときに値を指定できます。 Car 、すべてのコンストラクター引数が格納された値に解決されます。
4.1. JVMでの例
JVM内のこのパターンの非常に重要な例がいくつかあります。 StringBuilderクラスとStringBufferクラスは、多くの小さなパーツを提供することで長い文字列を構築できるようにするビルダーです。 最近のStream.Builderクラスでは、Streamを構築するためにまったく同じことを行うことができます。
Stream.Builder<Integer> builder = Stream.builder<Integer>();
builder.add(1);
builder.add(2);
if (condition) {
builder.add(3);
builder.add(4);
}
builder.add(5);
Stream<Integer> stream = builder.build();
5. 怠惰な初期化
レイジー初期化パターンを使用して、必要になるまで値の計算を延期します。 これには個々のデータが含まれる場合もあれば、オブジェクト全体を意味する場合もあります。
これは、多くのシナリオで役立ちます。 たとえば、オブジェクトを完全に構築するにはデータベースまたはネットワークへのアクセスが必要であり、それを使用する必要がない場合、これらの呼び出しを実行すると、アプリケーションのパフォーマンスが低下する可能性があります。 または、不要になる可能性のある多数の値を計算している場合、これにより不要なメモリ使用量が発生する可能性があります。
通常、これは、1つのオブジェクトを必要なデータのレイジーラッパーにし、getterメソッドを介してアクセスしたときにデータを計算することで機能します。
class LazyPi {
private Supplier<Double> calculator;
private Double value;
public synchronized Double getValue() {
if (value == null) {
value = calculator.get();
}
return value;
}
}
円周率の計算はコストのかかる操作であり、実行する必要がない場合があります。 上記は、 getValue()を初めて呼び出すときに、以前ではなく、これを行います。
5.1. JVMでの例
JVMでのこの例は比較的まれです。 ただし、Java8で導入されたStreamsAPIは優れた例です。 ストリームで実行されるすべての操作はレイジーであるため、ここで高価な計算を実行でき、必要な場合にのみ呼び出されることがわかります。
ただし、ストリーム自体の実際の生成も遅延する可能性があります。 Stream.generate()は、次の値が必要なときにいつでも呼び出す関数を取り、必要なときにだけ呼び出されます。 これを使用して、たとえばHTTP API呼び出しを行うことにより、高価な値をロードできます。また、新しい要素が実際に必要になった場合にのみコストを支払います。
Stream.generate(new BaeldungArticlesLoader())
.filter(article -> article.getTags().contains("java-streams"))
.map(article -> article.getTitle())
.findFirst();
ここに、 Supplier があります。これは、HTTP呼び出しを行って記事をロードし、関連するタグに基づいてそれらをフィルタリングしてから、最初に一致するタイトルを返します。 ロードされた最初の記事がこのフィルターに一致する場合、実際に存在する記事の数に関係なく、1回のネットワーク呼び出しのみを行う必要があります。
6. オブジェクトプール
作成に費用がかかる可能性のあるオブジェクトの新しいインスタンスを構築するときにオブジェクトプールパターンを使用しますが、既存のインスタンスを再利用することも許容できる代替手段です。 毎回新しいインスタンスを作成する代わりに、これらのセットを事前に作成して、必要に応じて使用することができます。
これらの共有オブジェクトを管理するための実際のオブジェクトプールが存在します。 また、それぞれが同時に1つの場所でのみ使用されるように、それらを追跡します。 場合によっては、オブジェクトのセット全体が最初にのみ構築されます。 その他の場合、必要に応じてプールがオンデマンドで新しいインスタンスを作成することがあります
6.1. JVMでの例
JVMでのこのパターンの主な例は、スレッドプールの使用です。 ExecutorService は一連のスレッドを管理し、タスクを1つで実行する必要があるときにそれらを使用できるようにします。 これを使用することは、非同期タスクを生成する必要があるときはいつでも、すべてのコストを伴う新しいスレッドを作成する必要がないことを意味します。
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(new SomeTask()); // Runs on a thread from the pool
pool.execute(new AnotherTask()); // Runs on a thread from the pool
これらの2つのタスクには、スレッドプールから実行するスレッドが割り当てられます。 同じスレッドでもまったく異なるスレッドでもかまいません。どのスレッドを使用するかはコードに関係ありません。
7. プロトタイプ
元のオブジェクトと同一のオブジェクトの新しいインスタンスを作成する必要がある場合は、プロトタイプパターンを使用します。 元のインスタンスはプロトタイプとして機能し、元のインスタンスから完全に独立した新しいインスタンスを構築するために使用されます。 その後、これらを使用できますが、必要です。
Javaは、Cloneableマーカーインターフェイスを実装してからObject.clone()を使用することにより、これをある程度サポートしています。 これにより、オブジェクトの浅いクローンが生成され、新しいインスタンスが作成され、フィールドが直接コピーされます。
これは安価ですが、オブジェクト内で構造化されたフィールドが同じインスタンスになるという欠点があります。 つまり、これは、これらのフィールドへの変更がすべてのインスタンスで発生することを意味します。 ただし、必要に応じて、いつでもこれを自分でオーバーライドできます。
public class Prototype implements Cloneable {
private Map<String, String> contents = new HashMap<>();
public void setValue(String key, String value) {
// ...
}
public String getValue(String key) {
// ...
}
@Override
public Prototype clone() {
Prototype result = new Prototype();
this.contents.entrySet().forEach(entry -> result.setValue(entry.getKey(), entry.getValue()));
return result;
}
}
7.1. JVMでの例
JVMには、この例がいくつかあります。 Cloneable インターフェースを実装するクラスに従うことで、これらを確認できます。 例えば、 PKIXCertPathBuilderResult 、 PKIXBuilderParameters 、 PKIXParameters 、 PKIXCertPathBuilderResult 、 と PKIXCertPathValidatorResult 全てです
もう1つの例は、java.util.Dateクラスです。 特に、これはObject.clone()メソッドをオーバーライドして、追加の一時フィールドにもコピーします。
8. シングルトン
シングルトンパターンは、インスタンスが1つしかないクラスがあり、このインスタンスにアプリケーション全体からアクセスできる必要がある場合によく使用されます。 通常、これは静的メソッドを介してアクセスする静的インスタンスで管理します。
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
正確なニーズに応じて、これにはいくつかのバリエーションがあります。たとえば、インスタンスが起動時または最初の使用時に作成されるかどうか、アクセスがスレッドセーフである必要があるかどうか、スレッドごとに異なるインスタンスが必要かどうかなどです。
8.1. JVMでの例
JVMには、JVM自体のコア部分を表すクラスを使用したこの例がいくつかあります — Runtime、Desktop、、およびSecurityManager。 これらはすべて、それぞれのクラスの単一インスタンスを返すアクセサメソッドを持っています。
さらに、JavaReflectionAPIの多くはシングルトンインスタンスで動作します。 同じ実際のクラスは、 Class.forName()、 String.class を使用してアクセスされたか、他のリフレクションを介してアクセスされたかに関係なく、常に Class、の同じインスタンスを返します。メソッド。
同様に、現在のスレッドを表すThreadインスタンスをシングルトンと見なすことができます。 多くの場合、これには多くのインスタンスがありますが、定義上、スレッドごとに1つのインスタンスがあります。 同じスレッドで実行されている場所からThread.currentThread()を呼び出すと、常に同じインスタンスが返されます。
9. 概要
この記事では、オブジェクトのインスタンスを作成および取得するために使用されるさまざまなデザインパターンについて説明しました。 また、コアJVM内で使用されているこれらのパターンの例も確認したので、多くのアプリケーションがすでに恩恵を受けている方法で使用されていることがわかります。