Multiple dispatch в C#

Ми вже розглянули дві статті, де функціонал C# dynamic міг привести до несподіваного поведінки коду.
На цей раз я б хотів показати позитивну сторону, де динамічна диспетчеризація дозволяє спростити код, залишаючись при цьому строго-типізованих.

У цьому пості ми дізнаємося:
  • можливі варіанти реалізації шаблону множинна диспетчеризація (multiple/double dispatch & co.)
  • позбутися реалізувати Exception Handling Block з Enterprise Library за пару хвилин. І, звичайно ж, спростити policy-based модель обробки помилок
  • dynamic – ефективніше Вашого коду

А воно нам треба?
Іноді ми можемо зіткнутися з проблемою вибору перевантаження методів. Наприклад:
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) { }
}

[ієрархія класів]
class Node { }

class Attribute : Node
{ }

class Document : Node
{ }

class Element : Node
{ }

class Text : Node
{ }

class HtmlElement : Element
{ }

class HtmlDocument : Document
{ }


Як ми бачимо, буде обраний метод тільки
void Cleanup(Node node)
. Дану проблему можна вирішити ООП-підходом, або використовувати приведення типів.

Почнемо з простого:
[приведення типів]
public static void Sanitize(Node node)
{
var sanitizer = new Sanitizer();
var document = node Document as;
if (document != null)
{
sanitizer.Cleanup(document);
}
var element = node as Element;
if (element != null)
{
sanitizer.Cleanup(element);
}
/*
* інші типи перевірки на
*/
{
// дію за замовчуванням
sanitizer.Cleanup(node);
}
}


Виглядає не дуже «красиво».
Тому застосуємо ООП:
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; }
}
}


Ну що ж, ми оголосили перерахування NodeType, ввели однойменне абстрактне властивість в клас Node. Задача вирішена. Дякую за увагу.

Такий шаблон допомагає в тих випадках, коли необхідно мати межплатформенную переносимість; чи то мова програмування, або середовище виконання. Таким шляхом пішов стандарт W3C DOM.

Multiple dispatch pattern
Множинна диспетчеризація або мультиметод (multiple dispatch) є варіацією концепції ООП для вибору викликається методу під час виконання, а не компіляції.

Щоб перейнятися ідеєю, почнемо з простого: double dispatch (більше про це тут).
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);
}

private void CollideWith(Asteroid asteroid)
{
Console.WriteLine("Spaceship collides with Asteroid");
}

private 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);
}
}


Суть double dispatch полягає в тому, що прив'язка методу проводиться спадкоємцем в ієрархії класів, а не в місці конкретного виклику. До мінусів варто віднести також і проблему розширюваності: при збільшенні елементів у системі, доведеться займатися copy-paste.

Так і де проблема C# dynamic?! – запитаєте Ви.
У прикладі з приведенням типів ми вже познайомилися з примітивної реалізацією шаблону мультиметод, де вибір необхідної перегрзуки методу відбувається в місці конкретного виклику на відміну від double dispatch.

Але постійно писати купу if'ів не по фен-шую — погано!

Не завжди, звичайно. Просто приклади вище — синтетичні. Тому розглянемо більш реалістичні.

i'll take two
Перш ніж рухатися далі, давайте згадаємо, що таке Enterprise Library.

Enterprise Library — це набір переиспользуемых компонентів/блоків (логування, валідація, доступ до даних, обробка виключень і т. п.) для побудови додатків. Існує окрема книга, де розглянуто всі подробиці роботи.

Кожен з блоків можна конфігурувати як в XML, так і в самому коді.

Блок по обробці помилок сьогодні ми і розглянемо.

Якщо Ви розробляєте додаток, в якому використовується pipeline патерн а-ля ASP.NET, тоді Exception Handling Block (далі просто «EHB») може сильно спростити життя. Адже наріжним місцем завжди є модель обробки помилок у мові/фрейворке і т. п.

Нехай у нас є ділянка коду, де ми замінили імперативний код на більш ООП-шний з шаблоном policy (варіації шаблону стратегія).

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

Стало (з використанням 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");
}

Що ж, виглядає більш «энтерпрайзно». Але чи можна уникнути масивних залежностей і обмежиться можливостями самої мови C#?

Імперативний підхід і є самі можливості мови, — можна заперечити.
Проте не тільки.

Спробуємо написати свій Exception Handling Block, але тільки простіше.

Для цього розглянемо реалізацію розкрутки обробників винятків у самому EHB.
Отже, вихідний код ще раз:

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

Ланцюжок викликів, починаючи з
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;
}



І це тільки вершина айсберга.

Ключовим інтерфейсом є IExceptionHandler:

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

Візьмемо його за основу і нічого більше.


Оголосимо два інтерфейсу (навіщо це потрібно — побачимо трохи пізніше):

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

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


А також обробник виключень для вводу-виводу (I/O):
public class FileSystemExceptionHandler : IExceptionHandler,
IExceptionHandler<Exception>,
IExceptionHandler<IOException>,
IExceptionHandler<FileNotFoundException>
{
public void HandleException<T>(T exception) where T : Exception
{
var handler = as this 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");
}
}


Застосуємо:

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

Все спрацювало! Але як? Адже ми не написали жодного рядка коду для вирішення типів виключень і т. п.

Розглянемо схему

Так, якщо у нас є відповідна реалізація IExceptionHandler, тоді використовуємо її.
Якщо ні — multiple dispatch через dynamic.

Так, приклад №1 можна вирішити лише одним рядком коду:
public static void Sanitize(Node node)
{
new Sanitizer().Cleanup((dynamic)node);
}

Підбиваючи підсумки
На перший погляд, досить неочевидно, що цілий патерн може поміститься лише в одній мовній конструкції, але це так.
При детальному розгляді ми побачили, що побудова простого policy-based обробника виключень цілком можливо.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.