📜 ⬆️ ⬇️

Double dispatch

Not so long ago I ran into a service with a very curious task. But there is nothing new under the moon - and the task has long been familiar to you: double dispatching in C # in terms of static typing. I will explain in more detail below, but for those who understand everything, I will say: yes, I will use a “visitor”, but rather unusual.

A few more reservations, before stating the problem more strictly: I will not dwell on why dynamic does not suit me, explicit type checking through coercion and reflection. There are two reasons for this: 1) the goal is to get rid of runtime exceptions 2) I want to show that the language is sufficiently expressive, even if you do not resort to the listed tools and remain within the framework of strict typing.

Staging


In other words, suppose that we have in our hands a certain collection of heterogeneous objects with a rather general and low-level interface. Taking any two of them, we want to understand the type of each and process it using a special method.

Example:


I think it is obvious that to solve such a problem we would have enough of one class of the form:

internal class Cell { public Cell(string color) { Color = color; } public string Color { get; private set; } } 

Of course, in practice, you need to get access to specific properties of objects that cannot be brought into the common interface, but you do not have to clutter up the example with a complex domain area, so let's agree: in the general interface there is not a word about color, and all the cells look like this red below:

  internal interface ICell { //Some code } internal class RedCell : ICell { public string Color { get { return ""; } } //Some code } 

Classic visitor


Classics has already become a solution to this problem with one element. I will not dwell on it, you can learn more about it, for example, in Wikipedia . In our case, the solution would look like this. With such a model (we denote this code by [1], I’ll remember about it below):

  internal interface ICell { T AcceptVisitor<T>(ICellVisitor<T> visitor); } internal interface ICellVisitor<out T> { T Visit(RedCell cell); T Visit(BlueCell cell); T Visit(GreenCell cell); } internal class RedCell : ICell { public string Color { get { return ""; } } public T AcceptVisitor<T>(ICellVisitor<T> visitor) { return visitor.Visit(this); } } internal class BlueCell : ICell { public string Color { get { return ""; } } public T AcceptVisitor<T>(ICellVisitor<T> visitor) { return visitor.Visit(this); } } internal class GreenCell : ICell { public string Color { get { return ""; } } public T AcceptVisitor<T>(ICellVisitor<T> visitor) { return visitor.Visit(this); } } 

A simple visitor easily solves our problem:

 internal class Visitor : ICellVisitor<string> { public string Visit(RedCell cell) { //        // ICell,   RedCell      //    Color return cell.Color; } public string Visit(BlueCell cell) { return cell.Color; } public string Visit(GreenCell cell) { return cell.Color; } } 

Application of the visitor to the task


Well, let's try to generalize the existing solution to the case of two elements. The first thing that comes to mind is to turn each cell into a visitor: she knows her own type, and, having visited her own product, she also recognizes her type, therefore, solves our problem. It turns out something like this (for simplicity, I will write a solution only for the red cell, otherwise there is a lot of code):

  internal interface ICell { T AcceptVisitor<T>(ICellVisitor<T> visitor); } internal class RedCell<T> : ICell, ICellVisitor<T> { private readonly IProcessor<T> _processor; public RedCell(IProcessor<T> processor) { _processor = processor; } public TVisit AcceptVisitor<TVisit>(ICellVisitor<TVisit> visitor) { return visitor.Visit(this); } public string Color { get { return "red"; } } public T Visit(RedCell<T> cell) { return _processor.Get(this, cell); } public T Visit(BlueCell<T> cell) { return _processor.Get(this, cell); } public T Visit(GreenCell<T> cell) { return _processor.Get(this, cell); } } interface IProcessor<T> { T Get(RedCell<T> a, RedCell<T> b); T Get(RedCell<T> a, BlueCell<T> b); T Get(RedCell<T> a, GreenCell<T> b); } 

Now all that remains is for us to add a simple processor, and the problem is solved!

  internal class Processor : IProcessor<string> { public string Get(RedCell<string> a, RedCell<string> b) { return "  "; } public string Get(RedCell<string> a, BlueCell<string> b) { return GeneralCase(a, b); } public string Get(RedCell<string> a, GreenCell<string> b) { return GeneralCase(a, b); } private string GeneralCase(ICell a, ICell b) { return a.Color + " --> " + b.Color; } } 

Criticism of the found solution


Well, indeed, a solution has been found. Do you like it? I do not. The reasons are:

I hope this is enough to confirm my opinion - the decision is so-so, however, there is one more important note that I want to dwell on. Let us think how many methods should IProcessor contain? N x N. That is a lot. But after all, we most likely need special treatment for a very small number of cases linear in N. And nevertheless, we cannot know in advance which of them will be useful to us (and we are writing the framework, isn’t it? The structure of the classes, the basis that everyone will use later, connecting our assembly to their solutions).

How can it be improved? The obvious step is to separate the model from the visitor. Yes, let, as before, each cell is able AcceptVisitor (...) , but all methods of Visit will be in separate classes. It is easy to understand what we need, in this case, N + 1 class, each of which contains N methods Visit . Not weak, right? In this case, any new cell leads to the addition of a new class + according to the method to each of the existing ones.

Best Solution Found


I have a solution that does not have these shortcomings, namely: I need several classes (I speak without precision, because different syntactic prettiness like the fluent interface, from which I could not resist, are added to this number, but whether using them is a matter of taste), and the number of classes depends on N ; when adding a new cell, I will need to add a number of methods independent of N to these classes.

If (well, you never know) you are still reading, then think for a moment, can you propose a solution that meets these requirements?

If yes, then great, write to me, but mine here:

Our model is still of the form [1], but this way (to slightly intrigue the patient reader) will look like an analogous processor from the previous example:

 internal class ConcreteDispatcherFactory { public ICellVisitor<ICellVisitor<string>> CreateDispatcher() { return new PrototypeDispatcher<string>(Do) .TakeRed.WithRed(Do) .TakeGreen.WithBlue(Do); } private string Do(ICell a, ICell b) { var colorRetriever = new ColorRetriever(); var aColor = a.AcceptVisitor(colorRetriever); var bColor = b.AcceptVisitor(colorRetriever); return aColor + "\t-->\t" + bColor; } private string Do(GreenCell a, BlueCell b) { return ""; } private string Do(RedCell a, RedCell b) { return "  "; } } 

where ColorRetriever is a simple “single” visitor:

 internal class ColorRetriever : ICellVisitor<string> { public string Visit(RedCell cell) { return cell.Color; } public string Visit(BlueCell cell) { return cell.Color; } public string Visit(GreenCell cell) { return cell.Color; } } 

[Before, in fact, by the decision itself, a small digression about this last ColorRetriever - I want to focus the reader’s attention on the fact that the lines themselves
  var colorRetriever = new ColorRetriever(); var aColor = a.AcceptVisitor(colorRetriever); var bColor = b.AcceptVisitor(colorRetriever); 

still do not solve the problem. With their help, we only consistently gain access to each of the two cells in the form of an explicit type, not hidden by the interface, whereas we need to get such access for both at the same time. The amendment was made thanks to the care maxim_0_o , my bow to him

As you can see, we stipulate a general case and two particular ones, and this will be enough to build a solution.
By and large, we need two classes - the first and second visitor. The second will be generalized and the first will create its typed instance so that it can be used for a specific cell.

Here is the first:

 class PrototypeDispatcher<TResult> : ICellVisitor<ICellVisitor<TResult>> { private readonly Builder<TResult, RedCell> _redBuilder; private readonly Builder<TResult, GreenCell> _greenBuilder; private readonly Builder<TResult, BlueCell> _blueBuilder; public PrototypeDispatcher(Func<ICell, ICell, TResult> generalCase) { _redBuilder = new Builder<TResult, RedCell>(this, generalCase); _blueBuilder = new Builder<TResult, BlueCell>(this, generalCase); _greenBuilder = new Builder<TResult, GreenCell>(this, generalCase); } public IBuilder<TResult, RedCell> TakeRed { get { return _redBuilder; } } public IBuilder<TResult, BlueCell> TakeBlue { get { return _blueBuilder; } } public IBuilder<TResult, GreenCell> TakeGreen { get { return _greenBuilder; } } public ICellVisitor<TResult> Visit(RedCell cell) { return _redBuilder.Take(cell); } public ICellVisitor<TResult> Visit(BlueCell cell) { return _blueBuilder.Take(cell); } public ICellVisitor<TResult> Visit(GreenCell cell) { return _greenBuilder.Take(cell); } } 

Here is the second:

  internal class Builder<TResult, TA> : IBuilder<TResult, TA>, ICellVisitor<TResult> where TA : ICell { private Func<TA, RedCell, TResult> _takeRed; private Func<TA, BlueCell, TResult> _takeBlue; private Func<TA, GreenCell, TResult> _takeGreen; private readonly Func<ICell, ICell, TResult> _generalCase; private readonly PrototypeDispatcher<TResult> _dispatcher; private TA _target; public Builder(PrototypeDispatcher<TResult> dispatcher, Func<ICell, ICell, TResult> generalCase) { _dispatcher = dispatcher; _generalCase = generalCase; _takeRed = (a, b) => _generalCase(a, b); _takeBlue = (a, b) => _generalCase(a, b); _takeGreen = (a, b) => _generalCase(a, b); } public PrototypeDispatcher<TResult> WithRed(Func<TA, RedCell, TResult> toDo) { _takeRed = toDo; return _dispatcher; } public PrototypeDispatcher<TResult> WithBlue(Func<TA, BlueCell, TResult> toDo) { _takeBlue = toDo; return _dispatcher; } public PrototypeDispatcher<TResult> WithGreen(Func<TA, GreenCell, TResult> toDo) { _takeGreen = toDo; return _dispatcher; } public TResult Visit(RedCell cell) { return _takeRed(_target, cell); } public TResult Visit(BlueCell cell) { return _takeBlue(_target, cell); } public TResult Visit(GreenCell cell) { return _takeGreen(_target, cell); } public ICellVisitor<TResult> Take(TA a) { _target = a; return this; } } 

And also an interface for beauty to separate the builder from the visitor (which merge in both classes, but the call syntax is beautiful):

  internal interface IBuilder<TResult, out TA> { PrototypeDispatcher<TResult> WithRed(Func<TA, RedCell, TResult> toDo); PrototypeDispatcher<TResult> WithBlue(Func<TA, BlueCell, TResult> toDo); PrototypeDispatcher<TResult> WithGreen(Func<TA, GreenCell, TResult> toDo); } 

In conclusion, I want to refer to a series of articles about wizards and warriors , where dispatching issues in C # are also discussed.

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


All Articles