Das Strategy-Muster

Definition

Das Strategy-Muster (Strategie) ist eines der GoFEntwurfsmuster (Design Pattern) und gehört zu der Kategorie der Verhaltensmuster (Behavioral Pattern). Das Muster „definiert eine Familie von Algorithmen, kapselt sie einzeln und macht sie austauschbar. [Es] ermöglicht, den Algorithmus unabhängig von den Clients die ihn einsetzen, variieren zu lassen“ (EvKbF S. 24).

Beschreibung

Die einzig wahre Konstante, auf die man sich bei der Software-Entwicklung immer verlassen kann, ist die Veränderung. Das Ziel sollte daher sein, die Entwürfe stets so zu gestallten, dass Änderungen minimale Auswirkungen auf den bestehenden Code haben. Das Strategy-Muster vereinigt drei OO-Entwurfsprinzipien, die helfen dieses Ziel zu erreichen.

„Kapseln Sie das, was variiert.“

Das Ziel bei diesem Entwurfsprinzip ist, die Aspekte einer Anwendung zu identifizieren, die sich ändern können, und sie von denen zu trennen, die konstant bleiben. Damit kann man die veränderlichen Aspekte später ändern oder erweitern, ohne Auswirkungen auf die konstanten Aspekte befürchten zu müssen. Der Entwurf bietet dadurch mehr Flexibilität und ist gleichzeitig weniger anfällig für unvorhergesehene Fehler, die durch Codeänderungen entstehen können.

„Programmieren Sie auf eine Schnittstelle, nicht auf eine Implementierung.“

In diesem und anderen Artikeln wird mal von „Schnittstelle“ mal von „Interface“ gesprochen. „Auch wenn der Begriff die deutsche Entsprechung des anderen Begriffs ist, werden die beiden Begriffe … für zwei verschiedene Dinge verwendet.“ Mit dem deutschen Wort Schnittstelle wird auf das allgemeine Konzept Schnittstelle verweisen, mit dem englischen Begriff Interface dagegen auf das Konstrukt interface. „Man kann also auf eine Schnittstelle programmieren, ohne dazu tatsächlich ein Interface verwenden zu müssen. Worum es geht, ist, dass man Polymorphismus ausnutzt, indem man auf einen Supertyp programmiert, damit das tatsächliche Laufzeitobjekt nicht im Code festgeschrieben werden muss.“ ‚Auf einen Supertyp programmieren‘ könnte man wie folgt formulieren: „Der deklarierte Typ der Variablen sollte ein Supertyp, in der Regel eine abstrakte Klasse oder ein Interface, sein, damit die Objekte, die dieser Variablen zugewiesen werden, zu einer beliebigen konkreten Implementierung dieses Supertyps gehören können – und das bedeutet, dass die Klasse, die diese Objekte deklariert, die tatsächlichen Objekttypen nicht kennen muss!“ (EvKbF S. 12).

„Ziehen Sie die Komposition der Vererbung vor.“

Entwürfe die Komposition verwenden sind viel flexibler und robuster. Sie ermöglichen eine Familie von Algorithmen zu kapseln und diese wiederzuverwenden. Statt ihr Verhalten nur zu erben, können Objekte ihr Verhalten zur Laufzeit ändern, solange das eingesetzte Objekt die entsprechende Schnittstelle implementiert.

Beispiel

Für eine Enten-Simulation entwerfen wir die nötigen Objekte unter der Berücksichtigung der OO-Entwurfsprinzipien. Als erstes trennen wird die veränderlichen Aspekte von den konstanten Aspekten. Im Vergleich zu lebenden Enten kann eine Gummiente nur quietschen und nicht fliegen, eine Stockente kann weder das eine noch das andere. Diese beiden Aspekte sind also veränderlich und deshalb trennen wir sie von dem konstanten Entenentwurf.

Im zweiten Schritt erstellen wir die nötigen Schnittstellen, auf Basis derer wir die unterschiedlichen Enten- und Verhaltensobjekte implementieren können.

public abstract class Duck
{
	public IFlyBehavior FlyBehavior { get; set; }

	public IQuackBehavior QuackBehavior { get; set; }

	public abstract void Display();

	public void PerformFly()
	{
		this.FlyBehavior.Fly();
	}

	public void PerformQuack()
	{
		this.QuackBehavior.Quack();
	}

	public void Swim()
	{
		Console.WriteLine("Swim");
	}
}

public interface IFlyBehavior
{
	void Fly();
}

public interface IQuackBehavior
{
	void Quack();
}

Als nächstes folgt die Implementierung der konkreten Flug-, Quackverhalten und der Entenobjekte.

public class FlyWithWings : IFlyBehavior
{
	public void Fly()
	{
		Console.WriteLine("Flying");
	}
}

public class FlyNoWay : IFlyBehavior
{
	public void Fly()
	{
		Console.WriteLine("Can't Fly");
	}
}
public class Quack : IQuackBehavior
{
	public void Quack()
	{
		Console.WriteLine("Quack");
	}
}

public class Squeak : IQuackBehavior
{
	public void Quack()
	{
		Console.WriteLine("Squeak");
	}
}

public class MuteQuack : IQuackBehavior
{
	public void Quack()
	{
		Console.WriteLine("Silence");
	}
}
public class MallardDuck : Duck
{
	public MallardDuck()
	{
		this.FlyBehavior = new FlyWithWings();
		this.QuackBehavior = new Quack();
	}

	public override void Display()
	{
		Console.WriteLine("Mallard Duck");
	}
}

public class RubberDuck : Duck
{
	public RubberDuck()
	{
		this.FlyBehavior = new FlyNoWay();
		this.QuackBehavior = new Squeak();
	}

	public override void Display()
	{
		Console.WriteLine("Rubber Duck");
	}
}

public class DecoyDuck : Duck
{
	public DecoyDuck()
	{
		this.FlyBehavior = new FlyNoWay();
		this.QuackBehavior = new MuteQuack();
	}

	public override void Display()
	{
		Console.WriteLine("Decoy Duck");
	}
}

Durch diese Trennung ist es möglich mit Hilfe der Komposition zur Laufzeit das Flug- und/oder Quackverhalten eines Entobjekts zu ändern.

public static void main(String[] args)
{
	Duck mallard = new MallardDuck();
	mallard.PerformQuack();
	mallard.PerformFly();

	// Quack
	// Flying

	mallard.QuackBehavior = new MuteQuack();
	mallard.FlyBehavior = new FlyNoWay();
	mallard.PerformQuack();
	mallard.PerformFly();

	// Silence
	// Can't Fly
}

Durch den Einsatz des Strategy-Musters können nun weitere Flug-, Quackverhalten und Entenobjekte implementiert und miteinander kombiniert werden. Dabei muss der Code der bestehenden Klassen nicht angepasst werden. Der Entwurf ist in Bezug auf Wiederverwendbarkeit und Wartbarkeit sehr flexibel und gleichzeitig werden unvorhergesehene Fehler reduziert.

UML

Strategy Design Pattern UML Diagram

Akteure

  • Strategy (Strategie)
    Definiert eine Schnittstelle für alle unterstützten Algorithmen.
  • ConcreteStrategy (KonkreteStrategie)
    Implementiert einen konkreten Algorithmus.
  • Context (Kontext)
    Enthält eine Referenz eines KonkreteStrategie Objektes, welches verwendet werden soll. Wenn ein Methoden-Aufruf notwendig ist, wird dieser auf dem Strategie Objekt aufgerufen. Dabei ist dem Kontext die konkrete Implementierung des Strategie Objekts nicht bekannt. Falls erforderlich, kann die Schnittstelle des Strategie Objekts so entworfen werden, dass sie Daten aus dem Kontext Objekt empfangen kann. Bei Bedarf kann die Strategie zur Laufzeit dynamisch durch eine andere Implementierung ausgetauscht werden.

Quellen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.