7 грудня 2010 р.

Запізнілий "Патерн Вівторка" #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 абощо.


Моя табличка Патернів
Developer's RoadMap To Success

2 коментарі:

  1. Доволі практичний патерн, думаю при обробці подій дозволяє уникнути багатьох "костилів". Та й архітектура в цілому виглядає досить гнучкою і компактною водночас.

    ВідповістиВидалити
  2. Так, патерн дуже практичний. Мій друг на ньому побудував бота, який хендлить ходи і в залежності від поточних умов хендлер передає обробку наступному у ланцюжку, який вже спеціальну логіку добавляє.

    ВідповістиВидалити