Longer than what? Definitely the one function is shorter than the sum of
the length of all the element type hierarchy shims plus concrete visitor
visit overloads.
> bigger cyclomatic complexity,
with good code organization, no bigger than absolutely necessary.
> harder to understand, with good code organization, actually, easier (see my point below about
maintenance.
> harder to
> form confidence that it is valid,
see below in maintenance.
> harder to maintain,
actually, easier to maintain. For using that GoF / wiki DD
implementation, non-insignificant amount of scaffolding code has to be
written. Especially in a complex hierarchy, one can easily forget to
write that accept shim for a couple of classes where it is needed. E.g.
if in the hierarchy (or a part thereof) is "(B, C, D) > A" (> meaning
inheritance), imagine the op shall do same on A and D but different on B
and C. Isn't it easy to forget to add the accept shim to C (even though
the actual logic visit(C), at which the programmer is likely to focus,
is written all right).
> harder to test and
why?
> also may need more access to implementation details of all types
> involved, breaking encapsulation.
How so? All code is written in the op function that has no access to
internals.
>
>> In particular, as I explained
>> at the beginning, if the operation is decomposed correctly, its behavior
>> on the objects of different types has to be coherent, similar in some
>> way (and maybe more similar than different) and, as such, a significant
>> parts of the context or results of intermediate computations could be
>> shared.
>
> Translating data hierarchy to xml for particular version of particular
> protocol while supporting several such in parallel?
Imagine {ABCD) type hierarchy above is to be translated to XML and B and
C have to become XML elements with some non-trivially computable
attribute atr1 but XML elements produced from A and D don't need it.
> There can be
> intermediate computations shared but all are typically in in visitor
> classes or helpers of those. No one wants the classes in hierarchy
> to know anything about xml whatsoever,
correct but do not see how it is connected to anything I wrote
> nothing to talk about
> particular brand used in particular version of kinds of end-points.
let's not argue with anything I did not say, it's really irritating, I
will gladly give you an example, just explain why {ABCD} above is no good.
>
>> I demonstrated this in my example with red and green borders: a)
>> a condition by area prevents visitor from operating on objects is
>> computed from their property that is orthogonal to their dynamic type;
>> and b) the exact behavior of the operation (red vs green border in the
>> example) depends on a subset of types (which, in general, is not
>> necessarily compact in the hierarchy).
>>
> Your example did not need visitor pattern.
No example "needs" visitor pattern: any problem solved with visitor can
be solved in other ways, e.g. just by adding a virtual method to every
class. But *both* my examples could be perfectly solved with visitor. If
the one-time scaffolding is in place, I am ready to argue it is a good
solution.
>
>>> Think why virtual methods were added? To get rid of switch cases over type.
>> Of course, not. Virtual methods were *used*, with less or greater
>> success, to replace switch statements in event-processing scenarios
>> (like window behavior) (with greater success) or in state machines (with
>> lesser success).
>>
>> But, they were *invented* to implement polymorphic behavior and
>> relatively loose coupling (generally, lesser dependencies) between the
>> code that calls the method and the code that implements the method.
>>
> Same loose coupling is with code that calls function that does switch
> case over type, so that kind of victory wasn't achieved.
There is no such thing as "coupling in general". Any particular
dependency is some kind of coupling. If one has to add
accept(AbstractVisitor) method to the root of the type hierarchy, both
the type hierarchy's interface and implementation become compile-time
dependent on the new operation's interface (AbstractVisitor) (interface
may depend on forward-declaration only in C++, but not the
implementation so the implementation will also "see" N "visit" methods).
Additionally, because every AbstractVisitor depends on (the interface
of) every of (say, M) concrete element types, the Element's interface
implementation becomes dependent on every of M Element types (at least
their forward declarations, in C++). In this implementation, any new
operation adds the dependency, needs recompilation of class hierarchy etc.
If, as in my example, you only have one AbstractVisitor, adding 2nd and
further operations adds zero dependencies, don't require recompilation.
>
>> Obviously, a single switch statement couples everything together and the
>> compilation unit where the switch is depends on every piece of code and
>> every concrete type the switch statement handles -- to the extent its
>> behavior in the operation must differ from that of other types). But
>> this loose coupling is only desirable when the behaviors are severely
>> different.
>>
> I don't follow what you are saying. Different stock markets and other
> financial institutions for example are not severely different in essence,
> but the format of data you are expected to exchange with their servers
> differs massively enough.
Nah, it's the other way around. Say, BankA and BankB may have different
formats of their, say, client-side FIX protocol. But, they likely both
talk to NYSE using NYSE FIX/FAST and OATS reporting in exactly same or
almost same format.
Also, who needs to write an app that puts BankA and BankB to the same
hierarchy? It's unlinkely that it's one of BankA or BankB. More likely,
some regulatory agency or an exchange or settlement firm (hereinafter
*an outside firm*). For the purpose of the outside firm, the formats
it's interested in to operate on are likely the formats it uses to
communicate with those Banks (i.e. same or with small customizations). A
drastically different formats that BankA and B use to communicate with
their clients will unlikely be needed by an exchange (they could be
needed by a surveillance agency; but then "my" code would become
respectively simpler, *always staying simpler* than the correspondent
GoF code).
>
>> On the other hand, when the operation does *to significant
>> extent, a single thing* on different type, with 2-3 relatively small
>> type-dependent differences, these differences are often better expressed
>> in if statement, switch or in a number of different ways (including
>> calling combinations of virtual functions *from within the top-level
>> method implementing the operation*)
>>
> That is not use-case of visitor pattern.
You say it more than once and never cared to provide any reasoning. I
cited the cause to use Visitor (from Wikipedia you should like). That
cause is fully applicable for the above setup assuming one needs to add
many operations. I do not intend to repeat this again unless you provide
good reasoning.
>
>>> Visitor is not meant as excuse to add those back, otherwise just use
>>> the switch case in function, do not manufacture visitors that do not use
>>> double dispatch for confusion.
>>
>> Ohh. Not again. If you want to use virtual functions, use them, you
>> don't need visitor for this. Add virtual void newOperation(context
>> parameters) to the base class and call it in some for_each. This is
>> exactly what I explained in the first post: Visitor itself is an
>> alternative to adding a virtual methods.
>>
> You add same number of virtual methods of visitor if you need several
> different classes of visitors to visit over same data hierarchies. The
> gain is that these are in one place, in particular visitor and that the
> classes in hierarchies are not changed by single letter.
Yes, not changing classes when adding an op is the one of the causes for
using Visitor. Both my and Wiki visitor implementation formally achieve
this cause. Plain and simple. BUT, Wiki implementation changes the
compilation units of every Element (aka creates compilation dependency).
Plain and simple. My implementation doesn't (except for the very 1st
operation).
That's not I, that's you favorite ref source Wiki correctly says double
a dispatch is a special form of multiple dispatch and then, in all C++
examples it gives to show MD, it actually shows DD only. In fact, both
articles use same "Asteroid-Spaceship" example (DD) to exemplify either.
Now IMO, here is nothing magical in dispatching on dynamic types of 3
parameters as compared to the 2. It is surely more boring to express as
there is usually more functions to write; but otherwise, nothing too
exciting.
>
>> Says Wikipedia:
>>
>> "In software engineering, double dispatch is a special form of multiple
>> dispatch, and a mechanism that dispatches a function call to different
>> concrete functions depending on the runtime types of two objects
>> involved in the call"
>>
>> And, looking at same Wikipedia on multiple dispatch, C++ is *not* one of
>> the languages that natively support multiple dispatch and there are
>> multiple ways of implementing it, among them
>> - "using run time type comparison via dynamic_cast" (if statement method)
>> - "pointer-to-method lookup table"; and (misleadingly)
>> - "the visitor pattern"
>>
>> Why "misleadingly"? Because, looking at visitor pattern, you find
>>
>> "The visitor takes the instance reference as input, and implements the
>> goal through double dispatch."
>>
>> So, according to Wikipedia article on multiple dispatch one can
>> implement it (among the others) using visitor pattern, whereas visitor
>> pattern "implements the goal via double dispatch" whereas the double
>> dispatch is a special form of multiple dispatch.
>>
> Yes. you were confused by first sentence in wikipedia and stopped to
> read there. Double dispatch is multiple dispatch like 2D grid is special
> case of 4 dimensional room with 2 dimensions being of zero size.
Nothing like it. Implementing DD thru the manual scaffolding of what you
seem to believe *the one and the only correct implementation of DD for
Visitor pattern in C++" is boring and error-prone enough (see above
about maintenance) to not be the first choice in explaining Visitor.
You could, the complete class of Visitor problems is still solvable
without the accept shim. It is an implementation detail as well. It is
sometimes useful for the purposes other than that particular DD
implementation, e.g. to limit the exposure of the element types to the
"object structure" participant.
For an example of a framework implementing Visitor without shims, see
e.g. std::visit.
> Think a bit, wall of text how everybody around are confused
> does not make it true.
agree
>
>>>
>>>> The advantage is in having this logic in one place. If your visitor does
>>>> a coherent thing (which it is supposed to do; else why is it a single
>>>> visitor, there could be and usually would be common code for all or many
>>>> visited). The most pliant cases (that are exactly those gaining most
>>>> from applying a visitor pattern) may get by object virtual methods.
>>>> Simple example:
>>>>
>>>> If we wanted a green border around a shape of any of 10 classes derived
>>>> from convex polygon (ConvexTriangle, ConvexQuadrilateral etc), red
>>>> border around a shape of any of 10 classes derived from Oval and no
>>>> border around any of 30 other zero-area shape classes, all we need to do
>>>> is to change doOp to:
>>>>
>>>
>>> Unused cp in your code?
>> Yes, I started with taking bounding rectangle from every type separately
>> (which sometimes may be a valid micro-optimization if we use a fully
>> qualified non-virtual call; but then decided to keep it simple to
>> emphasize the points I wanted to emphasize)
>>> Can't post sane code from gg ... but also don't want to install newsreaders
>>> to all devices I use. It is drawing border to any shape and so it is unclear
>>> why you need visitor.
Because to decide whether to draw the border and (in the Example 2) its
color the element type is needed. I was about to introduce an
intermediate "ConvexArea" class but got lazy; regardless, Example 2
demonstrates the dependency on type.
Logical one place is either DrwawingContext's or
>>> Shape's nonvirtual method or free function:
>>>
>>> void Shape::draw_border(DrawingContext &dc) const {
>>> if (area() <= 25) return;
>>> const Rectangle br{boundingRectangle().increaseBy(0.1)};
>>> if (isConvexPolygon()) { dc.drawGreenRectangle(br); return; }
>>> assert(isOval());
>>> dc.drawRedRectangle(br);
>>> }
>> exactly, if you are ok to change the hierarchy every time you add an op,
>> you might not need the visitor, all you need is to call draw_border from
>> some for_each loop or high-order function.
>>
> Free function does no way change hierarchy. Adding non-virtual
> convenience method to base class of one of participants does
> it minimally.
See above about dependency and maintenance.
>
>> This might be a valid choice sometimes (my first post started with
>> this), but you pay by changing every class *every time you need a new
>> operation* and making all Shape classes dependent on DrawingContext (and
>> other contexts you might need for different operations (e.g. log file,
>> network socket, database connection etc)). In visitor pattern, on the
>> other hand, the concrete visitor is the only code that becomes dependent
>> on that op-specific context.
>>
> It is totally OK choice for your presented use-case, visitor is unneeded
> bloat there.
It *is* a Visitor as it has all the participants and solve the problem.
>
>>>
>>>>
>>>> And this is the complete code for adding a new operation (virtual cast
>>>> is to be only defined once for all operations)
>>>
>>> You forget that you have BorderingVisitor class of unclear life , inject
>>> DrawingContext to it (is it copy?), etc. result is
>>>
>>> BorderingVisitor bv(dc);
>>> s.acceptVisit(bv);
>>>
>>> instead one of:
>>>
>>> dc.draw_border(s);
dc is not supposed to know anything about a Shape, It draws a
rectangular body given a Rectangle only to specify the exact geometry of
the Shape. It is not a business of dc to compute. What if I want to,
instead of as a border of the size of the shape-cirumscribing rectangle
increased by 0.1 unit in all directions, add margins of 2 units at the
right and 1 unit at other 3 directions? That increaseBy call in my
example code is there for reason.
>>>
>>> s.draw_border(dc);
>>>
>>> draw_border(dc, s);
>>>
> See? Crickets are chirping,
no, they are strangely croaking, the border does not look like I wanted
it to, see above.
> no one is explaining why the bloat was even
> needed.
someone did but someone else did not like to read. Where's my increaseBy?
not again. I already cited the definition of what DD is. You refer to a
particular implementation as ff it were the definition.
>
>>> These
>> Unsure I understand what "these" refers to, will assume my doOp and
>> acceptVisit functions
>>> were entirely added to do double dispatch
>>
>> correct. acceptVisit dispatches by op (the 1st parameter of the outer
>> structure "visit" operation) and doOp dispatches by object type (in my
>> examples, either by using its base type's virtual function only or via a
>> combination of the same and `if' statement on virtual cast result)
>>
> Except on your case it does not. There is just single function that
> decides if to draw red or green border.
yes, depending on the type. Do you expect colors, styles, font foundries
and textures from an example on visitor? You are welcome to provide your
own code (the above does not work).
>
>>> to add
>>> virtual methods to large potentially unrelated data hierarchies without
>>> changing any of classes in those hierarchies.
>> I assume, by "potentially unrelated" data hierarchy, you mean the new op
>> has to behave entirely differently on every type of the hierarchy and
>> does not need common data. The issue with this assumption is that the
>> very first op you add might relate to the classes like this with but the
>> second might benefit from exploiting the type similarities or grouping
>> them differently (not even necessary in groups compact in the hierarchy
>> graph).
>>> What you do is not even
>>> use case for visitor pattern.
>> Says Wikipedia:
>>
>> "
>> What problems can the Visitor design pattern solve?
>>
>> It should be possible to define a new operation for (some) classes of an
>> object structure without changing the classes.
>> "
>
> "When new operations are needed frequently and the object structure
> consists of many unrelated classes, it's inflexible to add new subclasses
> each time a new operation is required because "[..] distributing all these
> operations across the various node classes leads to a system that's hard
> to understand, maintain, and change."
You know what you did? You made me dig out my (licensed) CD-ROM with GoF
book. Thank you! As I suspected, the Wikipedia quoted GoF in absolutely
wrong context. There is nothing about "adding new subclasses" in GoF
book, it is all about changing Node hierachy classes. The complete
paragraph says:
"
This diagram shows part of the Node class hierarchy. The problem here is
that distributing all these operations across the various node classes
leads to a system that's hard to understand, maintain, and change. It
will be confusing to have type-checking code mixed with pretty-printing
code or flow analysis code. Moreover, adding a new operation usually
requires recompiling all of these classes. It would be better if each
new operation could be added separately, and the node classes were
independent of the operations that apply to them.
"
(this paragraph is below the diagram depicting a part of Element
hierarchy, has nothing to do to Visitor classes or adding "new
subclasses". Unsure where this came from).
Whoever wrote this page should be .. well, politely reprimanded.
The quote above also emphasizes my point that adding compilation
dependency to the element type hierarchy shall not be taken lightly.
>
> Exactly. But if all you need is single little function then write single
> little function. Neither single dispatch (virtual functions) nor double
> dispatch (matching visitor classes to data hierarchy classes) are
> needed for single little function. How did your function being in
> visitor make anything easier to understand, maintain and change?
How many operations should I have exemplified? I showed 2 and stated the
Visitor is applicable when one is to add plenty -- what else do you
expect, not providing a single complete counterexample for the same problem.
>
>>
>> "What I do" achieves the above IMO, so it is definitely the use case.
>> You might argue whether or not my solution is a Visitor pattern (it is,
>> at least it includes all canonical Visitor participants (Abstract
>> Visitor, Concrete Visitors, Abstract Element, Concrete Elements (an
>> object structure providing access to the elements is implied)) and none
>> extra) but I don't understand how it can be argued that my code does not
>> define a new operation for (some) classes of an object structure. And it
>> surely does not change the classes.
>>
> That is especially bad and confusing when something looks like a
> pattern, uses names from that pattern but actually isn't the pattern
> at all in essence.
Why is it you and not I who gets to define the "essence"? I stated the
formal Visitor applicability conditions exactly to make this discussion
objective, to the point, and maybe agree to something useful. How can we
agree on anything useful if I start referring to some esoteric "essence"
as I understand it?