Можно ли прилично сделать Visitor в C#?
interface IThingWithVisit1
{
string Description {get;}
}
class Visitor1 {
public static void Visit(IThing thing)
{
var visit1 = thing as IThingWithVisit1;
if (visit1 != null)
Console.WriteLine(visit1.Description);
else {
throw new ApplicationException(
string.Format("I do not know about this thing '{0}'!", thing.GetType.FullName;
}
}
}
partial class Milk: IThingWithVisit1
{
string IThingWithVisit1.Description {get {return this.Volume + " liters of milk";}}
}
partial class Cow:IThingWithVisit1
{
string IThingWithVisit1.Description {get {return this.Weight + " kgs of a raw beef";}}
}
Вывод на экран строчек тут - только для примера, чтобы понятно было, что имеется в виду.
В реальной жизни там вместо вывода строчек может быть какая-то обработка, меняющая внутреннее состояние visitor-а, а объекты на вход приходят совершенно разные, для которых нужна совершенно разная обработка. Эту обработку в принципе нельзя выносить в объекты Thing, она должна оставаться в Visitor-ах; вопрос в том, как лучше сделать, чтобы для каждого типа Thing была своя обработка.
Partial-классы - хорошая идея в некоторых случаях, но не в этом.
Вывод на экран строчек тут - только для примера, чтобы понятно было, что имеется в виду.без разницы, да хоть винт форматирует, или снаряд на луну закидывает.
меняющая внутреннее состояние visitor-азначит в метод объекта надо передавать сам visitor.
Эту обработку в принципе нельзя выносить в объекты Thing, она должна оставаться в Visitor-ах;вот это спорное утверждение.
если обработка разная для разных типов, то ее имеет смысл переносить в сам тип, если к самому типу эта обработка имеет косвенное отношение, то удобно эту обработку оформлять через partial классы.
interface IThingWithVisit1
{
void Visit(Visitor1 visitor);
}
class Visitor1 {
public void Visit(IThing thing)
{
var visit1 = thing as IThingWithVisit1;
if (visit1 != null)
visit1.Visit(this);
else {
throw new ApplicationException(
string.Format("I do not know about this thing '{0}'!", thing.GetType.FullName;
}
}
}
partial class Milk: IThingWithVisit1
{
void IThingWithVisit1.Visit(Visitor1 visitor){Console.WriteLine( this.Volume + " liters of milk");}
}
partial class MadCow:IThingWithVisit1
{
void IThingWithVisit1.Visit(Visitor1 visitor){Hdd.Format;}
}
partial class Worker:IThingWithVisit1
{
void IThingWithVisit1.Visit(Visitor1 visitor){visitor.ChangeState;}
}
без разницы, да хоть винт форматирует, или снаряд на луну закидывает.
значит в метод объекта надо передавать сам visitorЯ не хочу, чтобы всякие там коровы форматировали мой винчестер.
А для того, чтобы они могли менять состояние visitor-а - мне надо его поля состояния делать публичными.
если обработка разная для разных типов, то ее имеет смысл переносить в сам тип, если к самому типу эта обработка имеет косвенное отношение, то удобно эту обработку оформлять через partial классы.Обработка имеет крайне косвенное отношение к типу (корова не знает, что её зарубят на мясо, а её молоко чаще разливают в картонные пакеты, чем в пластиковые). Кроме того, у коровы нет доступа к заводу, где происходит фасовка молока по пакетам, и ты не должен хотеть научить её работать на этом заводе даже через partial-классы - её всё равно туда не пустят.
в общем случае это будет выглядеть такЯ понял, как это будет выглядеть.
Твоё решение не оставляет никакой изоляции; нам придётся разрешить менять внутреннее состояние visitor-а снаружи (откуда угодно, а не только из MadCow - мы ведь никак не можем разграничить доступ); не на заводе будут знать, как расчленять коров и куриц, а коровы будут знать, как им надо расчленять себя на российских, а как - на американских заводах.
вот это спорное утверждение.это утверждение пенартура правильное, слишком категоричное, но правильное.
в твоем случае модельные классы должны "знать" о всех реализованных визиторах, более того содержать их код.
это называется умным словом tight coupling и есть highly discouraged anti-pattern.
это называется умным словом tight coupling и есть highly discouraged anti-pattern.если в задаче есть n*k - различных поведений, то как это не обзывай анти-паттернами, но другого решения не будет.
какая разница: что визиторы знают про все объекты, что объекты знают про все визиторы?
ps
это будет анти-патерном, только если задачу можно так переформулировать, что n*k - замениться на n+k
какая разница: что визиторы знают про все объекты, что объекты знают про все визиторы?большая
разница очень большаяв чем именно?
что чаще будет добавляться виды объектов, или визиторы?
обычно чаще добавляются объекты, и получается, что при таких частых добавлениях, надо будет залезть в код каждого визитора и добавить обработку этого объекта.
при чем все это будет делаться на страшных и тормозных лестницах if-ов.
большаят.е ты пишешь, и предлагаешь писать вот такой код?:
public static void Visit(IThing thing) {
if(thing is Milk) {
Console.WriteLine("White milk");
} else if(thing is Cow) {
Console.WriteLine(thing.Color + " cow");
} else if(thing is Chameleon) {
Console.WriteLine("Chameleon");
} else if(thing is Laptop) {
Console.WriteLine(thing.Color + " laptop");
} else {
throw new ApplicationException("I do not know about this thing!");
}
при чем все это будет делаться на страшных и тормозных лестницах if-ов.Ты не прочитал мой пост целиком?
Там в конце есть работающее решение без всяких ифов.
какая разница: что визиторы знают про все объекты, что объекты знают про все визиторы?Какая разница - на заводе по производству фарша и на заводе по производству филе знают про то, что есть курицы и коровы, или курицы и коровы знают про то, что есть завод по производству фарша и завод по производству филе?
визитор обычно делается наоборот.Визитор обычно делается наоборот.
interface IUserVisitor
{
string HelloPenartur2(Penatrur2 user);
string Hello( user);
string Hello( user);
}
interface IUser
{
string SayHelloTo(IUserVisitor visitor);
}
class Penatrur2 : IUser
{
public string SayHelloTo(IUserVisitor visitor) { return visitor.HelloPenartur2(this); }
}
class : IUser
{
public string SayHelloTo(IUserVisitor visitor) { return visitor.Hello(this); }
}
class : IUser
{
public string SayHelloTo(IUserVisitor visitor) { return visitor.Hello(this); }
}
class IntellectCountVisitor : IUserVisitor
{
public string HelloPenartur2(Penatrur2 user) { return int.MinValue.ToString; }
public string Hello( user) { return 10.ToString; }
public string Hello( user) {return 10.ToString; }
}
class ReadGoFVisitor : IUserVisitor
{
public string HelloPenartur2(Penatrur2 user) { return "no"; }
public string Hello( user) { return "yes"; }
public string Hello( user) { return "no"; }
}
class Program
{
static void Main(string[] args)
{
IntellectCountVisitor icv = new IntellectCountVisitor;
ReadGoFVisitor rgv = new ReadGoFVisitor;
IUser[] users = new IUser[3] { new new new Penatrur2 };
foreach (IUser u in users)
{
Console.WriteLine(u.GetType.ToString + " intellect @ " + u.SayHelloTo(icv;
Console.WriteLine(u.GetType.ToString + " has read GoF " + u.SayHelloTo(rgv;
}
}
}
Применение: 1. развязка типов, 2. часто меняются операции над некоторыми объектами.
public string HelloPenartur2(Penatrur2 user) { return int.MinValue.ToString; }Похоже, что всё-таки наоборот.
public string Hello( user) { return 10.ToString; }
Потому что (ты ведь не читал первый пост?) твоё решение в точности совпадает с тем шаблонным, которое в конце моего первого поста (за исключением того, что у меня методы Hello для разных пользователей называются одинаково).
Потому что (ты ведь не читал первый пост?) твоё решение в точности совпадает с моим (за исключением того, что у меня методы Hello для разных пользователей называются одинаково).Я прочитал твой пост и ответил на него. А сейчас я отвечаю -ю.
Я прочитал твой пост и ответил на него. А сейчас я отвечаю -ю.И почему же ты в своём ответе -ю так высоко оценил свои интеллектуальные способности, и так низко - мои?
Кстати, твой пример, наоборот, показывает, что лучше эту информацию вынести в классы пользователей (например, объект пользователя лучше знает, читал ли он GoF, чем Visitor). Впрочем, в твоём примере вообще можно обойтись без visitor-ов.
обычно чаще добавляются объекты, и получается, что при таких частых добавлениях, надо будет залезть в код каждого визитора и добавить обработку этого объекта.Ага, только Visitor прекрасно ложится на дерево объектов, расслаивая его по уровням. Допустим от сервера может придти ответ, ответ - это ошибка или ок, ок - это команда1 или команда2. Получается
...response.AcceptResponse(this)
OnError throw...
OnOk(OkResponse r) r.AcceptOk(this)..
OnCommand1(Command1 c)
OnCommand2(Command2 c)
Таким образом для групп объектов может быть общая обработка, что в итоге удобнее, если паттерн не переворачивать.
обычно чаще добавляются объекты
т.е ты пишешь, и предлагаешь писать вот такой код?:см. код майка
и что мы выиграли?
кода столько же, но при добавление юзеров надо бегать по куче файлов.
видов юзеров обычно много больше, чем визиторов, соответственно операция добавления юзера чаще.
Таким образом для групп объектов может быть общая обработка, что в итоге удобнее если паттерн не переворачиватьтак это можно и через наследование объектов сделать, особенно если mixin-ы использовать.
кода столько же, ноон лежит в нужном месте.
Смотри пример с ответами от сервера. Ответ - ошибка или успех; он может содержать в себе (в зависимости от конкретного типа) какие-то дополнительные данные; в конкретном visitor-е мы знаем, как обрабатывать некоторые из ответов. А ты предлагаешь перенести эту обработку в сами ответы.
Кроме того, повторю - у ответов может просто не хватить прав на совершение нужных действий. В том же примере с сумасшедшей коровой - у нас в пакете с обработчиками и остальным ядерным функционалом есть граната; если приходит соответствующая команда - Visitor эту гранату взрывает. Ты предлагаешь сделать гранату доступной для всех желающих снаружи этого внутреннего защищённого пространства имён с проверенным безопасным кодом?
и что мы выиграли?Твой код очень слабо похож на паттерн. Я вообще понять не могу, что там за ерунда и зачем там приведение типов.
кода столько же, но при добавление юзеров надо бегать по куче файлов.
видов юзеров обычно много больше, чем визиторов, соответственно операция добавления юзера чаще.
При добавлении объектов в этом паттерне надо либо бегать по визиторам, либо работать с группами, либо иметь обработку по дефолту. Цитирую "добавление новых элеменетов затруднено". Паттерн не для удобного добавления большого количества объектов с одной операцией, а для добавления операций над объектами. В первом случае вообще используют виртуальный метод.
так это можно и через наследование объектов сделать, особенно если mixin-ы использовать.у тебя в голове каша
у тебя в голове кашаМБ он просто не выспался?
Совершенно не ожидал от него такую хрень услышать.
так это можно и через наследование объектов сделать, особенно если mixin-ы использовать.Слабо понял эту твою фразу. Но я тебе просто описал, почему при добавлении новых элементов я не буду бегать по куче файлов. Ещё раз:
- Одна операция и много объектов - виртуальный метод
- Много операций и много объектов - визитор
- Одна операция и иерархия объектов - визитор или виртуальный метод
Я вообще понять не могу, что там за ерунда и зачем там приведение типов.Приведение типов - типа для того, чтобы у нас в visitor не засунули объект, который этот конкретный тип visitor-а не поддерживает, и для того, чтобы на засовывание туда абстрактного объекта не выругался компилятор.
у тебя в голове кашаможет просто кому-то лень думать.
может просто кому-то лень думать.Я тоже что-то не вижу силы твоих аргументов... Мало что понял из твоих выкладок и кода.
Я тоже что-то не вижу силы твоих аргументов... Мало что понял из твоих выкладок и кода.что такое расщепление и зачем оно делается - понятно?
что такое переворот двойной передачи и зачем оно делается - понятно?
...response.AcceptResponse(this)о чем сказать хочешь - понятно, но приведи, плиз, полный код (в том числе и с обработкой групп) чтобы проще было обсуждать.
OnError throw...
OnOk(OkResponse r) r.AcceptOk(this)..
OnCommand1(Command1 c)
OnCommand2(Command2 c)
что такое расщепление и зачем оно делается - понятно?Да, или нет. Нет или да.
что такое переворот двойной передачи и зачем оно делается - понятно?
Этот пост только сильнее меня запутывает. Ещё раз, начиная с момента "визитор предназначен для определения разных операций над множеством объектов, в нём трудно добавлять новые объекты". И попроще.
Ещё раз, начиная с момента "визитор предназначен для определения разных операций над множеством объектов, в нём трудно добавлять новые объекты".операции бывают разные.
есть операции, которые слабо относятся к объекту (могут быть реализованы более менее независимо от объекта) - тогда используется твой вариант
но есть операции, которые сильно завязаны на состояние объекта - например, Open/Close, Draw/Move/Rotate,
тогда будет скорее использоваться перевернутый вариант
interface IThing
{
void Open;
void Close;
void Draw;
void Move;
void Rotate;
}
class OpenVisitor:IVisitor
{
public void Visit(IThing thing)
{
thing.Open;
}
}
class DrawVisitor:IVisitor
{
public void Visit(IThing thing)
{
thing.Draw;
}
}
часто бывает, что опреация имеет смысл не для всех объектов, а только для некоторых, тогда удобно использовать расщепление
interface IThing
{
}
interface IThingWithOpenClose
{
void Open;
void Close;
}
interface IThingWithDraw
{
void Draw;
}
class OpenVisitor:IVisitor
{
public void Visit(IThing thing)
{
if (thing is IThingWithOpenClose)
IThingWithOpenClose)thing).Open;
}
}
class DrawVisitor:IVisitor
{
public void Visit(IThing thing)
{
if (thing is IThingWithDraw)
IThingWithDraw)thing).Draw;
}
}
или для обратного варианта
interface IUserVisitor_Penartur2
{
string HelloPenartur2(Penatrur2 user);
}
interface IUserVisitor_
{
string Hello( user);
}
interface IUserVisitor_
{
string Hello( user);
}
interface IThingВ этом коде не ясно, что за задача стоит, зачем здесь Visitor и почему он так называется. Почему я не пишу сразу new Context; foreach(thing...) thing.Draw(context без вызовов двух виртуальных функций?
{
void Open;
void Close;
void Draw;
void Move;
void Rotate;
}
class OpenVisitor:IVisitor
{
public void Visit(IThing thing)
{
thing.Open;
}
}
class DrawVisitor:IVisitor
{
public void Visit(IThing thing)
{
thing.Draw;
}
}
часто бывает, что опреация имеет смысл не для всех объектов, а только для некоторых, тогда удобно использовать расщеплениеОпять же, не понимаю... Я получил какой-то объект или группу объектов. Я сначала их разделю на те, что умеют сохраняться и те, что умеют отрисовываться. А затем вызову сохранение или отрисовку. Всё, две функции, отсутствие reflection.
class ThingWithDraw : IThing {}
class ThingWithOpen : IThing {}
void VisitDraw(..)
void VisitOpen(...)
Я сначала их разделю на те, что умеют сохраняться и те, что умеют отрисовываться.Ну да, да, надо было разделить на три... И ещё упомянуть о том, что объекты могут принадлежать вообще разной иерархии типов. Тогда вообще не ясно, на кой чёрт там визитор.
В этом коде не ясно, что за задача стоит, зачем здесь Visitor и почему он так называется. Почему я не пишу сразу new Context; foreach(thing...) thing.Draw(context без вызовов двух виртуальных функций?допустим что-нибудь банальное, последовательность обходов набирается динамически (например, редактируется пользователем)
var scenario1 = new IVisitor[]{new OpenVisitor new DrawVisitor new CloseVisitor};
var scenario2 = new IVisitor[]{new MoveVisitor(12, 3 new RotateVisitor(90 new MoveVisitor(3,7 new DrawVisitor};
Не бывает универсальных паттернов на все случаи жизни. У каждого - достаточно строгий узкий круг применимости. Попытка их расширить приводит к той самой каше.
допустим что-нибудь банальное, последовательность обходов набирается динамически (например, редактируется пользователем)Это паттерн Strategy. Или Command, если отправляется одному объекту.
var scenario1 = new IVisitor[]{new OpenVisitor new DrawVisitor new CloseVisitor};
var scenario2 = new IVisitor[]{new MoveVisitor(12, 3 new RotateVisitor(90 new MoveVisitor(3,7 new DrawVisitor};
визитор обычно делается наоборот.
-1 тебе visitor пишется именно так как написал. этот подход используется когда структура данных более менее постоянна, а основное развитие идет написанием новых способов обработки. например данные - это AST программы на каком-нибудь языке программирования, то есть имеются классы там не знаю DeclareClass, DeclareMethod, Statements, Assing, Call, Operator, ... их много, но новые будут появляться не часто. а вот способов обработки "открытое" множество, сначала надо написать интерпретатор, потом компилятор, потом профайлер, дебаггер, визуализатор, ... если ты по каждой новой задаче будешь писать новые методы в классы самого AST (а не дай бог еще и добавлять туда данные, необходимые для этой обработки) то довольно быстро получится помойка. а вот подход через visitor-ы позволяет разные алгоритмы обработки разнести и отделить их от собственно данных. теперь когда возникает новая задача, мы не новые методы дописываем, а новый класс, полностью инкапсулируя в нем все тонкости этого конкретного алгоритма. так то (С)
только я так и не понял, какие у -а с применением всего этого проблемы?
> в каждом классе, унаследованном от Thing, надо писать один и тот же код (а если забудем - то будут очень трудноуловимые баги)
я еще раз повторяю, предполагается, что новые классы-данные возникают редко, в основном пишутся новые классы-visitor-ы. если это не так, значит паттерн не очень подходит. да и что это за мифические "трудноуловимые баги" непонятно, забыл метод интерфейса определить, получил ошибку компиляции, чего тут трудноуловимого то?
только я так и не понял, какие у -а с применением всего этого проблемы?Необязательно новые классы данных возникают редко. Главное - что из соображений изоляции, обработка должна быть в специальном классе-обработчике, а не в классе данных.
> в каждом классе, унаследованном от Thing, надо писать один и тот же код (а если забудем - то будут очень трудноуловимые баги)
я еще раз повторяю, предполагается, что новые классы-данные возникают редко, в основном пишутся новые классы-visitor-ы. если это не так, значит паттерн не очень подходит.
да и что это за мифические "трудноуловимые баги" непонятно, забыл метод интерфейса определить, получил ошибку компиляции, чего тут трудноуловимого то?См. мой пример из первого поста.
Взяли мы и забыли для Cow (которое, напомню, потомок Creature) определить метод Accept. И что? Какая будет ошибка компиляции?
Никакой ошибки компиляции не будет, и вообще никаких ошибок не будет, просто Cow мы всегда будем обрабатывать как Creature, даже если написан отдельный обработчик именно для Cow. Это вполне может оказаться трудноуловимым багом (когда у тебя где-то совсем в другом месте из-за этого окажутся кривые данные, и ты будешь часами ломать голову по поводу того, как же они так испортились).
я честно прочитаю тут весь тред вечерком, но один вопрос возникает сразу - а чем не помогут тут для красоты методы-расширения? ими можно, как и в плюсах, "переопределять шаблоны для конкретных типов". и код станет красивым и локализованным.
а чем не помогут тут для красоты методы-расширения? ими можно, как и в плюсах, "переопределять шаблоны для конкретных типов".Методы-расширения - это ты про возможность навесить на класс новый метод не там, где определяется этот класс?
Не вижу, чем они тут помогут, даже даркгреевский вариант с частичными классами лучше.
даркгреевский вариант с частичными классами лучше.да, есть такое. жаль, что это (Visitor красивше, чем в примере или LINQ или твоём) возможно только при перекомпиляции исходников.
Это паттерн Strategy. Или Command, если отправляется одному объекту.как минимум - это Strategy + Visitor.
to mike и , если вы так формально классифицируете паттерны, то могу вот так пример переформулировать:
interface IThing
{
}
interface IThingWithILGenerate
{
void Generate(ILGenerator generator);
}
interface IThingWithCsGenerate
{
void Generate(System.CodeDom.Compiler.IndentedTextWriter writer);
}
class Visitor<TThingFace, TInfo>:IVIsitor
{
public Visitor(Executor<TThingFace,TInfo> executor)
{
this.Executor = executor;
}
Executor<TThingFace,TInfo> executor;
void Visit(IThing thing)
{
if (thing is TThingFace)
executorTThingFace)thing, info);
}
...
}
var generator = isILGenerateElseCsGenerate
? (IVisitor)new Visitor< IThingWithILGenerate>thing,generator) => thing.Generate(thing, generator) :
(IVisitor)new Visitor<IThingWithCsGenerate>thing, writer) => thing.Generate(thing, writer);
generator.VisitAll;
да, есть такое. жаль, что это (Visitor красивше, чем в примере или LINQ или твоём) возможно только при перекомпиляции исходников.в общем случае, в обоих вариантах требуется перекомпиляция.
мой вариант требует перекомпиляции всегда, майковский вариант требует перекомпиляции при изменении набора элементов
как минимум - это Strategy + Visitor.Я опять не вижу там Visitor - двойная Strategy: какой тип мы хотим обработать в списке и способ обработки этого типа. Операции в этом случае как писались в классах элементов, так и продолжают там писаться, а смысл паттерна в том, чтобы отделить операцию от элемента.
to mike и , если вы так формально классифицируете паттерны, то могу вот так пример переформулировать:
Возможно, в разной литературе по-разному толкуют содержимое паттерна, но в моём понимании он полностью избавляет от проверки принадлежности экземпляра объекта какому-то типу и обязательно содержит два виртуальных вызова, по крайней мере в C++/Java/C# (так же этот приём носит умное название).
Оставить комментарий
kruzer25
Есть вариант - сделать через жопу в лоб:Можно сделать даже более анально - завести кучу методов вроде IsCow IsLaptop итд.
Можно написать нормальный код, но он не будет работать:
Но если немного модифицировать - заработает:
Минусы - придётся писать в конкретных Visitor-ах методы на каждый конкретный тип Thing; в каждом классе, унаследованном от Thing, надо писать один и тот же код (а если забудем - то будут очень трудноуловимые баги):
Вот в плюсах, говорят, за счёт того, что шаблоны можно переопределять для конкретных типов - эта задача решается очень легко и красиво.
А можно ли в шарпе предпоследний пример привести в рабочее состояние, чтобы не надо было в каждом классе Thing писать один и тот же метод, и чтобы можно было в Visitor-ах писать методы для обработки только каких-то абстрактных типов?
То есть, как-нибудь вроде
, где (**super**) в рантайме приводит объект к максимально конкретному типу (и, соответственно, конкретный Visit выбирается тоже в рантайме)?