Das Strategiemuster wird mit Java erklärt

In diesem Beitrag werde ich über eines der beliebtesten Designmuster sprechen - das Strategiemuster. Wenn Sie sich noch nicht bewusst sind, handelt es sich bei den Entwurfsmustern um eine Reihe objektorientierter Programmierprinzipien, die von namhaften Namen in der Softwareindustrie erstellt wurden und häufig als Gang of Four (GoF) bezeichnet werden. Diese Entwurfsmuster haben einen enormen Einfluss auf das Software-Ökosystem gehabt und werden bis heute verwendet, um häufig auftretende Probleme bei der objektorientierten Programmierung zu lösen.

Definieren wir das Strategiemuster formal:

Das Strategiemuster definiert eine Familie von Algorithmen, kapselt jeden einzelnen und macht sie austauschbar. Mit der Strategie kann der Algorithmus unabhängig von den Clients variieren, die ihn verwenden

Okay, wenn das aus dem Weg ist, lassen Sie uns in einen Code eintauchen, um zu verstehen, was diese Wörter WIRKLICH bedeuten. Wir werden ein Beispiel mit einer möglichen Gefahr nehmen und dann das Strategiemuster anwenden, um zu sehen, wie es das Problem überwindet.

Ich werde Ihnen zeigen, wie Sie ein Dope-Dog-Simulator-Programm erstellen, um das Strategiemuster zu lernen. So sehen unsere Klassen aus: Eine 'Hund'-Superklasse mit allgemeinem Verhalten und dann konkrete Klassen von Hunden, die durch Unterklassen der Hundeklasse erstellt wurden.

So sieht der Code aus

public abstract class Dog { public abstract void display(); //different dogs have different looks! public void eat(){} public void bark(){} // Other dog-like methods ... }

Die display () -Methode wird abstrakt gemacht, da verschiedene Hunde unterschiedlich aussehen. Alle anderen Unterklassen erben das Ess- und Rindenverhalten oder überschreiben es mit ihrer eigenen Implementierung. So weit, ist es gut!

Was wäre, wenn Sie ein neues Verhalten hinzufügen möchten? Angenommen, Sie brauchen einen coolen Roboterhund, der alle möglichen Tricks ausführen kann. Kein Problem, wir müssen nur eine performTricks () -Methode in unsere Dog-Superklasse einfügen und los geht's.

Aber Moment mal ... Ein Roboterhund sollte nicht fressen können, oder? Unbelebte Gegenstände können natürlich nicht essen. Okay, wie lösen wir dieses Problem dann? Nun, wir können die eat () -Methode überschreiben, um nichts zu tun, und es funktioniert einwandfrei!

public class RobotDog extends Dog { @override public void eat(){} // Do nothing }

Schön gemacht! Jetzt können Roboterhunde nicht mehr fressen, sondern nur noch bellen oder Tricks ausführen. Was ist mit Gummihunden? Sie können weder essen noch Tricks ausführen. Und Holzhunde können nicht essen, bellen oder Tricks ausführen. Wir können unmöglich Methoden überschreiben, um nichts zu tun, es ist nicht sauber und es fühlt sich einfach hackig an. Stellen Sie sich vor, Sie tun dies für ein Projekt, dessen Designspezifikation sich alle paar Monate ändert. Unser Beispiel ist nur ein naives, aber Sie haben die Idee. Wir müssen also einen saubereren Weg finden, um dieses Problem zu lösen.

Kann die Schnittstelle unser Problem lösen?

Wie wäre es mit Schnittstellen? Mal sehen, ob sie unser Problem lösen können. Also gut, wir erstellen eine CanEat- und eine CanBark-Schnittstelle:

interface CanEat { public void eat(); } interface CanBark { public void bark(); }

Wir haben jetzt die Methoden bark () und eat () aus der Dog-Superklasse entfernt und sie den jeweiligen Schnittstellen hinzugefügt. Damit nur die Hunde, die bellen können, die CanBark-Schnittstelle implementieren, und die Hunde, die fressen können, implementieren die CanEat-Schnittstelle. Jetzt machen Sie sich keine Sorgen mehr darüber, dass Hunde Verhalten erben, das sie nicht erben sollten. Unser Problem ist gelöst… oder doch?

Was passiert, wenn wir das Essverhalten der Hunde ändern müssen? Nehmen wir an, von nun an muss jeder Hund eine gewisse Menge Protein in sein Futter aufnehmen. Sie müssen jetzt die eat () -Methode aller Unterklassen von Dog ändern. Was ist, wenn es 50 solcher Klassen gibt, oh der Horror!

Schnittstellen lösen unser Problem also nur teilweise, dass Hunde nur das tun, wozu sie in der Lage sind - aber sie schaffen insgesamt ein anderes Problem. Schnittstellen haben keinen Implementierungscode, daher gibt es keine Wiederverwendbarkeit von Code und das Potenzial für viele doppelte Codes. Wie lösen wir das, fragen Sie? Strategiemuster kommt zur Rettung!

Das Strategiemuster

Also werden wir dies Schritt für Schritt tun. Bevor wir fortfahren, möchte ich Ihnen ein Gestaltungsprinzip vorstellen:

Identifizieren Sie die Teile Ihres Programms, die variieren, und trennen Sie sie von denen, die gleich bleiben.

Es ist eigentlich sehr einfach - das Prinzip besagt, dass alles, was sich häufig ändert, getrennt und „gekapselt“ werden muss, damit der gesamte Code, der sich ändert, an einem Ort lebt. Auf diese Weise hat der Code, der sich ändert, keine Auswirkungen auf den Rest des Programms, und unsere Anwendung ist flexibler und robuster.

In unserem Fall können das Verhalten "Rinde" und "Fressen" aus der Hundeklasse herausgenommen und an anderer Stelle eingekapselt werden. Wir wissen, dass diese Verhaltensweisen bei verschiedenen Hunden unterschiedlich sind und dass sie eine eigene Klasse haben müssen.

Wir werden neben der Hundeklasse zwei Klassen erstellen, eine zur Definition des Essverhaltens und eine für das Bellverhalten. Wir werden Schnittstellen verwenden, um das Verhalten wie 'EatBehavior' und 'BarkBehavior' darzustellen, und die konkrete Verhaltensklasse wird diese Schnittstellen implementieren. Die Dog-Klasse implementiert die Schnittstelle also nicht mehr. Wir erstellen separate Klassen, deren einzige Aufgabe es ist, das spezifische Verhalten darzustellen!

So sieht die EatBehavior-Oberfläche aus

interface EatBehavior { public void eat(); }

Und BarkBehavior

interface BarkBehavior { public void bark(); }

Alle Klassen, die diese Verhaltensweisen darstellen, implementieren die entsprechende Schnittstelle.

Konkrete Klassen für BarkBehavior

public class PlayfulBark implements BarkBehavior { @override public void bark(){ System.out.println("Bark! Bark!"); } } public class Growl implements BarkBehavior { @override public void bark(){ System.out.println("This is a growl"); } public class MuteBark implements BarkBehavior { @override public void bark(){ System.out.println("This is a mute bark"); }

Concrete classes for the EatBehavior

public class NormalDiet implements EatBehavior { @override public void eat(){ System.out.println("This is a normal diet"); } } public class ProteinDiet implements EatBehavior { @override public void eat(){ System.out.println("This is a protein diet"); } }

Now while we make concrete implementations by subclassing the ‘Dog’ superclass, naturally we want to be able to assign the behaviors dynamically to the dogs’ instances. After all, it was the inflexibility of the previous code that was causing the problem. We can define setter methods on the Dog subclass that will allow us to set different behaviors at runtime.

That brings us to another design principle:

Program to an interface and not an implementation.

What this means is that instead of using the concrete classes we use variables that are supertypes of those classes. In other words, we use variables of type EatBehavior and BarkBehavior and assign these variables objects of classes that implement these behaviors. That way, the Dog classes do not need to have any information about the actual object types of those variables!

To make the concept clear here’s an example that differentiates the two ways — Consider an abstract Animal class that has two concrete implementations, Dog and Cat.

Programming to an implementation would be:

Dog d = new Dog(); d.bark();

Here’s what programming to an interface looks like:

Animal animal = new Dog(); animal.animalSound();

Here, we know that animal contains an instance of a ‘Dog’ but we can use this reference polymorphically everywhere else in our code. All we care about is that the animal instance is able to respond to the animalSound() method and the appropriate method, depending on the object assigned, gets called.

That was a lot to take in. Without further explanation let’s see what our ‘Dog’ superclass looks like now:

public abstract class Dog { EatBehavior eatBehavior; BarkBehaviour barkBehavior; public Dog(){} public void doBark() { barkBehavior.bark(); } public void doEat() { eatBehavior.eat(); } }

Pay close attention to the methods of this class. The Dog class is now ‘delegating’ the task of eating and barking instead of implementing by itself or inheriting it(subclass). In the doBark() method we simply call the bark() method on the object referenced by barkBehavior. Now, we don’t care about the object’s actual type, we only care whether it knows how to bark!

Now the moment of truth, let’s create a concrete Dog!

public class Labrador extends Dog { public Labrador(){ barkBehavior = new PlayfulBark(); eatBehavior = new NormalDiet(); } public void display(){ System.out.println("I'm a playful Labrador"); } ... }

What’s happening in the constructor of the Labrador class? we are assigning the concrete instances to the supertype (remember the interface types are inherited from the Dog superclass). Now, when we call doEat() on the Labrador instance, the responsibility is handed over to the ProteinDiet class and it executes the eat() method.

The Strategy Pattern in Action

Alright, let’s see this in action. The time has come to run our dope Dog simulator program!

public class DogSimulatorApp { public static void main(String[] args) { Dog lab = new Labrador(); lab.doEat(); // Prints "This is a normal diet" lab.doBark(); // "Bark! Bark!" } }

How can we make this program better? By adding flexibility! Let’s add setter methods on the Dog class to be able to swap behaviors at runtime. Let’s add two more methods to the Dog superclass:

public void setEatBehavior(EatBehavior eb){ eatBehavior = eb; } public void setBarkBehavior(BarkBehavior bb){ barkBehavior = bb; }

Now we can modify our program and choose whatever behavior we like at runtime!

public class DogSimulatorApp { public static void main(String[] args){ Dog lab = new Labrador(); lab.doEat(); // This is a normal diet lab.setEatBehavior(new ProteinDiet()); lab.doEat(); // This is a protein diet lab.doBark(); // Bark! Bark! } }

Let’s look at the big picture:

We have the Dog superclass and the ‘Labrador’ class which is a subclass of Dog. Then we have the family of algorithms (Behaviors) “encapsulated” with their respective behavior types.

Take a look at the formal definition that I gave at the beginning: the algorithms are nothing but the behavior interfaces. Now they can be used not only in this program but other programs can also make use of it. Notice the relationships between the classes in the diagram. The IS-A and HAS-A relationships can be inferred from the diagram.

That’s it! I hope you have gotten a big picture overview of the Strategy pattern. The Strategy pattern is extremely useful when you have certain behaviors in your app that change constantly.

This brings us to the end of the Java implementation. Thank you so much for sticking with me so far! If you are interested to learn about the Kotlin version, stay tuned for the next post. I talk about interesting language features and how we can reduce all of the above code in a single Kotlin file :)

P.S

I have read the Head First Design Patterns book and most of this post is inspired by its content. I would highly recommend this book to anyone who is looking for a gentle introduction to Design Patterns.