Подвійна диспетчеризація

Ще кілька застережень, перед тим, як сформулювати завдання суворіше: я не зупинятимусь на тому, чому мені не підходять dynamic, явна перевірка типів через приведення і рефлексія. Тому дві причини: 1) мета - позбутися runtime винятків 2) хочу показати, що мова досить виразний, навіть якщо не вдаватися до перерахованих засобів і залишатися в рамках суворої типізації.

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

Приклад:

  • скажімо, є деякий інтерфейсICell, який виконують класиRedCell,BlueCell,GreenCell.
  • є правила такого виду:
  • отримавши два червоні комірки, пишемо «червоне на червоному»
  • отримавши зелену та синю пишемо «берег»
  • у всіх інших випадках - вказати перший і другий колір
  • прямим твором безлічі осередків на себе ж отримуємо:

червоне на червоному червоний --> зелений червоний --> синій зелений --> червоний зелений --> зелений берег синій --> червоний синій --> зелений синій --> синій

Думаю, очевидно, що для вирішення такого завдання нам вистачило б одного виду класу:

Зрозуміло, на практиці потрібно отримати доступ до специфічних властивостей об'єктів, які ніяк не можна винести в загальний інтерфейс, але захаращувати приклад складною доменною областю не з руки, тому умовимося: у загальному інтерфейсі немає ні слова про колір, а всі осередки виглядають ось як ця червона нижче:

Класичний відвідувач

Класикою вже стало вирішення такого завдання з однимелементом. Зупинятись на ньому не стану, докладно про це можна дізнатися, наприклад, у Вікіпедії. У нашому випадку рішення виглядало б таким чином. При такій моделі (позначимо цей код за [1], нижче згадаю про нього):

Простий відвідувач легко вирішує наше завдання:

Застосування відвідувача до завдання

Що ж, спробуємо узагальнити існуюче рішення на випадок двох елементів. Перше, що спадає на думку — перетворити кожен осередок на відвідувача: свій власний тип він і так знає, а, відвідавши свою товарку, дізнається і її тип, отже, вирішить наше завдання. Виходить приблизно ось що (для простоти напишу рішення тільки для червоного осередку, а то коду і так багато):

Тепер все, що нам залишається - дописати простий процесор, і завдання вирішене!

Критика знайденого рішення

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

Зрозуміло, можна позбавитися узагальнення: нехай відвідувач нічого не повертає, але змінює свій стан, який виставить назовні відкритим чином, але: (1) я віддаю перевагу immutable state і програмування, зміщене до функціонального (дозвольте мені опустити мотивацію - думаю, вона досить зрозуміла ), отже, потрібно уникати дій (Action) і прагнути використовувати функції (Fun), отже, добре б відвідувачеві повертати тип. Сподіваюся, перерахованого вистачить, щоб підтвердити мою думку — рішення так собі, однак є ще одне важливе зауваження, на якому я хочу зупинитися докладніше. Замислимося, скільки методів має міститиIProcessor?N x N. Тобто дуже багато. Але ж швидше за все нам потрібна спеціальна обробка для дуже невеликого, лінійного поN, числа випадків. І тим не менш, ми не можемо заздалегідь знати, які з них нам знадобляться (а адже ми пишемо framework, чи не так? Структуру класів, основу, якими всі потім користуватимуться, підключаючи нашу збірку до своїх рішень).

Як його можна покращити? Очевидний крок: відокремимо модель від відвідувача. Так, нехай, як і раніше, кожен осередок вмієAcceptVisitor(. ), але всі методи Visit будуть в окремих класах. Неважко зрозуміти, що нам знадобиться, у такому разіN+1клас, кожен із яких міститьNметодівVisit. Неслабко, правда? При цьому будь-яка нова осередок призводить до додавання нового класу + за методом у кожен із існуючих.

Найкраще знайдене рішення

У мене є рішення, яке не має цих недоліків, а саме: мені потрібно кілька класів (говорю без точності, тому що різні синтаксичні краси на кшталт fluent interface, від яких я не зміг утриматися, додають до цього числа, але чи використовувати їх — справа смаку), причому кількість класів відNзалежить; при додаванні нового осередку мені знадобиться додати не залежить відNчисло методів у ці класи.

Якщо так, то здорово, напишіть мені, але моє ось:

Модель у нас як і раніше виду [1], а от так (щоб трохи заінтригувати терплячого читача) виглядатиме аналог конкретного процесора з попереднього прикладу:

де ColorRetriever - це простий «одинарний» візитер:

[Перед, власне, самим рішенням, невеликий відступ про цей останнійColorRetriever— хочу загострити читацьку увагу на тому, що самі по собі рядки

ще не вирішують поставлене завдання. З їх допомогою ми лише послідовно отримуємо доступ до кожного з двох осередків у вигляді явного типу, не прихованогоінтерфейс, тоді як нам потрібно отримати такий доступ для обох одночасно. Поправка внесена завдяки уважності maxim_0_o, мій йому уклін]

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

І ще інтерфейс для краси, щоб відокремити будівельника від відвідувача (які в обох класах зливаються, зате синтаксис виклику гарний):

На закінчення хочу послатися на серію статей «про чарівників та воїнів», де також обговорюються питання диспетчеризації в C#.

А у нас тут можна отримати грант на тестовий період Яндекс.Хмари. Варто лише у полі «секретний пароль» запровадити «Хабр»