[Из песочницы] Двойная диспетчеризация
Не так давно столкнулся по службе с весьма любопытной задачей. Но нет ничего нового под луной — и задача вам давно знакома: двойная диспетчеризация в C# в терминах статической типизации. Подробнее объясню ниже, но тем, кто и так всё понял, скажу: да, я буду применять «посетителя», но довольно необычно.Ещё несколько оговорок, перед тем, как сформулировать задачу строже: я не буду останавливаться на том, почему мне не подходят dymamic, явная проверка типов через приведение и рефлексия. Тому две причины: 1) цель — избавиться от runtime исключений 2) хочу показать, что язык достаточно выразителен, даже если не прибегать к перечисленным средствам и оставаться в рамках строгой типизации.
Иначе говоря, предположим, что у нас на руках есть некоторая коллекция разнотипных объектов с довольно общим и малосодержательным интерфейсом. Взяв любые два их них мы хотим понять тип каждого и обработать специальным методом.Пример:
Думаю, очевидно, что для решения такой задачи нам хватило бы одного класса вида:
internal class Cell { public Cell (string color) { Color = color; }
public string Color { get; private set; } } Разумеется, на практике нужно получить доступ к специфичным свойствам объектов, которые никак нельзя вынести в общий интерфейс, но загромождать пример сложной доменной областью не с руки, потому условимся: в общем интерфейсе нет ни слова о цвете, а все ячейки выглядят вот как эта красная ниже: internal interface ICell { //Some code }
internal class RedCell: ICell
{
public string Color
{
get { return «красный»; }
}
//Some code
}
Классикой уже стало решение такой задачи с одним элементом. Останавливаться на нём не стану, подробно об этом можно узнать, например, в Википедии. В нашем случае решение выглядело бы так следующим образом. При вот такой модели (обозначим этот код за [1], ниже вспомню о нём):
internal interface ICell
{
T AcceptVisitor
internal interface ICellVisitor
T Visit (BlueCell cell);
T Visit (GreenCell cell); }
internal class RedCell: ICell { public string Color { get { return «красный»; } }
public T AcceptVisitor
internal class BlueCell: ICell { public string Color { get { return «синий»; } }
public T AcceptVisitor
internal class GreenCell: ICell { public string Color { get { return «зелёный»; } }
public T AcceptVisitor
public string Visit (BlueCell cell) { return cell.Color; }
public string Visit (GreenCell cell)
{
return cell.Color;
}
}
Что ж, попытаемся обобщить имеющееся решение на случай двух элементов. Первое, что приходит в голову — превратить каждую ячейку в посетителя: свой собственный тип она и так знает, а, посетив свою товарку, узнает и её тип, следовательно, решит нашу задачу. Получается примерно вот что (для простоты напишу решение только для красной ячейки, а то кода и без того много):
internal interface ICell
{
T AcceptVisitor
internal class RedCell
public RedCell (IProcessor
public TVisit AcceptVisitor
public string Color { get { return «red»; } }
public T Visit (RedCell
public T Visit (BlueCell
public T Visit (GreenCell
interface IProcessor
public string Get (RedCell
private string GeneralCase (ICell a, ICell b)
{
return a.Color + » --> » + b.Color;
}
}
Что ж, действительно, решение найдено. Вам оно нравится? Мне — нет. Причины какие: Нам необходимо написать N x N методов Visit: в каждой ячейке принять каждую; добавление ячейки нового цвета заставляет нас писать ещё N+1 новых методов. Если N достаточно велико (два десятка, например), то объём кода и трудозатрат устрашают;
Добавление любой новой ячейки необходимо приводит к изменению всех предыдущих. А если они в разных сборках, например? И, вполне вероятно, одну из этих сборок мы не может или не хотим менять;
В классы нашей модели мы добавили несвойственные им методы. Нужно пояснить — решить задачу, не добавив вовсе ни одного метода для посетителя невозможно (если вы можете — непременно мне расскажите!), но готовы ли мы терпеть N+1 дополнительный метод (в каждом классе!), не несущий никакой логической нагрузки? Увы, это синтаксический мусор;
Вы наверняка заметили, что ячейка стала обобщённой над T, именно:
class RedCell
Надеюсь, перечисленного хватит, чтобы подтвердить моё мнение — решение так себе, однако, есть ещё одно важное замечание, на котором я хочу остановится подробнее. Задумаемся, сколько методов должен содержать IProcessor? N x N. То есть, очень много. Но ведь скорее всего нам нужно специальная обработка для очень небольшого, линейного по N, числа случаев. И тем не менее, мы не можем заранее знать, какие из них нам пригодятся (а мы ведь пишем framework, не правда ли? Структуру классов, основу, которыми все потом будут пользоваться, подключая нашу сборку к своим решениям).Как его можно улучшить? Очевидный шаг: отделим модель от посетителя. Да, пусть, как и прежде, каждая ячейка умеет AcceptVisitor (…), но все методы Visit будут в отдельных классах. Несложно понять, что нам понадобится, в таком случае N+1 класс, каждый из которых содержит N методов Visit. Неслабо, правда? При этом любая новая ячейка приводит к добавлению нового класса + по методу в каждый из уже существующих.
У меня есть решение, которое не обладает этими недостатками, а именно: мне нужно несколько классов (говорю без точности, потому что разные синтаксические красивости вроде fluent interface, от которых я не смог удержаться, прибавляют к этому числу, но использовать ли их — дело вкуса), причём число классов от N зависит; при добавлении новой ячейки мне понадобится добавить не зависящее от N число методов в эти классы.Если (ну, мало ли) вы всё ещё читаете, то задумайтесь на мгновение, можете ли вы предложить решение, удовлетворяющее этим требованиям?
Если да, то здорово, напишите мне, но моё вот:
Модель у нас по-прежнему вида [1], а вот так (чтобы немного заинтриговать терпеливого читателя) будет выглядеть аналог конкретного процессора из предыдущего примера:
internal class ConcreteDispatcherFactory
{
public ICellVisitor
private string Do (ICell a, ICell b) { return a.Color + »\t-->\t» + b.Color; }
private string Do (GreenCell a, BlueCell b) { return «побережье»; }
private string Do (RedCell a, RedCell b) { return «красное на красном»; } } Как видно, мы оговариваем общий случай и два частных, и этого будет достаточно, чтобы построить решение.По большому счёту, нам понадобятся два класса — первый и второй посетитель. Второй будет обобщённым и первый создаст его типизированный экземпляр с тем, чтобы использовать для конкретной ячейки.Вот первый:
class PrototypeDispatcher
public PrototypeDispatcher (Func
public IBuilder
public IBuilder
public IBuilder
public ICellVisitor
public ICellVisitor
public ICellVisitor
private readonly PrototypeDispatcher
public Builder (PrototypeDispatcher
_takeRed = (a, b) => _generalCase (a, b); _takeBlue = (a, b) => _generalCase (a, b); _takeGreen = (a, b) => _generalCase (a, b); }
public PrototypeDispatcher
public PrototypeDispatcher
public PrototypeDispatcher
public TResult Visit (RedCell cell) { return _takeRed (_target, cell); }
public TResult Visit (BlueCell cell) { return _takeBlue (_target, cell); }
public TResult Visit (GreenCell cell) { return _takeGreen (_target, cell); }
public ICellVisitor