На той же предметной области, что и в прошлом посте, покажу в чем проблема: требовалось класс 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 в этом посте.