“Патерн Вівторка” #9: Відвідувач (Visitor)
|Відвідувач (Visitor) – це дизайн патерн, який дозволяє відділити певний алгоритм від елементів, на яких алгоритм має бути виконаний, таким чином ми можемо легко додати або ж змінити алгоритм без змін до елементів системи. Як на мене це і є однією із найбільш помітних переваг цього патерну.
public interface IVisitor
{
void Visit(OfficeBuilding building);
void Visit(Floor floor);
void Visit(Room room);
}
public interface IElement
{
void Accept(IVisitor visitor);
}
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));
}
}
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);
}
}
}
Елементи програми – діаграма
Нічого особливого? Ну якщо ні, то глянемо на код використання дизайн патерну.
Використання
Маємо будівлю із 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);
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]
- Андрій сказав…
- Андрію, якщо слідувати теорії сучасної шаблонної розробки (наприклад такої “нудної” штуки як “легка звязаність”), то в принципі “підганяти під патерн Відвідувач” ми взагалі не мали б – практично він повинен використовувати той функціонал який ми йому надали. Тобто наприклад, якщо це електрик, то “Кімната” надає йому тільки відомості про кількість і розташуванн лампочок, тип проводки, метод “ВимірятиСтрумВРозетці” (і т.д. – основні моменти в вас так і описані), а вже він сам (“Електрик”) повинен виконати їх обробку (ну тобто тут як ви писали – певним чином відділяється алгоритм тестування електропроводки, але тільки в межах обробки результатів і виведення висновку “дядею Васею-Електріком”). Якщо щось недопоняв – зразу вибачаюсь:)
- Геннадій Омельченко сказав…
- Це взагалі недолік патерна Відвідувач, класичний: порушення інкапсуляції, так як інтерфейс Елемента має бути досить розвинений для того, щоб Відвідувач зміг виконати свою роботу
скоро буде й більше коментарів…
А чому це недолік? З приватними полями і методами працювати не потрібно. Вони тільки для класу. Я б сказав що це теж перевага. ))))
Наприклад ми можемо мати методи CalcTax() – публічний і CalcFullTax() приватний.
CalcTax() може викликати CalcFullTax() для "проміжних" розрахунків. Але податківець зможе викликати тільки CalcTax(), що безумовно є перевагою )))
Добре, я згоден. Може я й помилився тут. Я дивився трішки із іншої сторони. Тобто при наслідуванні, яке часто використовується у патернах ми можемо працювати із протектед полями і так далі.
Тобто мається на увазі, що може нам треба буде алгоритм, який враховує щось дуже "захайдене" у тому класі.
Якщо інші згодяться із твоїм коментарем, тоді я це перемішу у "переваги" і напишу що я мало думаю 🙂
Це взагалі недолік патерна Відвідувач, класичний: порушення інкапсуляції, так як інтерфейс Елемента має бути досить розвинений для того, щоб Відвідувач зміг виконати свою роботу
Так, щось у тому є. Але із іншої сторони ми всерівно оперуємо тільки паблік властивостамя. Проблема у тому що коли ми будемо писати патерн Відвідувач, ми певно будемо підганяти трішки код під ньго і тоді отримаєму проблему згадану у коменті вище.
Андрію, якщо слідувати теорії сучасної шаблонної розробки (наприклад такої "нудної" штуки як "легка звязаність"), то в принципі "підганяти під патерн Відвідувач" ми взагалі не мали б – практично він повинен використовувати той функціонал який ми йому надали. Тобто наприклад, якщо це електрик, то "Кімната" надає йому тільки відомості про кількість і розташуванн лампочок, тип проводки, метод "ВимірятиСтрумВРозетці" (і т.д. – основні моменти в вас так і описані), а вже він сам ("Електрик") повинен виконати їх обробку (ну тобто тут як ви писали – певним чином відділяється алгоритм тестування електропроводки, але тільки в межах обробки результатів і виведення висновку "дядею Васею-Електріком"). Якщо щось недопоняв – зразу вибачаюсь:)
Андрій, все правильно, так б мало бути. Але як згадано у коментарі від Геннадія, "інтерфейс Елемента має бути досить розвинений". Якщо я правильно розумію Геннадія, то він має на увазі, що коли "Кімната" назовні показує кожну лампочку це дещо порушує інкапсуляцію. Думаю електрику буде мало знати скільки лампочок і де вони є, він захоче на неї подивитися. І ніхто не каже, що інший відвідувач не захоче ще на щось подивитися.
Але взагалі правильно. Все має бути "легко зв'язано".
Знову ж, слабка зв’язність це, звісно, добре, але… Не завжди вона має таке велике значення, і паттерн Відвідувач мабуть один з таких прикладів, адже Відвідувач створенний для використання САМЕ з цією структурою елементів, і ні для якої іншої, він не призначений для повторного використання, а тому залежність від конкретних особливостей реалізації єлементів на мою думку не має таких критичних наслідків. Гадаю, що шкідливим може бути використання тієї частини відкритого інтерфейсу ( що призначений винятково для використання Відвідувачем для реалізації своїх обов’язків, можливо методу "ВимірятиСтрумВРозетці"), іншими классами-Невідвідувачами (які мабуть також забажають використати зручний для них метод "ВимірятиСтрумВРозетці") тут виправдати посилення зв’язності вже складно. Наскільки я розумію, в С++ можливо було вирішити цю проблему за допомогою friend-класів, але в С# – для мене це поки що загадка.
А про інкапсуляцію… Так і інкапсуляцію можна порушувати по різному 🙂 Можна все ж таки спробувати абстрагуватися від того, чи зберігає Поверх Лампочки у хеш-таблиці, масиві чи зв’язному списку
Андрію, Геннадій так думав щойно – наче виникає 2 досить сумнівні пролеми – це місце зберігання алгоритму тестування і його розгалуженість (тобто варіанти "логіка в елементі". "логіка в відвідувачі", "логіка і там і там". Мені подобається 3 варіант – тобто Кімната надає інформацію що необхідна для отримання висновків (але вона про це не знає), а вже сам Відвідувач (і лише він для певного набору атрибутів) несе логіку їх обробки. На рахунок інших відвідувачів – вони в принципі теж можуть "ДивтисьНаЛампочку" і "МірятиСтрумВРозетці" – але це їм нічого не дасть якщо вони не реалізують певної логіки досягнення висновку (а якщо реалізують то вони стають електриками:) ).
А про абстрагування – тут до структур даних взагалі не варто привязуватись:) Патерни то швидше філософія в першу чергу 😉
Клас! Якщо вкладаєш досить багато зусиль у написання статті, то отримуєш коментарі, які навіть кращі за саму статтю. Це дуже радує. Дякую вам, хлопці!
Андрію, та нема за що 🙂 Аби тільки ті коментарі були корисними, а так то я завжди радий обговорити цікаву річ