Strategy Design Pattern in C++
Ich habe das Strategy Design Pattern in C++ implementiert und zeige euch in diesem Artikel wie es funktioniert, wofür man es einsetzt und welche Vor- beziehungsweise Nachteile es hat. Von all den Pattern ist es mein Lieblings Entwurfsmuster. Auch das werde ich noch etwas näher ausführen.
Strategy Design Pattern in C++
Das Strategie Entwursmuster gehört zur Gruppe der Verhaltensmuster (behavioral design patterns) und ist sehr hilfreich um dem so genannten „design nightmare“ entgegenzuwirken. Bei diesem Pattern geht es darum einen Algorithmus austauschbar und wiederverwendbar zu machen. Unterscheiden sich verwandte Klasse nur in ihrem Verhalten, dann kann man dieses extrahieren und austauschbar machen. Das bedeutet aus einer Funktion wird ein Pointer auf ein Objekt, dass diese Funktion implementiert. Je nach Typ dieses Objekts hat man ein unterschiedliches Verhalten und dank des Pointers ist dieses sogar zur Laufzeit austauschbar.
Beispiel
Bleiben wir beim klassischen Beispiel eines Rollenspiels, dass ja schon beim Factory Design Pattern sehr gut zur Illustration gedient hat. Im letzten Artikel haben wir eine Character Klasse entwickelt von der spezielle Klasse (Player, Orc und Dragon) erben und jede dieser Klassen die attack Methode implementieren. D.h. jeder Typ von Character hat einen anderen Angriffstext. Wir wollen nun einen Schritt weiter gehen und unseren Charakteren Waffen zuordnen. Je nach verwendeter Waffe soll sich das Verhalten beim Angriff unterscheiden. Ein schlechtes Konzept wäre nun jeweils eine Spezialklasse pro Waffe und Charakter zu erstellen zum Beispiel:
- PlayerWithSword
- PlayerWithMac
- PlayerWithFireMagic
- …
Niemand würde so etwas umsetzen wollen. Mit dem Strategy Design Pattern erstellen wir eine AttackBehavior die man bei jeder Klasse setzen und auch zur Laufzeit austauschen kann.
Implementierung
Der SourceCode ist wie immer auf meiner GitHub Seite zu finden. Basis der Implementierung ist ein AttackBehavior Interface (IAttackBehavior), welches pro verfügbarer Waffe ausprogrammiert wird. Dieses Interface beinhaltet lediglich eine Methode performAttack() in der jeweilige Angriff ausprogrammiert wird.
class IAttackBehavior { public: virtual void performAttack(Character* attacker, Character* enemy) = 0; }; class AxeAttack : public IAttackBehavior { public: virtual void performAttack(Character* attacker, Character* enemy); }; class SwordAttack : public IAttackBehavior { public: virtual void performAttack(Character* attacker, Character* enemy); };
Das Interface enthält eine pure virtual Methode performAttack, welche zwar deklariert ist, aber in einer abgeleiteten Klasse implementiert werden muss. Jede spezialisierte Ableitung enthält nun wieder diese performAttack Methode als virtual, diese wird aber ausprogrammiert. Ausprogrammiert könnte das nun beispielsweise so aussehen:
void AxeAttack::performAttack(Character *attacker, Character* enemy) { cout << attacker->getName() << " crushes " << enemy->getName() << endl; }; void SwordAttack::performAttack(Character *attacker, Character* enemy) { cout << attacker->getName() << " slashes " << enemy->getName() << endl; }
Wie wird das Interface nun in unserer Character Basisklasse verwendet?
class Character { protected: string name; double damage; IAttackBehavior* behavior; public: //Constructor Character(string name, double damage) : name(name), damage(damage), behavior(0) {} void attack(Character *enemy); void setAttackBehavior(IAttackBehavior* behavior); const string& getName() const { return this->name; } void setDamage(double damage) { this->damage = damage; } double getDamage() const { return this->damage; } };
Neben den Standardeigenschaften wie name und damage haben wir nun ein behavior. Dieses wird über die setAttackBehavior gesetzt und in der ausprogrammierten attack Methode aufgerufen. Zum Beispiel so:
void Character::attack(Character *enemy) { if(this->behavior) this->behavior->performAttack(this, enemy); }
Zuletzt zeige ich noch wie man dieses Konstrukt in einem kleinen Beispiel verwendet. Dazu schreiben wir folgende main Funktion:
int main(int argc, char **argv) { Character orc("Urbul", 5.0); AxeAttack axe; orc.setAttackBehavior(&axe); Character dragon("Bymarth, The Deathlord", 500.0); FireAttack fire; dragon.setAttackBehavior(&fire); Player player("Gandalf", 10.0); SwordAttack sword; player.setAttackBehavior(&sword); orc.attack(&player); dragon.attack(&player); player.attack(&dragon); //the dragon is hard to defeat -> switch weapon at runtime player.setAttackBehavior(&axe); player.attack(&dragon); return 0; }
Wir erstellen uns zuerst zwei Gegner, einen Orc und einen Dragon. Bei diesen setzen wir jeweils einen anderen Angriff (Axt und Feuer). Danach erstellen wir noch den Spieler und geben ihm ein Schwert. Wir führen danach einige Angriffe aus:
- Orc greift Player mit Axt an
- Dragon greift Player mit Feuer an
- Player greift Drachen mit Schwert an
Da das Schwert leider zu wenig effizient ist und den Panzer des Drachen nicht durchdringen kann wechseln wir nun einfach zur Laufzeit die Waffe und holen uns eine Axt. Danach greift der Spieler den Drachen nochmal mit der Axt an. Die unterschiedliche Ausgabe zeigt uns die korrekte Verwendung der jeweiligen Waffen.
Fazit
Das Strategy Design Pattern in C++ erlaubt uns bestimmte Verhaltensweisen in einem Programm dynamisch umzusetzen. Mir gefällt das Pattern so gut, weil man damit recht schnell bestimmte Verhalten fix implementieren kann und diese dann mit jeglichen Objekten verwenden kann. Gerade wenn man an einer Simulation arbeitet (wie zum Beispiel ein Spiel) lassen sich so gut unterschiedlichste Akteure definieren die unterschiedlichste Verhalten bekommen und diese dann dynamisch auf anderen Akteure anwenden können. Recht schnell bekommt man dadurch eine sehr lebendige Simulation zusammengebaut.