Event BrokerWzór.4 Poprzedni przykład wzorca jest trochę sztuczny. W prawdziwym świecie będziesz chciał, aby stwory, zyskiwały lub traciły bonusy w dowolny sposób. To jest coś, czego nie zrobisz, mając listę jednokierunkową czy łańcuch zobowiązań
Nie chciałbyś permanentnie zmieniać dane swojego stwora. Chcesz, aby ta zmiana działa w tymczasowo i tylko wtedy gdy jest ona potrzebna.
Potrzebny nam będzie scentralizowany komponent. Będzie on trzymał listę wszystkich modyfikatorów dostępnych w grze. Będziemy wyszukiwać te modyfikatory i dodawać je do stworów mając pewność, że wszystko odbywa się w dobrej kolejności.
Ten komponent nazywa się Event Broker. Co ciekawe jest on połączeniem kilku wzorców projektów jak Mediator, Observer, Command
public class Game // mediator pattern
{
public event EventHandler<Query> Queries;
public void PerformQuery(object sender, Query q)
{
Queries?.Invoke(sender, q);
}
}
Klasa Game jest zasadniczo nazywana "Event Brokerem". Jest on centralnym komponentem i przez niego będą przechodzić zdarzenia. Jak użyłem domyślnej implantacji zdarzeń w .NET-ecie, ale wyobrażam sobie, że równie dobrze mogłaby to być delegata Func
W grze będziemy używać zdarzeń i są one przetrzymywane w kolekcji "Queries".
Będziemy wywoływać te zdarzenia i będą one obsługiwane przez klasy, które pod te zdarzenia się przypinały.
Co mają zdarzenia do zapytań o Atak, Obronę i punkty życia danego stwora.
Otóż korzystając ze wzorca Command, nasze potwory będą mogły pytać jaki atak, obronę , punkty życia powinny mieć.
Te zapytania będą wywoływać wszystkie zdarzenia, a raczej modyfikatory/przedmioty, które odwołują się do danej cechy postaci jak Atak czy Obrona
public class Query
{
public string MonsterName;
public enum Argument
{
Attack, Defense, LifePoints
}
public Argument WhatToQuery;
public int Value;
public Query(string name, Argument whatquery, int val)
{
Value = val;
WhatToQuery = whatquery;
MonsterName = name;
}
}
Ta klasa oddzieliła koncepcje zapytania danego parametru stwora od jego głównej klasy Monster2.
W zapytaniu podajemy nazwę potwora oraz statystkę, która nas interesuje.
Musi jednak założyć, że nazwa potwora jest unikatowa, aby ten mechanizm działał poprawnie.
Chyba że gra zakłada, że zbroje noszą wszystkie stwory z tą nazwą.
Jak wygląda nowa definicja klasy potwora.
public class Monster2
{
private Game game;
public string Name;
private int attack, defense, lifepoints;
public Monster2(Game game, string name,
int attack, int defense, int lifepoints)
{
Name = name;
this.attack = attack;
this.defense = defense;
this.lifepoints = lifepoints;
}
public int Attack
{
get
{
var q = new Query(Name, Query.Argument.Attack, attack);
game.PerformQuery(this, q);
return q.Value;
}
}
public int Defense
{
get
{
var q = new Query(Name, Query.Argument.Defense, defense);
game.PerformQuery(this, q);
return q.Value;
}
}
public int LifePoints
{
get
{
var q = new Query(Name, Query.Argument.LifePoints, lifepoints);
game.PerformQuery(this, q);
return q.Value;
}
}
}
Jak widzisz, gdy chcemy pobrać Atak, Obronę czy punkty życia potwora musi o nie zapytać grę.
Klasa Game natomiast posiada kolekcje zdarzeń/modyfikatorów/przedmiotów i ona wywołując je po kolej ustali, jaką wartość danej statystki nasz potwór powinien mieć.
Narysowałem prosty diagram w Paint, aby było to lepsze do zrozumienia.
Teraz nadchodzi magia
Pora na klasę bazową naszego modyfikatora.
Każdy modyfikator/przedmiot doda swoją metodę Handle do tablicy zdarzeń w klasie Game w swoim konstruktorze.
Dodałem też obsługę metody Dispose, aby się pozbyć danego działania modyfikatora, kiedy już go nie chce mieć.
Poza tym nie będziemy mieć też wycieków pamięci.
public abstract class MonsterModifier2 : IDisposable
{
protected Game game;
protected Monster2 creature;
protected MonsterModifier2(Game game, Monster2 creature)
{
this.game = game;
this.creature = creature;
game.Queries += Handle; // subscribe
}
protected abstract void Handle(object sender, Query q);
public void Dispose()
{
game.Queries -= Handle; // unsubscribe
}
}
Metoda Handle jest abstrakcyjna taka by każda klasa przedmiotu/modyfikatora dziedzicząca po niej mogła obsłużyć daną statystkę po swojemu.
Oto klasa złotej zbroi. Sprawdza ona czy idzie zapytanie o Obronę, danego potwora i jeśli tak jest, to zwiększy on obronę o 4 punkty.
public class GoldArmourModifier2 : MonsterModifier2
{
public GoldArmourModifier2(Game game, Monster2 monster)
: base(game, monster) { }
protected override void Handle(object sender, Query q)
{
if (monster.Name == q.MonsterName
&& q.WhatToQuery == Query.Argument.Defense)
{
Console.WriteLine($"Adding GoldArmour to {monster.Name}");
q.Value += 4;
Console.WriteLine($"Defense increased by 4");
}
}
}
Czerwony eliksir sprawdza nazwę potwora i czy pytamy o punkty życia. Jeśli tak to zwiększamy punkty życia o 2.
public class RedPotionModifier2 : MonsterModifier2
{
public RedPotionModifier2(Game game, Monster2 monster)
: base(game, monster) { }
protected override void Handle(object sender, Query q)
{
if (monster.Name == q.MonsterName
&& q.WhatToQuery == Query.Argument.LifePoints)
{
Console.WriteLine($"Adding RedPotion to {monster.Name}");
q.Value += 2;
Console.WriteLine($"Life increased by 2");
}
}
}
Mieć sprawdza nazwę potwora oraz to czy pytamy o atak. Jeśli tak jest to, zwiększamy atak o 1.
public class SwordModifier2 : MonsterModifier2
{
public SwordModifier2(Game game, Monster2 monster)
: base(game, monster) { }
protected override void Handle(object sender, Query q)
{
if (monster.Name == q.MonsterName
&& q.WhatToQuery == Query.Argument.Attack)
{
Console.WriteLine($"Adding Sword to {monster.Name}");
q.Value += 1;
Console.WriteLine($"Attack increased by 1");
}
}
}
Gdybyśmy nie sprawdzali tego "o jaką statystkę pytamy" to kod by działałby nieprawidłowo w zależności od ilości zapytań, wartości naszego potwora byłyby czystą bzdurą.
Teraz gdy mamy przedmioty gotowe wraz z ich metodami Handle.
Zobaczmy, jak działa nasza gra.
Tworze Szamana Orka o statystce 2,3,3. W klauzurze using dodaje kolejne przedmioty do niego
var game = new Game();
var orcShaman = new Monster2(game, "Orc Shaman", 2, 3, 3);
Console.WriteLine("{3} = A:{0}, D:{1}, L:{2}",
orcShaman.Attack, orcShaman.Defense,
orcShaman.LifePoints, orcShaman.Name);
Console.WriteLine("------Start Session------");
using (new SwordModifier2(game, orcShaman))
{
using (new RedPotionModifier2(game, orcShaman))
{
using (new GoldArmourModifier2(game, orcShaman))
{
Console.WriteLine("{3} = A:{0}, D:{1}, L:{2}",
orcShaman.Attack,
orcShaman.Defense,
orcShaman.LifePoints,
orcShaman.Name);
}
}
}
Console.WriteLine("--------End Session--------");
Console.WriteLine("{3} = A:{0}, D:{1}, L:{2}",
orcShaman.Attack, orcShaman.Defense,
orcShaman.LifePoints, orcShaman.Name);
Wewnątrz zasięgu kodu using przedmioty będą działać. Są one w kolejce zdarzeń w klasie Game.
Natomiast gdy wyjdziemy z tej przestrzeni to modyfikatory/przedmioty znikają z kolejki zdarzeń. To znaczy, że nasz orczy szaman wraca do swoich punktów bazowych.
Jak widzisz to rozwiązanie, jest dużo ciekawsze i wzorce projektowe nie muszą być nudne
Wzorce projektowe jak Mediator i Observer pozwalają nam tworzenie zapytań do zdarzeń i pozwalać każdego, kto podłączył się do danego zdarzenia zmodyfikować obiekt.
Trzeba tylko pamiętać, że klasa Mediatora w tym przypadku Game musi być singletonem.