На той же предметной области, что и в прошлом посте, покажу в чем проблема: требовалось класс Engine, который ничего не знает о посетителе, привести к интерфейсу ICarElement, который обеспечивает метод Accept, обращающийся к методу посетителя ICarElementVisitor<TResult>, соответствующий типу Engine. Адаптер решал эту проблему, однако, при необходимости адаптировать к интерфейсу ICarElement значительное количество классов (даже 10), без кодогенератора будет скучно. Каждый адаптер – 12 линий кода. 120 линий кода для адаптации 10 классов – явный перебор, если есть другие решения. А они есть!
Восстановим и упростим предметную область примера:
class CarElement
{
}
class Wheel : CarElement
{
}
class Engine : CarElement
{
}
class Car : CarElement
{
public Wheel[] Wheels = Enumerable.Range(1, 4).Select(_ => new Wheel()).ToArray();
public Engine Engine = new Engine();
}Я оставил супертип у иерархии частей машины. Он играет только роль обобщающего типа. Если супертипа в вашей иерархии нет - можно вместо него использовать System.Object. Никакой ответственности на супертип CarElement не накладывается. Никаких методов типа Accept он не предоставляет. Считаем, что создатель этой иерархии не задумывался о применении паттерна посетитель.Но нам хочется воспользоваться паттерном посетитель. Воспроизведу код из предыдущего поста. Единственное отличие – для класса Engine не используется адаптер.
interface ICarElementVisitor<out TResult>
{
TResult Visit(Wheel visitable);
TResult Visit(Engine visitable);
TResult Visit(Car visitable);
}
class GetNameVisitor : ICarElementVisitor<string>
{
public string Visit(Wheel visitable)
{
return "wheel";
}
public string Visit(Engine visitable)
{
return "engine";
}
public string Visit(Car visitable)
{
return "car";
}
}
class GetChildrenVisitor : ICarElementVisitor<IEnumerable<CarElement>>
{
public IEnumerable<CarElement> Visit(Wheel visitable)
{
yield break;
}
public IEnumerable<CarElement> Visit(Engine visitable)
{
yield break;
}
public IEnumerable<CarElement> Visit(Car visitable)
{
yield return visitable.Engine;
foreach (var wheel in visitable.Wheels)
{
yield return wheel;
}
}
}Еще раз хочу обратить внимание на то что код конкретных посетителей не зависит от способа диспетчеризации вызовов, и потому инвариантен к диспетчеризации. За диспетчеризацию будет отвечать следующий метод-расширение:static class CarElementExtensions
{
public static T Accept<T>(this CarElement element, ICarElementVisitor<T> visitor)
{
var wheel = element as Wheel;
if(wheel != null)
return visitor.Visit(wheel);
var engine = element as Engine;
if (engine != null)
return visitor.Visit(engine);
var car = element as Car;
if (car != null)
return visitor.Visit(car);
throw new NotSupportedException();
}
}Осталось повторить код теста. Он остался неизменен.static class Test
{
public static void TestVisitor()
{
var childrenVisitor = new GetChildrenVisitor();
var nameVisitor = new GetNameVisitor();
CarElement car = new Car();
var elements = car.Walk(e => e.Accept(childrenVisitor));
var elementNames = elements.Select(e => e.Accept(nameVisitor));
foreach (var elementName in elementNames)
{
Console.WriteLine(elementName);
}
}
public static IEnumerable<T> Walk<T>(this T root, Func<T, IEnumerable<T>> next)
{
var q = next(root).SelectMany(n => Walk(n, next));
return new[] { root }.Concat(q);
}
}Таким образом, для того чтобы добавить обход по некоторому классу, достаточно внести его в интерфейс посетителя ICarElementVisitor и добавить несколько строк в метод расширение Accept. Этот подход существенно экономнее, чем написание адаптеров к непослушным классам. Осталось только сделать замечание, что представленный в этом посте подход нельзя называть классическим посетителем.Еще один штрих будет полезен тем, кто уже перешел на .NET 4, либо собирается переходить на него. Написание метода-расширения Accept можно водрузить на рантайм! Вот так:
static class CarElementExtensions
{
public static T Accept<T>(this CarElement element, ICarElementVisitor<T> visitor)
{
return visitor.Visit((dynamic)element);
}
}Решение о том, какой из методов интерфейса ICarElementVisitor нужно вызывать для конкретного объекта CarElement, будет принято прямо во время выполнения программы. Полагаю, что это несколько снизит производительность, возможно стоит относиться к этому осторожно при посещении больших иерархий.Последняя деталь – в связи с неожиданным поведением RuntimeBinder-а, не получается использовать следующую форму объявления интерфейса посетителя:
interface IVisitor<in TVisitable, out TResult>
{
TResult Visit(TVisitable visitable);
}
interface ICarElementVisitor<out TResult> :
IVisitor<Wheel, TResult>,
IVisitor<Engine, TResult>,
IVisitor<Car, TResult>
{
//TResult Visit(Wheel visitable);
//TResult Visit(Engine visitable);
//TResult Visit(Car visitable);
}Binder считает что у интерфейса ICarElementVisitor нет методов Visit. Это действительно так, но такое поведение отличается от поведения компилятора C#. Подозрение о том что это баг есть не только у меня.Код с динамической диспетчеризацией вполне работоспособен с первым объявлением интерфейса ICarElementVisitor в этом посте.