Недавно на Хабре проскакивала статья, в которой человек описывал способы реализаиции своей «хотелки». Суть хотелки довольно путана описана в первоисточнике, поэтому я не буду ее дублировать, отсылая к оригинальной заметке, а от себя замечу, что по сути задача сводится к реализации примесей (mixins) в C#.

Конечно, решение автора не поражает изяществом. Давайте рассмотрим его проблемы несколько подробнее.

  • Интерфейс-метка IImplementor не имеет никакого семантического смысла, единственная его задача — это создавать иллюзию строгой типизации. Если мы его снимем — мы увидим то, чем он на самом деле является — голым object.
  • Необходимость сохранять функциональную связь между классами ведет к тому, что используются статические переменные, которые скрывают переменные классов-родителей. Особенно хорошо это видно в случае, если скомпилировать код — компилятор выдаст соответствующее предупреждение. Следует использовать метку new перед скрываемым полем, чтобы явно показать всю низость наших намерений.
  • Неоправданно много дублирования кода. В каждом без исключения объекте необходимо заводить статическое поле и переопределять виртуальную функцию получателя. Если по ошибке забыть это сделать — получим undefined behaviour на уровне системы, и найти ошибку в этом случае будет затруднительно.
  • Объект бизнес-логики (по постановке) обязан знать о возможности своего расширения «левым кодом», что явно нарушает SRP.
  • Необходимость сознательно соблюдать последовательность вызовоа метода — передавать в него тот же объект, от которого был запрошен исполнитель. Это очень легко нарушить непренамерено.

Автор сходу отметает решение с рефлексией, как не соответствующее духу бизнес-приложений. Предлагаемое же им решение не соответствует духу ООП в принципе. Кроме того, в .NET отказывать себе в использовании рефлексии все равно, что использовать только 300 слов из всего словаря Даля при общении. В рамках дискуссии предлагаю обобщить задачу до реализации примесей (а это по сути именно они) и в очередной раз героически ее решить.

Допустим, на уровне разделяемой сборки у нас есть следующая иерархия классов.

#region Shared assembly

public class Document

{

public virtual string ID { get { return “Document”; } }

}

public class SpecialDocument : Document

{

public override string ID { get { return “SpecialDocument”; } }

}

#endregion

Мы хотим навесить на класс набор дополнительных методов, которые бы добавляли ему функционала извне, но при этом были бы максимально просты в работе.

class Program

{

static void Main(string[] args)

{

try

{

Document doc = new Document();

Document spDoc = new SpecialDocument();

doc.Store();

spDoc.Store();

}

catch (Exception ex)

{

Console.WriteLine(ex.Message);

}

Console.ReadLine();

}

}

Для решения, конечно же, придется задействовать рефлексию, благо этот мощный механизм позволяет нам следать достаточно многое.

Определим интерфейс, который должны будут реализовывать классы-расширения.

interface IDocumentPersistentStorage

{

void Store(Document doc);

}

В этом интерфейсе первым параметром всегда будет идти ссылка на оригинальный класс, для простоты вызовов. Можно сделать более изящное решение, но оно потребует больше кодирования и будет менее понятным.

Определим атрибут, который поможет нам связать обработчик функции-примеси с объектом из оригинальной иерархии классов.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]

class DocumentPersistentTargetTypeAttribute : Attribute

{

public Type BoundClass { get; private set; }

public DocumentPersistentTargetTypeAttribute(Type boundClass)

{

if(boundClass != typeof(Document) && !boundClass.IsSubclassOf(typeof(Document)))

throw new ApplicationException(“Bound class must be subclass of Document”);

BoundClass = boundClass;

}

}

Теперь определим объекты-обработчики для каждого члена оригинальной иерархии.

[DocumentPersistentTargetType(typeof(Document))]

class DocumentPersistentStorage : IDocumentPersistentStorage

{

public void Store(Document doc)

{

Console.WriteLine(“DocumentPersistentStorage store {0}”, doc.ID);

}

}

[DocumentPersistentTargetType(typeof(SpecialDocument))]

class SpecialDocumentPersistentStorage : IDocumentPersistentStorage

{

public void Store(Document doc)

{

Console.WriteLine(“SpecialDocumentPersistentStorage store {0}”, doc.ID);

}

}

И последним штрихом — привяжем эти обработчики к оригинальным классам с помощью довольно громоздкого (но при этом однократно написанного и позволяющего повторное использование кода) статического класса.

static class DocumentPersistentItemMixin

{

private static Dictionary<Type, Func<IDocumentPersistentStorage>> _factories;

private static Exception _initException;

static DocumentPersistentItemMixin()

{

try

{

var pFacts = Assembly.GetExecutingAssembly()

.GetTypes()

.Select(t => new { Type = t, Attribute = (DocumentPersistentTargetTypeAttribute)t.GetCustomAttributes(typeof(DocumentPersistentTargetTypeAttribute), false).SingleOrDefault() })

.Where(obj => obj.Attribute != null).ToArray();

foreach (var type in pFacts.Select(obj => obj.Attribute.BoundClass).Distinct())

if (pFacts.Count(obj => obj.Attribute.BoundClass == type) > 1)

throw new ApplicationException(string.Format(“{0} have more then one binding”, type.FullName));

_factories = pFacts.ToDictionary(obj => obj.Attribute.BoundClass, obj => new Func<IDocumentPersistentStorage>(() => (IDocumentPersistentStorage)Activator.CreateInstance(obj.Type)));

}

catch (Exception ex)

{

_initException = ex;

}

}

public static void Store(this Document doc)

{

GetPersistentItemFor(doc.GetType()).Store(doc);

}

private static IDocumentPersistentStorage GetPersistentItemFor(Type docType)

{

if (_initException != null)

throw new ApplicationException(string.Format(“DocumentPersistentItemMixin exception: {0}”, _initException.Message), _initException);

if (!_factories.ContainsKey(docType))

throw new ApplicationException(string.Format(“DocumentPersistentItemMixin exception: {0} have no binding”, docType.FullName));

return _factories[docType]();

}

}

В коде заложена страховка на случай ошибки программиста — классу может не соответствовать ни одного обработчика, либо соответствовать более чем один. В любом случае, диагностика подобных ошибок довольно проста благодаря понятным сообщениям.

Теперь мы можем работать с нашими классами так, как нам удобно, не задумываясь над тем, кто же на самом деле выполняет код, не захламляя систему дублированием кода, не заставляя выполнять тонну ненужных операций при расширении системуы и не нарушая SRP.

Tagged with →  
Share →

Leave a Reply

Войти с помощью: 

Your email address will not be published. Required fields are marked *

PageLines