📜 ⬆️ ⬇️

Multiple dispatch in C #

We have already reviewed two articles where C # dynamic functionality could lead to unexpected code behavior.
This time I would like to show a positive side, where dynamic scheduling allows you to simplify the code while remaining strictly typed.

In this post we will learn:


And we need it?


Sometimes we may face the problem of choosing overloading methods. For example:
public static void Sanitize(Node node) { Node node = new Document(); new Sanitizer().Cleanup(node); // void Cleanup(Node node) } class Sanitizer { public void Cleanup(Node node) { } public void Cleanup(Element element) { } public void Cleanup(Attribute attribute) { } public void Cleanup(Document document) { } } 

[class hierarchy]
 class Node { } class Attribute : Node { } class Document : Node { } class Element : Node { } class Text : Node { } class HtmlElement : Element { } class HtmlDocument : Document { } 


As we can see, only the void Cleanup(Node node) method will be selected. This problem can be solved by OOP-approach, or use type conversion.

Let's start with the simple:
[cast]
 public static void Sanitize(Node node) { var sanitizer = new Sanitizer(); var document = node as Document; if (document != null) { sanitizer.Cleanup(document); } var element = node as Element; if (element != null) { sanitizer.Cleanup(element); } /* *     */ { //  - sanitizer.Cleanup(node); } } 


It does not look very "beautiful."
Therefore, apply the PLO:
 public static void Sanitize(Node node) { var sanitizer = new Sanitizer(); switch (node.NodeType) { case NodeType.Node: sanitizer.Cleanup(node); break; case NodeType.Element: sanitizer.Cleanup((Element)node); break; case NodeType.Document: sanitizer.Cleanup((Document)node); break; case NodeType.Text: sanitizer.Cleanup((Text)node); break; case NodeType.Attribute: sanitizer.Cleanup((Attribute)node); break; default: throw new ArgumentOutOfRangeException(); } } enum NodeType { Node, Element, Document, Text, Attribute } abstract class Node { public abstract NodeType NodeType { get; } } class Attribute : Node { public override NodeType NodeType { get { return NodeType.Attribute; } } } class Document : Node { public override NodeType NodeType { get { return NodeType.Document; } } } class Element : Node { public override NodeType NodeType { get { return NodeType.Element; } } } class Text : Node { public override NodeType NodeType { get { return NodeType.Text; } } } 


Well, we declared enumeration NodeType , entered the abstract property with the same name in class Node . Problem solved. Thank you for your attention .
')
This pattern helps in cases where it is necessary to have cross-platform portability; whether it is a programming language or execution environment. On this path went the standard W3C DOM , for example.

Multiple dispatch pattern


Multiple dispatching or multimethod (multiple dispatch) is a variation of the concept in OOP to select the method to be called at runtime rather than compilation.

To get an idea, let's start with a simple one: double dispatch (more on this here ).
Double dispatch
 class Program { interface ICollidable { void CollideWith(ICollidable other); } class Asteroid : ICollidable { public void CollideWith(Asteroid other) { Console.WriteLine("Asteroid collides with Asteroid"); } public void CollideWith(Spaceship spaceship) { Console.WriteLine("Asteroid collides with Spaceship"); } public void CollideWith(ICollidable other) { other.CollideWith(this); } } class Spaceship : ICollidable { public void CollideWith(ICollidable other) { other.CollideWith(this); } public void CollideWith(Asteroid asteroid) { Console.WriteLine("Spaceship collides with Asteroid"); } public void CollideWith(Spaceship spaceship) { Console.WriteLine("Spaceship collides with Spaceship"); } } static void Main(string[] args) { var asteroid = new Asteroid(); var spaceship = new Spaceship(); asteroid.CollideWith(spaceship); asteroid.CollideWith(asteroid); } } 


The essence of double dispatch is that the method is bound by the successor in the class hierarchy, and not at the place of a particular call. The disadvantages are also attributed to the problem of extensibility: with an increase in the elements in the system, you will have to deal with copy-paste.

- So where is the C # dynamic problem ?! - you ask.
In the example of type casting, we have already become acquainted with the primitive implementation of the multimethod pattern, where the choice of the required method overload occurs at the place of a particular call, in contrast to double dispatch.

But constantly writing a bunch of ifs not Feng Shui is bad!

Not always, of course. Just the examples above are synthetic. Therefore, we consider more realistic.

I'll take two


Before moving on, let's remember what an Enterprise Library is.

The Enterprise Library is a set of reusable components / blocks (logging, validation, data access, exception handling, etc.) for building applications. There is a separate book where all the details of the work are reviewed.

Each of the blocks can be configured both in XML and in the code itself.

Error handling unit today we will consider.

If you are developing an application that uses the ASP.NET pipeline pattern, then the Exception Handling Block (hereinafter simply “EHB”) can greatly simplify life. After all, the key place is always the error handling model in the language / framework, etc.

Suppose we have a section of code where we have replaced the imperative code with a more OOP type with the policy template (strategy variation).

It was:
 try { // code to throw exception } catch (InvalidCastException invalidCastException) { // log ex // rethrow if needed } catch (Exception e) { // throw new Exception with inner } 

It has become (using EHB):

 var policies = new List<ExceptionPolicyDefinition>(); var myTestExceptionPolicy = new List<ExceptionPolicyEntry> { { new ExceptionPolicyEntry(typeof (InvalidCastException), PostHandlingAction.NotifyRethrow, new IExceptionHandler[] {new LoggingExceptionHandler(...),}) }, { new ExceptionPolicyEntry(typeof (Exception), PostHandlingAction.NotifyRethrow, new IExceptionHandler[] {new ReplaceHandler(...)}) } }; policies.Add(new ExceptionPolicyDefinition("MyTestExceptionPolicy", myTestExceptionPolicy)); ExceptionManager manager = new ExceptionManager(policies); try { // code to throw exception } catch (Exception e) { manager.HandleException(e, "Exception Policy Name"); } 

Well, it looks more "enterprise". But is it possible to avoid massive dependencies and be limited to the capabilities of the C # language itself?

“The imperative approach is the very possibilities of the language ,” one may argue.
However, not only .

Let's try to write our Exception Handling Block, but only easier.

To do this, consider the implementation of the promotion of exception handlers in the EHB itself.
So, the source code again:

 ExceptionManager manager = new ExceptionManager(policies); try { // code to throw exception } catch (Exception e) { manager.HandleException(e, "Exception Policy Name"); } 

Call chain starting with
manager.HandleException(e, "Exception Policy Name")
ExceptionPolicyDefinition.FindExceptionPolicyEntry
 private ExceptionPolicyEntry FindExceptionPolicyEntry(Type exceptionType) { ExceptionPolicyEntry policyEntry = null; while (exceptionType != typeof(object)) { policyEntry = this.GetPolicyEntry(exceptionType); if (policyEntry != null) { return policyEntry; } exceptionType = exceptionType.BaseType; } return policyEntry; } 


ExceptionPolicyEntry.Handle
 public bool Handle(Exception exceptionToHandle) { if (exceptionToHandle == null) { throw new ArgumentNullException("exceptionToHandle"); } Guid handlingInstanceID = Guid.NewGuid(); Exception chainException = this.ExecuteHandlerChain(exceptionToHandle, handlingInstanceID); return this.RethrowRecommended(chainException, exceptionToHandle); } 


ExceptionPolicyEntry.ExecuteHandlerChain
 private Exception ExecuteHandlerChain(Exception ex, Guid handlingInstanceID) { string name = string.Empty; try { foreach (IExceptionHandler handler in this.handlers) { name = handler.GetType().Name; ex = handler.HandleException(ex, handlingInstanceID); } } catch (Exception exception) { // rest of implementation } return ex; } 



And this is just the tip of the iceberg.

The key interface is the IExceptionHandler:

 namespace Microsoft.Practices.EnterpriseLibrary.ExceptionHandling { public interface IExceptionHandler { Exception HandleException(Exception ex, Guid handlingInstanceID); } } 

Take it as a basis and nothing more.


Let's declare two interfaces (why we need it - we'll see later):

 public interface IExceptionHandler { void HandleException<T>(T exception) where T : Exception; } public interface IExceptionHandler<T> where T : Exception { void Handle(T exception); } 


As well as a handler for input / output (I / O) exceptions:
 public class FileSystemExceptionHandler : IExceptionHandler, IExceptionHandler<Exception>, IExceptionHandler<IOException>, IExceptionHandler<FileNotFoundException> { public void HandleException<T>(T exception) where T : Exception { var handler = this as IExceptionHandler<T>; if (handler != null) handler.Handle(exception); else this.Handle((dynamic) exception); } public void Handle(Exception exception) { OnFallback(exception); } protected virtual void OnFallback(Exception exception) { // rest of implementation Console.WriteLine("Fallback: {0}", exception.GetType().Name); } public void Handle(IOException exception) { // rest of implementation Console.WriteLine("IO spec"); } public void Handle(FileNotFoundException exception) { // rest of implementation Console.WriteLine("FileNotFoundException spec"); } } 


Apply:

 IExceptionHandler defaultHandler = new FileSystemExceptionHandler(); defaultHandler.HandleException(new IOException()); // Handle(IOException) overload defaultHandler.HandleException(new DirectoryNotFoundException()); // Handle(IOException) overload defaultHandler.HandleException(new FileNotFoundException()); // Handle(FileNotFoundException) overload defaultHandler.HandleException(new FormatException()); // Handle(Exception) => OnFallback 

Everything worked! But how? After all, we have not written a single line of code to resolve exception types, etc.

Consider the scheme


So, if we have an appropriate IExceptionHandler implementation, then we use it.
If not, multiple dispatch via dynamic .

Thus, example No. 1 can be solved with just one line of code:
 public static void Sanitize(Node node) { new Sanitizer().Cleanup((dynamic)node); } 

Summing up


At first glance, it is not very obvious that the whole pattern can fit only in one language construct, but it is.
On closer examination, we saw that building a simple policy-based exception handler is quite possible.

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


All Articles