Chain of ItemsWzór.3 Pomyśl, że pracujesz nad daną aplikacją i okazuje się, że coś zostało źle zaprogramowane. Kto jest za to odpowiedzialny? Ty? Twój szef, który dał Ci zadanie? A może analityk aplikacji? A może osoba biznesową źle zrozumiała proces, który próbujesz zaprogramować
To jest właśnie łańcuch odpowiedzialności. Dodatkowo jest to także wzorzec projektowy. Jego zadaniem jest wywołać elementy systemu jeden po drugim.
Implementacja tej filozofii wydaje się prosta. Potrzebna Ci jest lista jednokierunkowa oraz klasa abstrakcyjna, która posłuży za szablon do działania
Na siłę możesz użyć nawet listy jednokierunkowej, której jest już dostępna w .NET. Tak tyle wystarczy ci, by wykorzystać ten wzorzec projektowy.
static void Main(string[] args)
{
Action a1 = () => { Console.WriteLine("A"); };
Action a2 = () => { Console.WriteLine("C"); };
Action a3 = () => { Console.WriteLine("E"); };
Action a4 = () => { Console.WriteLine("Z"); };
Action a5 = () => { Console.WriteLine("R"); };
Action a6 = () => { Console.WriteLine("Y"); };
Action aColorR = () => { Console.ForegroundColor = ConsoleColor.Red; };
Action aColorG = () => { Console.ForegroundColor = ConsoleColor.Green; };
var printWords = new LinkedList<Action>();
printWords.AddFirst(a1); //A
printWords.AddBefore(printWords.First, a4); //ZA
printWords.AddFirst(a3); //EZA
printWords.AddFirst(aColorR);
foreach (Action s in printWords)
s.Invoke();
Console.WriteLine("-------------------");
printWords.RemoveFirst();
printWords.AddLast(a6); //EZAY
printWords.AddBefore(printWords.Last, a5); //EZARY
printWords.AddFirst(a2); //CEZARY
printWords.AddFirst(aColorG);
foreach (Action s in printWords)
s.Invoke();
}
Oto przykład listy jednokierunkowej. Każdy element z listy zna adres do następnego elementu. LinkedList daje nam możliwość dodawania elementu jako ostatnich, przedostatnich, pierwszych oraz drugich .
Korzystając z właściwości Previous i Next możesz też uzyskać dostęp do każdego elementy tej listy. Tak jak pisałem każdy element, wie, jaki jest element następny i poprzedni
Rezultat kodu jest następujący:
Jaki jest cel tego wzorca projektowego?
- Obsługa wysłanego żądania przez łańcuch obiektów, który obsługują te żądanie lub przekazuje go dalej.
- Wysłanie zadania do łańcucha, który zawiera wiele handlerów (obiektów, które mogą obsłużyć żądanie)
Teraz użyjmy przykładu z potworami, byś mógł zobaczyć jak ten wzorzec wykorzystać, budując go od zera.
Scenariusz
Wyobraź sobie grę komputerową, w której każdy potwór ma trzy cechy : Atak, Obrona i Punkty życia.
public class Monster
{
public string Name;
public int Attack, Defense, LifePoints;
public Monster(string name, int attack,
int defense, int lifepoints) { }
}
Teraz każdy potwór poza swoimi bazowymi punktami może zostać ulepszony. Czyli dostać magiczny przedmiot, który zwiększy jego statystki.
Do zmodyfikowania tych rzeczy będzie nam potrzebny kod tylko jak to zrobić.
Co więcej, każdy potwór będzie mógł mieć więcej taki przedmiotów, które będą działać na siebie po kolej jak w liście jednokierunkowej i dawać wynik zbiorowy na naszym potworze
Łańcuch metod
Oto klasyczna implementacja Chain Of Responsibility.
public class MonsterModifier
{
protected Monster monster;
protected MonsterModifier next;
public MonsterModifier(Monster monster)
{
this.monster = monster;
}
public void Add(MonsterModifier mm)
{
if (next != null) next.Add(mm);
else next = mm;
}
public virtual void Handle() => next?.Handle();
}
Dużo się tutaj dzieje więc po kolei:
- Klasa ma w sobie referencje do Monster i planuje ją zmodyfikować
- Tak klasa nie robi dużo. Nie jest jednak klasą abstrakcyjną . Wszystkie metody w tej klasie mają swoją implementację. Pole next przechowuje informacje o następnym modyfikatorze, o ile będzie on istniał. Ten następny modyfikator będzie dziedziczył po MonsterModifier.
- Metoda Add() dodaje kolejny modyfikator do łańcucha. Jeśli następny element jest pusty, to on tam trafia, jeśli nie to szukamy po następnych elementach, aż trafimy na koniec tego łańcucha, gdzie pole next będzie na pewno puste.
- Metoda Handle() wykonuje swoją modyfikację, a potem przekazuje ją dalej do łańcucha, o ile istnieje następny element w tym łańcuchu. Metoda ta jest wirtualna by można było ją nadpisać
Jeśli jest to dla Ciebie zbyt trudne, to nie martw się za chwilę, wszystko będzie jasne.
Na razie mamy prymitywną listę jednokierunkową. Zacznijmy po niej dziedziczyć, aby zobaczyć, jak ten wzorzec działa.
Oto złota zbroja, która zwiększy punkt obrony o 4.
public class GoldArmourModifier : MonsterModifier
{
public GoldArmourModifier(Monster monster)
: base(monster) { }
public override void Handle()
{
Console.WriteLine($"Adding GoldArmour to {monster.Name}");
monster.Defense += 4;
Console.WriteLine($"Defense increased by 4");
base.Handle();
}
}
Ten modyfikator dziedziczy po MonsterModifier i ma swoją własną metodę Handle. Oczywiście wywołuję on następną metodę Handle, aby mógł powstać łańcuch wywołań.
Stworzymy jeszcze parę takich modyfikatorów.
Oto czerwony eliksir, który doda dwa życia.
public class RedPotionModifier : MonsterModifier
{
public RedPotionModifier(Monster monster)
: base(monster) { }
public override void Handle()
{
Console.WriteLine($"Adding RedPotion to {monster.Name}");
monster.LifePoints += 2;
Console.WriteLine($"Life points increased by 2");
base.Handle();
}
}
A ten miecz doda jeden punkt ataku.
public class SwordModifier : MonsterModifier
{
public SwordModifier(Monster monster)
: base(monster) { }
public override void Handle()
{
Console.WriteLine($"Adding Sword to {monster.Name}");
monster.Attack += 1;
Console.WriteLine($"Attack increased by 1");
base.Handle();
}
}
Mając to wszystko dodajmy te modyfikatory do naszego potwora Orka
var orc = new Monster("Orc", 3, 3, 3);
Console.WriteLine("{3} = A:{0}, D:{1}, L:{2}"
, orc.Attack, orc.Defense, orc.LifePoints,
orc.Name);
// Name: orc, Attack: 3, Defense: 3, Life : 3
var root = new MonsterModifier(orc);
root.Add(new SwordModifier(orc));
root.Add(new RedPotionModifier(orc));
root.Add(new GoldArmourModifier(orc));
root.Handle();
Console.WriteLine("{3} = A:{0}, D:{1}, L:{2}"
, orc.Attack, orc.Defense, orc.LifePoints,
orc.Name);
// Name: orc, Attack: 4, Defense: 7, Life : 5
Jak widzisz nasz dzielny Ork, uzyskał więcej punktów ataku, obrony i życia dzięki naszym przedmiotom.
Możesz też pobawić się w odwoływanie całego łańcucha tych modyfikatorów.
Utwórzmy klątwę, która sprawia, że cały potwór jest goły.
public class CurseBeNakedModifier : MonsterModifier
{
public CurseBeNakedModifier(Monster creature)
: base(creature) { }
public override void Handle()
{
Console.WriteLine("You are naked!");
}
}
Jest to proste, po prostu nie wywołujesz następnym modyfikatorów w łańcuchu.
Pamiętaj jednak, że ta klątwa musi być na początku łańcucha by rzeczywiście zadziała ona tak jak my tego chcemy
Podsumowanie:
Łańcuch zobowiązań, który wykonuje dane polecenia w określonej kolejności.
Pokazałem Ci najprostszą implementację tego wzorca. Prawdopodobnie to samo mógłbyś osiągnąć, korzystając z List<T> bądź LinkedList<T>.
Te klasy generyczne w końcu mają własną logikę usuwania rzeczy z łańcucha.
Ten wzorzec rozwiązuje pewien problem, ale co by było, gdybyś chciał te modyfikatory uruchamiać, na żądanie jak byś wysłał poleceniem ("Command") w grze.
Jakiś obiekt musi to obserwować i innym obiekt musiałby być mediatorem, aby było to możliwe.
Jest na to rozwiązanie, które nazywa się Event Broker i łączy on kilka wzorców projektowych jak: Mediator, Command i Observer
Zobaczymy te rozwiązanie w następnym wpisie.