Запізнілий “Патерн Вівторка” #18: Ланцюжок відповідальностей (Chain of Responsibility)
|Уявіть, що ви пішли із своїми друзями в кафе. Кафе дещо специфічне – мало місця, і коли вам приносять якусь страву зазвичай доводиться передавати її наступній людині за столом. Ваш найкращий друг сів найближче до краю, тому першим він і отримує до рук замовлення. Так як він спав мало зранку і любить щось поїсти із м’ясом, то він ніколи не передесть вам м’ясної страви і напою із кавою, допоки не вип’є хоч одну чашку чогось. Наступним після друга сидете ви, а далі ваша подружка, яка знаходиться біля стіни. Вона отримає все останньою, але на щастя вона хоче тільки капучіно, та й передавати їй уже не треба нікому.
Я думаю що весь механізм патерну ланцюжка відповідальностей є зрозумілим – ми маємо набір обробників (handler) або відвідувачів кафе, які вміють обробляти команду або їжу у нашому випадку, а якщо обробити її не вдається, то команда передається наступному обробітнику.
Для прикладу із нашим кафе загальним інтерфейсом відвідувача такого дивного кафе може бути такий базовий клас:
public abstract class WierdCafeVisitor
{
public WierdCafeVisitor CafeVisitor { get; private set; }
protected WierdCafeVisitor(WierdCafeVisitor cafeVisitor)
{
CafeVisitor = cafeVisitor;
}
public virtual void HandleFood(Food food)
{
// If I cannot handle other food, passing it to my successor
if (CafeVisitor != null)
{
CafeVisitor.HandleFood(food);
}
}
}
Як бачимо по замовчуванню їжа просто передається до наступного відвідувача у ланцюжку, якщо такий є.
Тапер глянемо на реалізацію, яка найбільше підходить вашому вибагловому другові:
public class BestFriend : WierdCafeVisitor
{
public List<Food> CoffeeContainingFood { get; private set; }
public BestFriend(WierdCafeVisitor cafeVisitor) : base(cafeVisitor)
{
CoffeeContainingFood = new List<Food>();
}
public override void HandleFood(Food food)
{
if(food.Ingradients.Contains("Meat"))
{
Console.WriteLine("BestFriend: I just ate {0}. It was testy.", food.Name);
return;
}
if (food.Ingradients.Contains("Coffee") && CoffeeContainingFood.Count < 1)
{
CoffeeContainingFood.Add(food);
Console.WriteLine("BestFriend: I have to take something with coffee. {0} looks fine.", food.Name);
return;
}
base.HandleFood(food);
}
}
Реалізації ще двох обробітників – Me та GirlFriend мають бути зрозумілими, але всештаки наведемо реалізацію відвідувача-подружки:
public class GirlFriend : WierdCafeVisitor
{
public GirlFriend(WierdCafeVisitor cafeVisitor) : base(cafeVisitor)
{
}
public override void HandleFood(Food food)
{
if(food.Name == "Cappuccino")
{
Console.WriteLine("GirlFriend: My lovely cappuccino!!!");
return;
}
base.HandleFood(food);
}
}
Все відносно просто – дівчина хоче капучіно, але вона у ланцюжку остання, пому поки друг ви вип’є щось із кавою капучіно вона не отримає.
А тепер використання – створимо два капучіно, два супи і кусок м’яса, створимо наших відвідувачів кафе, та будемо подавати їжу в руки другові:
var cappuccino1 = new Food("Cappuccino", new List<string> {"Coffee", "Milk", "Sugar"});
var cappuccino2 = new Food("Cappuccino", new List<string> {"Coffee", "Milk"});
var soup1 = new Food("Soup with meat", new List<string> {"Meat", "Water", "Potato"});
var soup2 = new Food("Soup with potato", new List<string> {"Water", "Potato"});
var meat = new Food("Meat", new List<string> {"Meat"});
var girlFriend = new GirlFriend(null);
var me = new Me(girlFriend);
var bestFriend = new BestFriend(me);
bestFriend.HandleFood(cappuccino1);
bestFriend.HandleFood(cappuccino2);
bestFriend.HandleFood(soup1);
bestFriend.HandleFood(soup2);
bestFriend.HandleFood(meat);
Вивід:
BestFriend: I have to take something with coffee. Cappuccino looks fine.
GirlFriend: My lovely cappuccino!!!
BestFriend: I just ate Soup with meat. It was testy.
Me: I like Soup. It went well.
BestFriend: I just ate Meat. It was testy.
Як видно із виводу в консоль, дівчина отримала тільки друге капучіно, а ви були змушені їсти суп без м’яса 🙂
Що цікаво, мо ми можемо після моєї дівчини підчепити ще один обробітник – скажімо мішечок для собачки і туди скинути, щось що ніхто не захоче їсти. Для цього прийдеться трішки змінити клас, щоб він мав метод на подобі SetNextVisitor абощо.
Доволі практичний патерн, думаю при обробці подій дозволяє уникнути багатьох "костилів". Та й архітектура в цілому виглядає досить гнучкою і компактною водночас.
Так, патерн дуже практичний. Мій друг на ньому побудував бота, який хендлить ходи і в залежності від поточних умов хендлер передає обробку наступному у ланцюжку, який вже спеціальну логіку добавляє.