“Патерн Вівторка” #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 коментарів

Add a Comment

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *