5 жовтня 2010 р.

"Патерн Вівторка" #9: Відвідувач (Visitor)

Уявімо собі, що ви нарешті спромоглися створити свою власну компанію, і оскільки вона пристойного розміру, ви вирішили орендувати для неї цілу будівлю. Оскільки у нас держава дуже хороша і дбає про підприємства, щоб у них усе відповідало вимогам, постійно висилаються всякі перевірки. Причому правила, по яких перевіряють ваше підприємство, постійно міняються. Найближчим часом вам слід буде прийнятати багато відвідувачів (visitors), таких як електрик (electrician), сантехнік (plumber), податківець і так далі... Усі вони будуть перевіряти вашу будівлю вздовж і в поперек, проходячи від поверха до поверха від кімнати до кімнати. Я так здогадуюся, що якась схема класів у вас уже появилася у голові. Якщо так, то у мене є наступне питання: де має жити логіка певної перевірки будівлі? Чи має будівля знати як перевіряти електричні щитки, чи це має знати електрик, або чи має знати кімната як перевірити включателі, чи це так само робота електрика? Звичайно, що електрик , який і є відвідувачем, інкапсулює логіку перевірки певних елементів (elements) вашої будівлі.

ВІДВІДУВАЧ


Відвідувач (Visitor) - це дизайн патерн, який дозволяє відділити певний алгоритм від елементів, на яких алгоритм має бути виконаний, таким чином ми можемо легко додати або ж змінити алгоритм без змін до елементів системи. Як на мене це і є однією із найбільш помітних переваг цього патерну.

Давайте глянемо на наш приклад.

Отже, як і було згадано вище, інкапсульована логіка живе у конкретному відвідувачі (Visitor). Ця логіка може бути застосована до елементів (Elements) системи.  Загально кажучи є два інтерфейси, які представляють основу цього дизайн патерну. Ось вони:

IVisitor

public interface IVisitor 
{ 
    void Visit(OfficeBuilding building); 
    void Visit(Floor floor); 
    void Visit(Room room); 
}

IElement

public interface IElement 
{ 
    void Accept(IVisitor visitor); 
} 


ElectricitySystemValidator

Одна із конкретних реалізацій інтерфейсу IVisitor є  ElectricitySystemValidator, яка може виглядати як наведений трішки нижче код. Що цей клас говорить нам? Він говорить, що певна логіка потрібна для елементів живе в одному відвідувачі, який знає що робити для кожного елемента, оскільки інтерфейс вимагає це. Виходячи із цього ми можемо із чистою совістю проводити нашого відвідувача із поверху на поверх та із кімнати до кімнати.

public class ElectricitySystemValidator : IVisitor 
{ 
    public void Visit(OfficeBuilding building) 
    { 
        var electricityState = (building.ElectricitySystemId > 1000) ? "Good" : "Bad"; 
        Console.WriteLine(string.Format("Main electric shield in building {0} is in {1} state.", building.BuildingName, electricityState)); 
    } 

    public void Visit(Floor floor) 
    { 
        Console.WriteLine(string.Format("Diagnosting electricity on floor {0}.", floor.FloorNumber)); 
    } 

    public void Visit(Room room) 
    { 
        Console.WriteLine(string.Format("Diagnosting electricity in room {0}.", room.RoomNumber)); 
    } 
}

PlumbingSystemValidator

Клас PlumbingSystemValidator схожий на ElectricitySystemValidator, але в своїй логіці бере до уваги вік будівлі, щоб приблизно оцінити на скільки сантехнічна частина справна.  Що ще цікаво про цей клас, так це те, що він нічого не робить у кімнатах. Ну хіба ваше підприємство не займається написанням програм, а є якимось хім заводом і вода потрібна у кожній кімнаті.

Elements

До цього часу вже стало зрозуміло, що структура будівлі обхідною. Все починається із Будівлі (OfficeBuilding), яка має поверхи (Floors), і кожен із поверхів може мати багато кімнат. Глянемо на імплементацію поверху.

Floor

public class Floor : IElement 
{ 
    private readonly IList<Room> _rooms = new List<Room>(); 
    public int FloorNumber { get; private set; } 
    public IEnumerable<Room> Rooms { get { return _rooms; } } 

    public Floor(int floorNumber) 
    { 
        FloorNumber = floorNumber; 
    } 

    public void AddRoom(Room room) 
    { 
        _rooms.Add(room); 
    } 

    public void Accept(IVisitor visitor) 
    { 
        visitor.Visit(this); 
        foreach (var room in Rooms) 
        { 
            room.Accept(visitor); 
        } 
    } 
} 

Як можна побачити цей клас мітить метод Accept, який вимагається інтерфейсом, і який приймає відвідувача. В середині цього методу ми виконуємо наш алгорим і, якщо треба, передаємо нашого відвідувача "по колу". Як ми бачимо, ніякі технічні перевірки не виконуються прямо у цьому класі, тому ми можемо бути певні, що якщо у майбутньому слід буде змінити перевірку електросистеми у кімнаті, то це буде зроблено прямо у відвідувачі без будь-яких впливів на клас кімнати.

OfficeBuiling є досить подібним класом, хіба що має багато інших додаткових властивостей. Room взагалі є простим класом, який не агрегує чи компонує інших елементів.

Елементи програми - діаграма



Нічого особливого? Ну якщо ні, то глянемо на код використання дизайн патерну.

Використання

Маємо будівлю із 2-ма поверхами, на кожному є по 3 кімнати. Запускаємо у будівлю електрика і сантехніка як відвідувачів.

var floor1 = new Floor(1); 
floor1.AddRoom(new Room(100)); 
floor1.AddRoom(new Room(101)); 
floor1.AddRoom(new Room(102)); 
var floor2 = new Floor(2); 
floor2.AddRoom(new Room(200)); 
floor2.AddRoom(new Room(201)); 
floor2.AddRoom(new Room(202)); 
var myFirmOffice = new OfficeBuilding("[Design Patterns Center]", 25, 990); 
myFirmOffice.AddFloor(floor1); 
myFirmOffice.AddFloor(floor2); 

var electrician = new ElectricitySystemValidator(); 
myFirmOffice.Accept(electrician); 

var plumber = new PlumbingSystemValidator(); 
myFirmOffice.Accept(plumber);

Вивід:

Main electric shield in building [Design Patterns Center] is in Bad state.
Diagnosting electricity on floor 1.
Diagnosting electricity in room 100.
Diagnosting electricity in room 101.
Diagnosting electricity in room 102.
Diagnosting electricity on floor 2.
Diagnosting electricity in room 200.
Diagnosting electricity in room 201.
Diagnosting electricity in room 202.
Plumbing state of building [Design Patterns Center] probably is in Good condition, since builing is New.
Diagnosting plumbing on floor 1.
Diagnosting plumbing on floor 2.

UML діаграма класів

Чим більше я пишу про дизайн патерни, тим більше я розумію що UML діаграми часто можуть ввести в оману. Ті діаграми, що представлені в GoF книжці насправді хороші, але вони зображають одні із найбільш частіших випадків застосування певного патерну. Таким чином Відвідувача найчастіше зображають як один базовий клас із двома похідними. Базовий визначає, що похідні мають реалізовувати "відвідування" елементів системи. Елемент системи може мати декілька реалізацій також. Загалом можна глянути на таку діаграму нижче:



Але коли я собі думаю, як інакше цей дизайн патерн може бути реалізований, то діаграми будуть виглядати зовсім інакше. Зокрема, із використанням композит дизайн патерну буде тільки один елемент, який містить вкладені елементи.  Або що найпростіше, ваша програма буде мати один клас для відвідувача і один для елемента. Насправді за цим ховається патерн, але ви його можете не розгледіти. (Насправді, якщо ви самі придумали прикол із відвідуванням класів іншим, вам не слід пробував вглядатися там якісь патерни - ви вже розумієте, що й куди).

Переваги і недоліки

Я подумав, що спільнота зможе допомогти мені знайти те чого я не бачу. Тому я перелічу тільки по одній перевазі і недоліку. А далі просто буду дуже радий бачити у коментарях інші версії.

[РЕДАКТОВАНО 02.11.2010]

Переваги
1) відоклемлює алгоритм від елементів, до яких він має бути застосований;
... тут далі переваги перелічені вами ...
Андрій сказав...
Андрію, якщо слідувати теорії сучасної шаблонної розробки (наприклад такої "нудної" штуки як "легка звязаність"), то в принципі "підганяти під патерн Відвідувач" ми взагалі не мали б - практично він повинен використовувати той функціонал який ми йому надали. Тобто наприклад, якщо це електрик, то "Кімната" надає йому тільки відомості про кількість і розташуванн лампочок, тип проводки, метод "ВимірятиСтрумВРозетці" (і т.д. - основні моменти в вас так і описані), а вже він сам ("Електрик") повинен виконати їх обробку (ну тобто тут як ви писали - певним чином відділяється алгоритм тестування електропроводки, але тільки в межах обробки результатів і виведення висновку "дядею Васею-Електріком"). Якщо щось недопоняв - зразу вибачаюсь:)
скоро буде й більше коментарів...

Недоліки
1) неможливо працювати із приватними методами/полями елемента, який відвідується;
... тут далі недоліки перелічені вами ...

Геннадій Омельченко сказав...
Це взагалі недолік патерна Відвідувач, класичний: порушення інкапсуляції, так як інтерфейс Елемента має бути досить розвинений для того, щоб Відвідувач зміг виконати свою роботу 
 скоро буде й більше коментарів...

Дуже дякую за гарні коментарі! Дорогі читачі, не полінуйтеся заглянути в коментарі - там багато цікавих думок про це дизайн патерн.

10 коментарів:

  1. А чому це недолік? З приватними полями і методами працювати не потрібно. Вони тільки для класу. Я б сказав що це теж перевага. ))))

    Наприклад ми можемо мати методи CalcTax() - публічний і CalcFullTax() приватний.
    CalcTax() може викликати CalcFullTax() для "проміжних" розрахунків. Але податківець зможе викликати тільки CalcTax(), що безумовно є перевагою )))

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

    Якщо інші згодяться із твоїм коментарем, тоді я це перемішу у "переваги" і напишу що я мало думаю :)

    ВідповістиВидалити
  3. Це взагалі недолік патерна Відвідувач, класичний: порушення інкапсуляції, так як інтерфейс Елемента має бути досить розвинений для того, щоб Відвідувач зміг виконати свою роботу

    ВідповістиВидалити
  4. Так, щось у тому є. Але із іншої сторони ми всерівно оперуємо тільки паблік властивостамя. Проблема у тому що коли ми будемо писати патерн Відвідувач, ми певно будемо підганяти трішки код під ньго і тоді отримаєму проблему згадану у коменті вище.

    ВідповістиВидалити
  5. Андрію, якщо слідувати теорії сучасної шаблонної розробки (наприклад такої "нудної" штуки як "легка звязаність"), то в принципі "підганяти під патерн Відвідувач" ми взагалі не мали б - практично він повинен використовувати той функціонал який ми йому надали. Тобто наприклад, якщо це електрик, то "Кімната" надає йому тільки відомості про кількість і розташуванн лампочок, тип проводки, метод "ВимірятиСтрумВРозетці" (і т.д. - основні моменти в вас так і описані), а вже він сам ("Електрик") повинен виконати їх обробку (ну тобто тут як ви писали - певним чином відділяється алгоритм тестування електропроводки, але тільки в межах обробки результатів і виведення висновку "дядею Васею-Електріком"). Якщо щось недопоняв - зразу вибачаюсь:)

    ВідповістиВидалити
  6. Андрій, все правильно, так б мало бути. Але як згадано у коментарі від Геннадія, "інтерфейс Елемента має бути досить розвинений". Якщо я правильно розумію Геннадія, то він має на увазі, що коли "Кімната" назовні показує кожну лампочку це дещо порушує інкапсуляцію. Думаю електрику буде мало знати скільки лампочок і де вони є, він захоче на неї подивитися. І ніхто не каже, що інший відвідувач не захоче ще на щось подивитися.

    Але взагалі правильно. Все має бути "легко зв'язано".

    ВідповістиВидалити
  7. Знову ж, слабка зв’язність це, звісно, добре, але... Не завжди вона має таке велике значення, і паттерн Відвідувач мабуть один з таких прикладів, адже Відвідувач створенний для використання САМЕ з цією структурою елементів, і ні для якої іншої, він не призначений для повторного використання, а тому залежність від конкретних особливостей реалізації єлементів на мою думку не має таких критичних наслідків. Гадаю, що шкідливим може бути використання тієї частини відкритого інтерфейсу ( що призначений винятково для використання Відвідувачем для реалізації своїх обов’язків, можливо методу "ВимірятиСтрумВРозетці"), іншими классами-Невідвідувачами (які мабуть також забажають використати зручний для них метод "ВимірятиСтрумВРозетці") тут виправдати посилення зв’язності вже складно. Наскільки я розумію, в С++ можливо було вирішити цю проблему за допомогою friend-класів, але в С# - для мене це поки що загадка.
    А про інкапсуляцію... Так і інкапсуляцію можна порушувати по різному :) Можна все ж таки спробувати абстрагуватися від того, чи зберігає Поверх Лампочки у хеш-таблиці, масиві чи зв’язному списку

    ВідповістиВидалити
  8. Андрію, Геннадій так думав щойно - наче виникає 2 досить сумнівні пролеми - це місце зберігання алгоритму тестування і його розгалуженість (тобто варіанти "логіка в елементі". "логіка в відвідувачі", "логіка і там і там". Мені подобається 3 варіант - тобто Кімната надає інформацію що необхідна для отримання висновків (але вона про це не знає), а вже сам Відвідувач (і лише він для певного набору атрибутів) несе логіку їх обробки. На рахунок інших відвідувачів - вони в принципі теж можуть "ДивтисьНаЛампочку" і "МірятиСтрумВРозетці" - але це їм нічого не дасть якщо вони не реалізують певної логіки досягнення висновку (а якщо реалізують то вони стають електриками:) ).

    А про абстрагування - тут до структур даних взагалі не варто привязуватись:) Патерни то швидше філософія в першу чергу ;)

    ВідповістиВидалити
  9. Клас! Якщо вкладаєш досить багато зусиль у написання статті, то отримуєш коментарі, які навіть кращі за саму статтю. Це дуже радує. Дякую вам, хлопці!

    ВідповістиВидалити
  10. Андрію, та нема за що :) Аби тільки ті коментарі були корисними, а так то я завжди радий обговорити цікаву річ

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