📜 ⬆️ ⬇️

Some thoughts on the Visitor pattern

Recently, very often I often have to use the well-known Visitor pattern (he is the Visitor, hereinafter referred to as the visitor). Previously, I neglected them, considered it a crutch, an extra complication of the code. In this article, I will share my thoughts about what is in this pattern, in my opinion, good, what is bad, what tasks it helps to solve and how to simplify its use. The code will be in C #. If interested - please under the cat.



What it is ?


First, let's remember a little what this pattern is for and why it is used. Those who are familiar with it, can view diagonally. Suppose we have a library with a hierarchy of geometric figures.


')
Now we need to learn how to calculate their area. How? No problem. Add a method to iFigure and implement. Everything is great, except that now our library depends on the library of algorithms.

Then we needed to display the description of each figure in the console. And then draw the shapes. By adding the appropriate methods, we inflate our library, at the same time severely violating SRP and OCP.

What to do? Of course, in separate libraries to create classes that solve the tasks we need. How do they know which particular figure they were given? Cast!

public void Draw(IFigure figure) { if (figure is Rectangle) { /////// return; } if (figure is Triangle) { /////// return; } if (figure is Triangle) { /////// return; } } 

See an error? And I noticed it only in runtime. Downcasting is an acknowledged bad form, a way to break the LSP, etc., etc. ... There are languages ​​whose type system solves our problem out of the box (see multimethods), but C # does not apply to them.

This is where the Visitor aka Visitor comes to the rescue. The essence is this: there is a class - a visitor, which contains methods for working with each of the specific implementations of our abstraction. And each concrete implementation contains a method that does one single thing — it transfers itself to the corresponding method of the visitor.



A bit confusing, right? In general, one of the main drawbacks of the visitor is that not everyone enters it right away (judging by itself). Those. its use slightly increases the threshold of complexity of your system.

What happened? As you can see, all the logic is outside of our geometric figures, and in the visitors. No type conversion in runtime - the choice of method for each shape is determined at compilation. The problems that we encountered a little earlier, managed to get around. It would seem that everything is great? Of course not. There are drawbacks, but about them - at the very end.

Cooking options


What type of value should the Visit and AcceptVisitor methods return? In the classic version they are void. How to be in the case of calculating the area? You can enter a property in the visitor and assign a value to it, and after calling Visit, read it. But it is much more convenient for the AcceptVisitor method to immediately return the result. In our case, the type of result is double, but it is obvious that this is not always the case. Let's make a visitor and AcceptVisitor method generics.

 public interface IFigure { T AcceptVisitor<T>(IFiguresVisitor<T> visitor); } public interface IFiguresVisitor<out T> { T Visit(Rectangle rectangle); T Visit(Triangle triangle); T Visit(Circle circle); } 

This interface can be used in all cases. For asynchronous operations, the result type will be Task. If nothing needs to be returned, then the returned type can be a dummy type, known in functional languages ​​as Unit. In C #, it is also defined in some libraries, for example, in Reactive Extensions.

There are situations when, depending on the type of object, we need to perform some kind of trivial action, but only in one place of the program. For example, in practice it is unlikely that we will need to display the name of the figure somewhere, except in the test example. Or in some unit test it is necessary to determine that the figure is a circle or a rectangle. Well, for each such primitive case create a new entity - a specialized visitor? You can do differently:

 public class FiguresVisitor<T> : IFiguresVisitor<T> { private readonly Func<Circle, T> _ifCircle; private readonly Func<Rectangle, T> _ifRectangle; private readonly Func<Triangle, T> _ifTriangle; public FiguresVisitor(Func<Rectangle, T> ifRectangle, Func<Triangle, T> ifTrian-gle, Func<Circle, T> ifCircle) { _ifRectangle = ifRectangle; _ifTriangle = ifTriangle; _ifCircle = ifCircle; } public T Visit(Rectangle rectangle) => _ifRectangle(rectangle); public T Visit(Triangle triangle) => _ifTriangle(triangle); public T Visit(Circle circle) => _ifCircle(circle); } 

 public double CalcArea(IFigure figure) { var visitor = new FiguresVisitor<double>( r => r.Height * r.Width, t => { var p = (tA + tB + tC) / 2; return Math.Sqrt(p * (p - tA) * (p - tB) * (p - tC)); }, c => Math.PI * c.Radius * c.Radius); return figure.AcceptVisitor(visitor); } 

As you can see, it turned out something resembling pattern matching. Not the one that was added to C # 7 and which, in essence, is only powdered downcasting, but typed and controlled by the compiler.

But what if we have about a dozen figures, and we only need to do something special for one or two, and for some of the others, some kind of “default” action? Copy-paste into the designer dozens of identical expressions - lazily and ugly. How about this syntax?

 string description = figure .IfRectangle(r => $"Rectangle with area={r.Height * r.Width}") .Else(() => "Not rectangle"); 

 bool isCircle = figure .IfCircle(_=>true) .Else(() => false); 

In the last example we got a real analogue of the operator "is"! The implementation of this factory for our set of figures, like all other sources - on the githaba . This begs the question - well, for each case to write this boilerplate? Yes. Or you can, armed with T4 and Roslyn, write a code generator. Frankly, at the time of publication of the article, I planned to do it, but I just did not have time.

disadvantages


Of course, the visitor has enough disadvantages and limitations in the application. Take at least the AcceptVisitor method from IFifgure. What does he have to do with geometry? Yes, no. So again we have a violation of the SRP.

Next, take a look at the diagram again.



We see a closed system where everyone knows about everyone. Each type of hierarchy knows about the visitor — the visitor knows about all types — hence, each type transitively knows about all the others! Adding a new type (shapes in our example) actually affects everyone. And this is again a direct violation of the previously mentioned Open Close Principle. If we have the ability to change the code, then there is even a significant plus - if we add a new figure, the compiler will force us to add the appropriate method to the visitor's interface and its implementation - we will not forget anything. But what if we are only library users, not authors, and cannot change the hierarchy? No We cannot expand someone else’s structure with a visitor. Not for nothing in all definitions of the pattern they write that it is used in the presence of an established hierarchy. Thus, if we design an expandable library of geometric figures, we cannot use the visitor.

Total


The “Visitor” pattern is very convenient when we have the opportunity to make changes to its code. It allows you to get away from downcasting, its “non-expandability” allows the compiler to make sure that you add all handlers for all newly added types.

If we are writing a library that can be expanded by adding new types, then the visitor cannot be used. And then what? Yes, all the same downcasting, wrapped in pattrn-matching in C # 7. Or come up with something more interesting. If it works out, I will try to write about it.

And, of course, I will be glad to read opinions and ideas in the camments.
Thanks for attention!

Source: https://habr.com/ru/post/332042/


All Articles