Die 3 Arten von Entwurfsmustern, die alle Entwickler kennen sollten (mit jeweils Codebeispielen)

Was ist ein Designmuster?

Entwurfsmuster sind Lösungen auf Entwurfsebene für wiederkehrende Probleme, auf die wir Softwareentwickler häufig stoßen. Es ist kein Code - ich wiederhole,CODE . Es ist wie eine Beschreibung, wie man diese Probleme angeht und eine Lösung entwirft.

Die Verwendung dieser Muster wird als bewährte Methode angesehen, da sich das Design der Lösung bewährt hat, was zu einer besseren Lesbarkeit des endgültigen Codes führt. Entwurfsmuster werden häufig für OOP-Sprachen wie Java erstellt und von diesen verwendet, in denen die meisten Beispiele von nun an geschrieben werden.

Arten von Entwurfsmustern

Derzeit sind ungefähr 26 Muster entdeckt (ich glaube kaum, dass ich sie alle machen werde…).

Diese 26 können in 3 Typen eingeteilt werden:

1. Kreativ: Diese Muster dienen zur Instanziierung von Klassen. Sie können entweder Klassenerstellungsmuster oder Objekterstellungsmuster sein.

2. Strukturell: Diese Muster werden in Bezug auf die Struktur und Zusammensetzung einer Klasse entworfen. Das Hauptziel der meisten dieser Muster besteht darin, die Funktionalität der beteiligten Klasse (n) zu verbessern, ohne einen großen Teil ihrer Zusammensetzung zu ändern.

3. Verhalten: Diese Muster hängen davon ab, wie eine Klasse mit anderen kommuniziert.

In diesem Beitrag werden wir ein grundlegendes Entwurfsmuster für jeden klassifizierten Typ durchgehen.

Typ 1: Kreativ - Das Singleton-Entwurfsmuster

Das Singleton-Entwurfsmuster ist ein Erstellungsmuster, dessen Ziel es ist, nur eine Instanz einer Klasse zu erstellen und nur einen globalen Zugriffspunkt für dieses Objekt bereitzustellen. Ein häufig verwendetes Beispiel für eine solche Klasse in Java ist Kalender, in dem Sie keine Instanz dieser Klasse erstellen können. Es verwendet auch eine eigene getInstance()Methode, um das zu verwendende Objekt abzurufen.

Eine Klasse, die das Singleton-Entwurfsmuster verwendet, umfasst:

  1. Eine private statische Variable, die die einzige Instanz der Klasse enthält.
  2. Ein privater Konstruktor, daher kann er nirgendwo anders instanziiert werden.
  3. Eine öffentliche statische Methode, um die einzelne Instanz der Klasse zurückzugeben.

Es gibt viele verschiedene Implementierungen des Singleton-Designs. Heute werde ich die Implementierungen von durchgehen;

1. Eifrige Instanziierung

2. Faule Instanziierung

3. Thread-sichere Instanziierung

Eifriger Biber

public class EagerSingleton { // create an instance of the class. private static EagerSingleton instance = new EagerSingleton(); // private constructor, so it cannot be instantiated outside this class. private EagerSingleton() { } // get the only instance of the object created. public static EagerSingleton getInstance() { return instance; } }

Diese Art der Instanziierung erfolgt während des Ladens der Klasse, da die Instanziierung der variablen Instanz außerhalb einer Methode erfolgt. Dies stellt einen erheblichen Nachteil dar, wenn diese Klasse von der Clientanwendung überhaupt nicht verwendet wird. Der Notfallplan ist die Lazy Instantiation, wenn diese Klasse nicht verwendet wird.

Entspannte Tage

Es gibt keinen großen Unterschied zur obigen Implementierung. Die Hauptunterschiede bestehen darin, dass die statische Variable anfänglich als null deklariert wird und innerhalb der getInstance()Methode nur dann instanziiert wird, wenn - und nur wenn - die Instanzvariable zum Zeitpunkt der Prüfung null bleibt.

public class LazySingleton { // initialize the instance as null. private static LazySingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private LazySingleton() { } // check if the instance is null, and if so, create the object. public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }

Dies behebt ein Problem, aber es gibt noch ein anderes. Was ist, wenn zwei verschiedene Clients gleichzeitig auf die Singleton-Klasse zugreifen, bis zur Millisekunde? Nun, sie prüfen gleichzeitig, ob die Instanz null ist, und stellen fest, dass sie wahr ist, und erstellen so zwei Instanzen der Klasse für jede Anforderung der beiden Clients. Um dies zu beheben, muss die Thread-sichere Instanziierung implementiert werden.

(Thread) Sicherheit ist der Schlüssel

In Java wird das Schlüsselwort synchronized für Methoden oder Objekte verwendet, um die Thread-Sicherheit zu implementieren, sodass jeweils nur ein Thread auf eine bestimmte Ressource zugreift. Die Klasseninstanziierung wird in einen synchronisierten Block eingefügt, sodass nur ein Client zu einem bestimmten Zeitpunkt auf die Methode zugreifen kann.

public class ThreadSafeSingleton { // initialize the instance as null. private static ThreadSafeSingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private ThreadSafeSingleton() { } // check if the instance is null, within a synchronized block. If so, create the object public static ThreadSafeSingleton getInstance() { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } return instance; } }

Der Overhead für die synchronisierte Methode ist hoch und verringert die Leistung des gesamten Vorgangs.

Wenn die Instanzvariable beispielsweise bereits instanziiert wurde, wird jedes Mal, wenn ein Client auf die getInstance()Methode zugreift , die synchronizedMethode ausgeführt und die Leistung sinkt. Dies geschieht nur, um zu überprüfen, ob der instanceWert der Variablen null ist. Wenn es feststellt, dass dies der Fall ist, verlässt es die Methode.

Um diesen Overhead zu reduzieren, wird eine doppelte Verriegelung verwendet. Die Prüfung wird auch vor der synchronizedMethode verwendet. Wenn der Wert allein null ist, wird die synchronizedMethode ausgeführt.

// double locking is used to reduce the overhead of the synchronized method public static ThreadSafeSingleton getInstanceDoubleLocking() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }

Nun zur nächsten Klassifizierung.

Typ 2: Strukturell - Das Dekorationsmuster

Ich werde Ihnen ein kleines Szenario geben, um einen besseren Kontext dafür zu geben, warum und wo Sie das Dekorationsmuster verwenden sollten.

Angenommen, Sie besitzen ein Café, und wie jeder Neuling beginnen Sie mit nur zwei Arten von einfachem Kaffee, der Hausmischung und dem dunklen Braten. In Ihrem Abrechnungssystem gab es eine Klasse für die verschiedenen Kaffeemischungen, die die abstrakte Klasse für Getränke erbt. Die Leute kommen tatsächlich vorbei und trinken Ihren wunderbaren (wenn auch bitteren?) Kaffee. Dann gibt es die Kaffee-Newbs, die, Gott bewahre, Zucker oder Milch wollen. So eine Travestie für Kaffee !! ??

Jetzt müssen Sie auch diese beiden Add-Ons haben, sowohl im Menü als auch leider im Abrechnungssystem. Ursprünglich erstellt Ihre IT-Person eine Unterklasse für beide Kaffeesorten, darunter Zucker und Milch. Da die Kunden immer Recht haben, sagt man diese gefürchteten Worte:

"Kann ich bitte einen Milchkaffee mit Zucker bekommen?"

???

Da lacht Ihr Abrechnungssystem wieder in Ihrem Gesicht. Nun, zurück zum Zeichenbrett….

The IT person then adds milk coffee with sugar as another subclass to each parent coffee class. The rest of the month is smooth sailing, people lining up to have your coffee, you actually making money. ??

But wait, there’s more!

The world is against you once again. A competitor opens up across the street, with not just 4 types of coffee, but more than 10 add-ons as well! ?

You buy all those and more, to sell better coffee yourself, and just then remember that you forgot to update that dratted billing system. You quite possibly cannot make the infinite number of subclasses for any and all combinations of all the add-ons, with the new coffee blends too. Not to mention, the size of the final system.??

Time to actually invest in a proper billing system. You find new IT personnel, who actually knows what they are doing and they say;

“Why, this will be so much easier and smaller if it used the decorator pattern.”

What on earth is that?

The decorator design pattern falls into the structural category, that deals with the actual structure of a class, whether is by inheritance, composition or both. The goal of this design is to modify an objects’ functionality at runtime. This is one of the many other design patterns that utilize abstract classes and interfaces with composition to get its desired result.

Let’s give Math a chance (shudder?) to bring this all into perspective;

Take 4 coffee blends and 10 add-ons. If we stuck to the generation of subclasses for each different combination of all the add-ons for one type of coffee. That’s;

(10–1)² = 9² = 81 subclasses

We subtract 1 from the 10, as you cannot combine one add-on with another of the same type, sugar with sugar sounds stupid. And that’s for just one coffee blend. Multiply that 81 by 4 and you get a whopping 324 different subclasses! Talk about all that coding…

But with the decorator pattern will require only 16 classes in this scenario. Wanna bet?

If we map out our scenario according to the class diagram above, we get 4 classes for the 4 coffee blends, 10 for each add-on and 1 for the abstract component and 1 more for the abstract decorator. See! 16! Now hand over that $100.?? (jk, but it will not be refused if given… just saying)

As you can see from above, just as the concrete coffee blends are subclasses of the beverage abstract class, the AddOn abstract class also inherits its methods from it. The add-ons, that are its subclasses, in turn inherit any new methods to add functionality to the base object when needed.

Let’s get to coding, to see this pattern in use.

First to make the Abstract beverage class, that all the different coffee blends will inherit from:

public abstract class Beverage { private String description; public Beverage(String description) { super(); this.description = description; } public String getDescription() { return description; } public abstract double cost(); }

Then to add both the concrete coffee blend classes.

public class HouseBlend extends Beverage { public HouseBlend() { super(“House blend”); } @Override public double cost() { return 250; } } public class DarkRoast extends Beverage { public DarkRoast() { super(“Dark roast”); } @Override public double cost() { return 300; } }

The AddOn abstract class also inherits from the Beverage abstract class (more on this below).

public abstract class AddOn extends Beverage { protected Beverage beverage; public AddOn(String description, Beverage bev) { super(description); this.beverage = bev; } public abstract String getDescription(); }

And now the concrete implementations of this abstract class:

public class Sugar extends AddOn { public Sugar(Beverage bev) { super(“Sugar”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Mocha”; } @Override public double cost() { return beverage.cost() + 50; } } public class Milk extends AddOn { public Milk(Beverage bev) { super(“Milk”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Milk”; } @Override public double cost() { return beverage.cost() + 100; } }

As you can see above, we can pass any subclass of Beverage to any subclass of AddOn, and get the added cost as well as the updated description. And, since the AddOn class is essentially of type Beverage, we can pass an AddOn into another AddOn. This way, we can add any number of add-ons to a specific coffee blend.

Now to write some code to test this out.

public class CoffeeShop { public static void main(String[] args) { HouseBlend houseblend = new HouseBlend(); System.out.println(houseblend.getDescription() + “: “ + houseblend.cost()); Milk milkAddOn = new Milk(houseblend); System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost()); Sugar sugarAddOn = new Sugar(milkAddOn); System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost()); } }

The final result is:

It works! We were able to add more than one add-on to a coffee blend and successfully update its final cost and description, without the need to make infinite subclasses for each add-on combination for all coffee blends.

Finally, to the last category.

Type 3: Behavioral - The Command Design Pattern

A behavioral design pattern focuses on how classes and objects communicate with each other. The main focus of the command pattern is to inculcate a higher degree of loose coupling between involved parties (read: classes).

Uhhhh… What’s that?

Coupling is the way that two (or more) classes that interact with each other, well, interact. The ideal scenario when these classes interact is that they do not depend heavily on each other. That’s loose coupling. So, a better definition for loose coupling would be, classes that are interconnected, making the least use of each other.

The need for this pattern arose when requests needed to be sent without consciously knowing what you are asking for or who the receiver is.

In this pattern, the invoking class is decoupled from the class that actually performs an action. The invoker class only has the callable method execute, which runs the necessary command, when the client requests it.

Let’s take a basic real-world example, ordering a meal at a fancy restaurant. As the flow goes, you give your order (command) to the waiter (invoker), who then hands it over to the chef(receiver), so you can get food. Might sound simple… but a bit meh to code.

The idea is pretty simple, but the coding goes around the nose.

The flow of operation on the technical side is, you make a concrete command, which implements the Command interface, asking the receiver to complete an action, and send the command to the invoker. The invoker is the person that knows when to give this command. The chef is the only one who knows what to do when given the specific command/order. So, when the execute method of the invoker is run, it, in turn, causes the command objects’ execute method to run on the receiver, thus completing necessary actions.

What we need to implement is;

  1. An interface Command
  2. A class Order that implements Command interface
  3. A class Waiter (invoker)
  4. A class Chef (receiver)

So, the coding goes like this:

Chef, the receiver

public class Chef { public void cookPasta() { System.out.println(“Chef is cooking Chicken Alfredo…”); } public void bakeCake() { System.out.println(“Chef is baking Chocolate Fudge Cake…”); } }

Command, the interface

public interface Command { public abstract void execute(); }

Order, the concrete command

public class Order implements Command { private Chef chef; private String food; public Order(Chef chef, String food) { this.chef = chef; this.food = food; } @Override public void execute() { if (this.food.equals(“Pasta”)) { this.chef.cookPasta(); } else { this.chef.bakeCake(); } } }

Waiter, the invoker

public class Waiter { private Order order; public Waiter(Order ord) { this.order = ord; } public void execute() { this.order.execute(); } }

You, the client

public class Client { public static void main(String[] args) { Chef chef = new Chef(); Order order = new Order(chef, “Pasta”); Waiter waiter = new Waiter(order); waiter.execute(); order = new Order(chef, “Cake”); waiter = new Waiter(order); waiter.execute(); } }

As you can see above, the Client makes an Order and sets the Receiver as the Chef. The Order is sent to the Waiter, who will know when to execute the Order (i.e. when to give the chef the order to cook). When the invoker is executed, the Orders’ execute method is run on the receiver (i.e. the chef is given the command to either cook pasta ? or bake cake?).

Quick recap

In this post we went through:

  1. What a design pattern really is,
  2. The different types of design patterns and why they are different
  3. One basic or common design pattern for each type

I hope this was helpful.  

Find the code repo for the post, here.