Kreative Entwurfsmuster in Core Java

1. Einleitung

Entwurfsmuster sind gängige Muster, die wir beim Schreiben unserer Software verwenden . Sie stellen etablierte Best Practices dar, die im Laufe der Zeit entwickelt wurden. Diese können uns dann helfen, sicherzustellen, dass unser Code gut gestaltet und gut erstellt ist.

Kreationsmuster sind Entwurfsmuster, die sich darauf konzentrieren, wie wir Instanzen von Objekten erhalten . In der Regel bedeutet dies, wie wir neue Instanzen einer Klasse erstellen. In einigen Fällen bedeutet dies jedoch, dass eine bereits erstellte Instanz zur Verwendung bereitsteht.

In diesem Artikel werden wir einige gängige kreative Designmuster erneut betrachten. Wir werden sehen, wie sie aussehen und wo sie in der JVM oder anderen Kernbibliotheken zu finden sind.

2. Werksmethode

Das Factory-Methodenmuster ist für uns eine Möglichkeit, die Konstruktion einer Instanz von der Klasse zu trennen, die wir konstruieren. Auf diese Weise können wir den genauen Typ abstrahieren und unseren Client-Code stattdessen in Form von Schnittstellen oder abstrakten Klassen verwenden:

class SomeImplementation implements SomeInterface { // ... } 
public class SomeInterfaceFactory { public SomeInterface newInstance() { return new SomeImplementation(); } }

Hier muss unser Client-Code nie etwas über SomeImplementation wissen , sondern funktioniert in Bezug auf SomeInterface . Darüber hinaus können wir den von unserer Fabrik zurückgegebenen Typ ändern, und der Client-Code muss nicht geändert werden . Dies kann sogar die dynamische Auswahl des Typs zur Laufzeit umfassen.

2.1. Beispiele in der JVM

Möglicherweise sind die bekanntesten Beispiele für dieses Muster, die JVM, die Methoden zum Erstellen von Sammlungen in der Collections- Klasse, wie singleton () , singletonList () und singletonMap (). Diese geben alle Instanzen der entsprechenden Sammlung zurück - Set , List oder Map -, aber der genaue Typ ist irrelevant . Darüber hinaus können wir mit der Stream.of () -Methode und den neuen Methoden Set.of () , List.of () und Map.ofEntries () dasselbe mit größeren Sammlungen tun.

Es gibt auch viele andere Beispiele dafür, einschließlich Charset.forName () , das abhängig vom angeforderten Namen eine andere Instanz der Charset- Klasse zurückgibt , und ResourceBundle.getBundle () , das abhängig davon ein anderes Ressourcenpaket lädt auf den angegebenen Namen.

Auch müssen nicht alle unterschiedliche Instanzen bereitstellen. Einige sind nur Abstraktionen, um das Innenleben zu verbergen. Beispielsweise geben Calendar.getInstance () und NumberFormat.getInstance () immer dieselbe Instanz zurück, aber die genauen Details sind für den Clientcode irrelevant.

3. Abstrakte Fabrik

Das Abstract Factory-Muster ist ein Schritt darüber hinaus, wobei die verwendete Factory auch einen abstrakten Basistyp hat. Wir können dann unseren Code in Form dieser abstrakten Typen schreiben und die konkrete Factory-Instanz zur Laufzeit irgendwie auswählen.

Erstens haben wir eine Schnittstelle und einige konkrete Implementierungen für die Funktionalität, die wir tatsächlich verwenden möchten:

interface FileSystem { // ... } 
class LocalFileSystem implements FileSystem { // ... } 
class NetworkFileSystem implements FileSystem { // ... } 

Als nächstes haben wir eine Schnittstelle und einige konkrete Implementierungen für die Fabrik, um Folgendes zu erhalten:

interface FileSystemFactory { FileSystem newInstance(); } 
class LocalFileSystemFactory implements FileSystemFactory { // ... } 
class NetworkFileSystemFactory implements FileSystemFactory { // ... } 

Wir haben dann eine andere Factory-Methode, um die abstrakte Factory zu erhalten, über die wir die tatsächliche Instanz erhalten können:

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; } }

Hier haben wir eine FileSystemFactory- Schnittstelle, die zwei konkrete Implementierungen hat. Wir wählen die genaue Implementierung zur Laufzeit aus, aber der Code, der sie verwendet, muss sich nicht darum kümmern, welche Instanz tatsächlich verwendet wird . Diese geben dann jeweils eine andere konkrete Instanz der FileSystem- Schnittstelle zurück, aber auch hier muss sich unser Code nicht genau darum kümmern, welche Instanz davon wir haben.

Oft erhalten wir die Fabrik selbst mit einer anderen Fabrikmethode, wie oben beschrieben. In unserem Beispiel hier ist die Methode getFactory () selbst eine Factory-Methode, die eine abstrakte FileSystemFactory zurückgibt , die dann zum Erstellen eines FileSystems verwendet wird .

3.1. Beispiele in der JVM

Es gibt viele Beispiele für dieses Entwurfsmuster, das in der gesamten JVM verwendet wird. Am häufigsten werden XML-Pakete verwendet, z. B. DocumentBuilderFactory , TransformerFactory und XPathFactory . Diese haben alle eine spezielle newInstance () -Factory -Methode, mit der unser Code eine Instanz der abstrakten Factory abrufen kann .

Intern verwendet diese Methode eine Reihe verschiedener Mechanismen - Systemeigenschaften, Konfigurationsdateien in der JVM und die Service Provider-Schnittstelle -, um zu versuchen, genau zu entscheiden, welche konkrete Instanz verwendet werden soll. Auf diese Weise können wir auf Wunsch alternative XML-Bibliotheken in unserer Anwendung installieren. Dies ist jedoch für jeden Code, der sie tatsächlich verwendet, transparent.

Sobald unser Code die newInstance () -Methode aufgerufen hat , verfügt er über eine Factory-Instanz aus der entsprechenden XML-Bibliothek. Diese Factory erstellt dann die tatsächlichen Klassen, die wir verwenden möchten, aus derselben Bibliothek.

Wenn wir beispielsweise die JVM-Standardimplementierung von Xerces verwenden, erhalten wir eine Instanz von com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl . Wenn wir jedoch stattdessen eine andere Implementierung verwenden möchten , rufen Sie auf newInstance () würde dies stattdessen transparent zurückgeben.

4. Builder

Das Builder-Muster ist nützlich, wenn Sie ein kompliziertes Objekt flexibler erstellen möchten. Es funktioniert, indem wir eine separate Klasse haben, die wir zum Erstellen unseres komplizierten Objekts verwenden, und dem Client ermöglichen, dies mit einer einfacheren Oberfläche zu erstellen:

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); } }

Auf diese Weise können wir Werte für Marke , Modell , Türen und Farbe individuell angeben. Wenn wir dann das Auto bauen , werden alle Konstruktorargumente in die gespeicherten Werte aufgelöst.

4.1. Beispiele in der JVM

There are some very key examples of this pattern within the JVM. The StringBuilder and StringBuffer classes are builders that allow us to construct a long String by providing many small parts. The more recent Stream.Builder class allows us to do exactly the same in order to construct a Stream:

Stream.Builder builder = Stream.builder(); builder.add(1); builder.add(2); if (condition) { builder.add(3); builder.add(4); } builder.add(5); Stream stream = builder.build();

5. Lazy Initialization

We use the Lazy Initialization pattern to defer the calculation of some value until it's needed. Sometimes, this can involve individual pieces of data, and other times, this can mean entire objects.

This is useful in a number of scenarios. For example, if fully constructing an object requires database or network access and we may never need to use it, then performing those calls may cause our application to under-perform. Alternatively, if we're computing a large number of values that we may never need, then this can cause unnecessary memory usage.

Typically, this works by having one object be the lazy wrapper around the data that we need, and having the data computed when accessed via a getter method:

class LazyPi { private Supplier calculator; private Double value; public synchronized Double getValue() { if (value == null) { value = calculator.get(); } return value; } }

Computing pi is an expensive operation and one that we may not need to perform. The above will do so on the first time that we call getValue() and not before.

5.1. Examples in the JVM

Examples of this in the JVM are relatively rare. However, the Streams API introduced in Java 8 is a great example. All of the operations performed on a stream are lazy, so we can perform expensive calculations here and know they are only called if needed.

However, the actual generation of the stream itself can be lazy as well. Stream.generate() takes a function to call whenever the next value is needed and is only ever called when needed. We can use this to load expensive values – for example, by making HTTP API calls – and we only pay the cost whenever a new element is actually needed:

Stream.generate(new BaeldungArticlesLoader()) .filter(article -> article.getTags().contains("java-streams")) .map(article -> article.getTitle()) .findFirst();

Here, we have a Supplier that will make HTTP calls to load articles, filter them based on the associated tags, and then return the first matching title. If the very first article loaded matches this filter, then only a single network call needs to be made, regardless of how many articles are actually present.

6. Object Pool

We'll use the Object Pool pattern when constructing a new instance of an object that may be expensive to create, but re-using an existing instance is an acceptable alternative. Instead of constructing a new instance every time, we can instead construct a set of these up-front and then use them as needed.

The actual object pool exists to manage these shared objects. It also tracks them so that each one is only used in one place at the same time. In some cases, the entire set of objects gets constructed only at the start. In other cases, the pool may create new instances on demand if it's necessary

6.1. Examples in the JVM

The main example of this pattern in the JVM is the use of thread pools. An ExecutorService will manage a set of threads and will allow us to use them when a task needs to execute on one. Using this means that we don't need to create new threads, with all of the cost involved, whenever we need to spawn an asynchronous task:

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

These two tasks get allocated a thread on which to run from the thread pool. It might be the same thread or a totally different one, and it doesn't matter to our code which threads are used.

7. Prototype

We use the Prototype pattern when we need to create new instances of an object that are identical to the original. The original instance acts as our prototype and gets used to construct new instances that are then completely independent of the original. We can then use these however is necessary.

Java has a level of support for this by implementing the Cloneable marker interface and then using Object.clone(). This will produce a shallow clone of the object, creating a new instance, and copying the fields directly.

This is cheaper but has the downside that any fields inside our object that have structured themselves will be the same instance. This, then, means changes to those fields also happen across all instances. However, we can always override this ourselves if necessary:

public class Prototype implements Cloneable { private Map 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. Examples in the JVM

The JVM has a few examples of this. We can see these by following the classes that implement the Cloneable interface. For example, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIXParameters, PKIXCertPathBuilderResult, and PKIXCertPathValidatorResult are all Cloneable.

Another example is the java.util.Date class. Notably, this overrides the Object.clone() method to copy across an additional transient field as well.

8. Singleton

The Singleton pattern is often used when we have a class that should only ever have one instance, and this instance should be accessible from throughout the application. Typically, we manage this with a static instance that we access via a static method:

public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

There are several variations to this depending on the exact needs — for example, whether the instance is created at startup or on first use, whether accessing it needs to be threadsafe, and whether or not there needs to be a different instance per thread.

8.1. Examples in the JVM

The JVM has some examples of this with classes that represent core parts of the JVM itselfRuntime, Desktop, and SecurityManager. These all have accessor methods that return the single instance of the respective class.

Additionally, much of the Java Reflection API works with singleton instances. The same actual class always returns the same instance of Class, regardless of whether it's accessed using Class.forName(), String.class, or through other reflection methods.

In ähnlicher Weise können wir die Thread- Instanz, die den aktuellen Thread darstellt, als Singleton betrachten. Es wird oft viele Instanzen davon geben, aber per Definition gibt es eine einzelne Instanz pro Thread. Wenn Sie Thread.currentThread () von einer beliebigen Stelle aus aufrufen , die im selben Thread ausgeführt wird, wird immer dieselbe Instanz zurückgegeben.

9. Zusammenfassung

In diesem Artikel haben wir uns verschiedene Entwurfsmuster angesehen, die zum Erstellen und Abrufen von Instanzen von Objekten verwendet werden. Wir haben uns auch Beispiele für diese Muster angesehen, die auch in der Kern-JVM verwendet werden, damit wir sehen können, wie sie in einer Weise verwendet werden, von der viele Anwendungen bereits profitieren.