[Из песочницы] Двойная диспетчеризация

Не так давно столкнулся по службе с весьма любопытной задачей. Но нет ничего нового под луной — и задача вам давно знакома: двойная диспетчеризация в 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(ICellVisitor visitor); }

internal interface ICellVisitor { T Visit (RedCell cell);

T Visit (BlueCell cell);

T Visit (GreenCell cell); }

internal class RedCell: ICell { public string Color { get { return «красный»; } }

public T AcceptVisitor(ICellVisitor visitor) { return visitor.Visit (this); } }

internal class BlueCell: ICell { public string Color { get { return «синий»; } }

public T AcceptVisitor(ICellVisitor visitor) { return visitor.Visit (this); } }

internal class GreenCell: ICell { public string Color { get { return «зелёный»; } }

public T AcceptVisitor(ICellVisitor visitor) { return visitor.Visit (this); } } Простой посетитель легко решает нашу задачу: internal class Visitor: ICellVisitor { public string Visit (RedCell cell) { // здесь мы получаем доступ уже не к // ICell, но к RedCell и к её не вынесенному // в интерфейс свойству Color return cell.Color; }

public string Visit (BlueCell cell) { return cell.Color; }

public string Visit (GreenCell cell) { return cell.Color; } } Что ж, попытаемся обобщить имеющееся решение на случай двух элементов. Первое, что приходит в голову — превратить каждую ячейку в посетителя: свой собственный тип она и так знает, а, посетив свою товарку, узнает и её тип, следовательно, решит нашу задачу. Получается примерно вот что (для простоты напишу решение только для красной ячейки, а то кода и без того много): internal interface ICell { T AcceptVisitor(ICellVisitor visitor); }

internal class RedCell: ICell, ICellVisitor { private readonly IProcessor _processor;

public RedCell (IProcessor processor) { _processor = processor; }

public TVisit AcceptVisitor(ICellVisitor visitor) { return visitor.Visit (this); }

public string Color { get { return «red»; } }

public T Visit (RedCell cell) { return _processor.Get (this, cell); }

public T Visit (BlueCell cell) { return _processor.Get (this, cell); }

public T Visit (GreenCell cell) { return _processor.Get (this, cell); } }

interface IProcessor { T Get (RedCell a, RedCell b); T Get (RedCell a, BlueCell b); T Get (RedCell a, GreenCell b); } Теперь всё, что нам остаётся — дописать простой процессор, и задача решена! internal class Processor: IProcessor { public string Get (RedCell a, RedCell b) { return «красное на красном»; }

public string Get (RedCell a, BlueCell b) { return GeneralCase (a, b); } public string Get (RedCell a, GreenCell b) { return GeneralCase (a, b); }

private string GeneralCase (ICell a, ICell b) { return a.Color + » --> » + b.Color; } } Что ж, действительно, решение найдено. Вам оно нравится? Мне — нет. Причины какие: Нам необходимо написать N x N методов Visit: в каждой ячейке принять каждую; добавление ячейки нового цвета заставляет нас писать ещё N+1 новых методов. Если N достаточно велико (два десятка, например), то объём кода и трудозатрат устрашают; Добавление любой новой ячейки необходимо приводит к изменению всех предыдущих. А если они в разных сборках, например? И, вполне вероятно, одну из этих сборок мы не может или не хотим менять; В классы нашей модели мы добавили несвойственные им методы. Нужно пояснить — решить задачу, не добавив вовсе ни одного метода для посетителя невозможно (если вы можете — непременно мне расскажите!), но готовы ли мы терпеть N+1 дополнительный метод (в каждом классе!), не несущий никакой логической нагрузки? Увы, это синтаксический мусор; Вы наверняка заметили, что ячейка стала обобщённой над T, именно: class RedCell: ICell, ICellVisitor Это уже совсем никуда не годится! Я не могу создать ячейку, не определившись заранее, какой тип будет возвращать заходящий в неё посетитель! Полнейший вздор.Разумеется, можно избавиться от обобщения: пусть посетитель ничего не возвращает, но изменяет своё состояние, которое выставит наружу открытым образом, но: (1) я предпочитаю immutable state и программирование, смещённое к функциональному (позвольте мне опустить мотивацию — думаю, она довольно понятна), значит, нужно избегать действий (Action) и стремиться использовать функции (Fun), значит, хорошо бы посетителю возвращать тип.

Надеюсь, перечисленного хватит, чтобы подтвердить моё мнение — решение так себе, однако, есть ещё одно важное замечание, на котором я хочу остановится подробнее. Задумаемся, сколько методов должен содержать IProcessor? N x N. То есть, очень много. Но ведь скорее всего нам нужно специальная обработка для очень небольшого, линейного по N, числа случаев. И тем не менее, мы не можем заранее знать, какие из них нам пригодятся (а мы ведь пишем framework, не правда ли? Структуру классов, основу, которыми все потом будут пользоваться, подключая нашу сборку к своим решениям).Как его можно улучшить? Очевидный шаг: отделим модель от посетителя. Да, пусть, как и прежде, каждая ячейка умеет AcceptVisitor (…), но все методы Visit будут в отдельных классах. Несложно понять, что нам понадобится, в таком случае N+1 класс, каждый из которых содержит N методов Visit. Неслабо, правда? При этом любая новая ячейка приводит к добавлению нового класса + по методу в каждый из уже существующих.

У меня есть решение, которое не обладает этими недостатками, а именно: мне нужно несколько классов (говорю без точности, потому что разные синтаксические красивости вроде fluent interface, от которых я не смог удержаться, прибавляют к этому числу, но использовать ли их — дело вкуса), причём число классов от N зависит; при добавлении новой ячейки мне понадобится добавить не зависящее от N число методов в эти классы.Если (ну, мало ли) вы всё ещё читаете, то задумайтесь на мгновение, можете ли вы предложить решение, удовлетворяющее этим требованиям?

Если да, то здорово, напишите мне, но моё вот:

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

internal class ConcreteDispatcherFactory { public ICellVisitor> CreateDispatcher () { return new PrototypeDispatcher(Do) .TakeRed.WithRed (Do) .TakeGreen.WithBlue (Do); }

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: ICellVisitor> { private readonly Builder _redBuilder; private readonly Builder _greenBuilder; private readonly Builder _blueBuilder;

public PrototypeDispatcher (Func generalCase) { _redBuilder = new Builder(this, generalCase); _blueBuilder = new Builder(this, generalCase); _greenBuilder = new Builder(this, generalCase); }

public IBuilder TakeRed { get { return _redBuilder; } }

public IBuilder TakeBlue { get { return _blueBuilder; } }

public IBuilder TakeGreen { get { return _greenBuilder; } }

public ICellVisitor Visit (RedCell cell) { return _redBuilder.Take (cell); }

public ICellVisitor Visit (BlueCell cell) { return _blueBuilder.Take (cell); }

public ICellVisitor Visit (GreenCell cell) { return _greenBuilder.Take (cell); } } Вот второй: internal class Builder: IBuilder, ICellVisitor where TA: ICell { private Func _takeRed; private Func _takeBlue; private Func _takeGreen; private readonly Func _generalCase;

private readonly PrototypeDispatcher _dispatcher; private TA _target;

public Builder (PrototypeDispatcher dispatcher, Func generalCase) { _dispatcher = dispatcher; _generalCase = generalCase;

_takeRed = (a, b) => _generalCase (a, b); _takeBlue = (a, b) => _generalCase (a, b); _takeGreen = (a, b) => _generalCase (a, b); }

public PrototypeDispatcher WithRed (Func toDo) { _takeRed = toDo; return _dispatcher; }

public PrototypeDispatcher WithBlue (Func toDo) { _takeBlue = toDo; return _dispatcher; }

public PrototypeDispatcher WithGreen (Func toDo) { _takeGreen = toDo; return _dispatcher; }

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 Take (TA a) { _target = a; return this; } } И ещё интерфейс для красоты, чтобы отделить строителя от посетителя (которые в обоих классах сливаются, но зато синтаксис вызова красивый): internal interface IBuilder { PrototypeDispatcher WithRed (Func toDo); PrototypeDispatcher WithBlue (Func toDo); PrototypeDispatcher WithGreen (Func toDo); } В заключение хочу сослаться на серию статей «про волшебников и воинов», где тоже обсуждаются вопросы диспетчеризации в C#.

© Habrahabr.ru