В этой статье речь пойдет об унификации работы с атрибутами в проектах написанных на C#. Статья предназначена для разработчиков средних и больших проектов, или тех кому интересна тематика проектирования систем. Все примеры и реализации являются условными и предназначены для отражения подходов или идей.
Введение
При росте проекта, растет и количество различных типов атрибутов. В каждом конкретном случае разработчик выбирает удобный ему вариант обработки. При большом количестве разработчиков подходы могут сильно различаться, что усложняет понимание систем. Типичные примеры использования атрибутов это: различные виды сериализации, клонирование, привязка и тому подобные.
Для начала рассмотрим различные варианты использования атрибутов, выпишем их плюсы и минусы, а после этого попробуем избавиться от минусов, реализовав некую обобщенную систему.
Типичные примеры
Привязка
Предположим, у нас есть некое описание пользовательского интерфейса (UI, Form, форма) во внешнем источнике. Есть базовый класс формы Form, который позволяет загружать описание и создавать все необходимые управляющие элементы (widgets, controls, виджеты). Пользователи формы наследуются от класса Form и размечают необходимые поля с помощью атрибутов. При вызове метода Initialise, класса Form, происходит привязка созданных виджетов к полям класса наследника.
using System;
public class Widget
{
}
Описание класса атрибута:
using System;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class WidgetNameAttribute : Attribute
{
public WidgetNameAttribute(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
private string name;
}
Описание класса формы:
using System;
using System.Reflection;
public class Form
{
public void Initialise()
{
FieldInfo[] fields = GetType().GetFields(
BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
foreach (FieldInfo field in fields)
{
foreach (WidgetNameAttribute item in field.GetCustomAttributes(
typeof(WidgetNameAttribute), false))
{
Widget widget = FindWidget(item.Name);
field.SetValue(this, widget);
break;
}
}
}
public Widget FindWidget(string name)
{
return new Widget();
}
}
Описание класса пользовательской формы:
using System;
public class TestForm1 :
Form
{
[WidgetName("Test1")]
public Widget TestWidget1;
[WidgetName("Test2")]
public Widget TestWidget2;
}
Загрузка и инициализация формы:
using System;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
TestForm1 form1 = new TestForm1();
form1.Initialise();
}
}
}
Сериализация
Предположим, у нас есть некое описание данных в xml файле. Это могут быть как данные, так и, например, настройки. Есть базовый класс Data, который позволяет отыскивать значения и преобразовывать их в необходимый тип. Пользователь наследуется от класса Data и размечает поля атрибутами, где указывает XPath путь до конкретного значения поля в xml. При вызове метода InitialiseByXml класса Data происходит поиск и преобразование значения в необходимый тип. В текущем примере, для упрощения, преобразователь встроен в класс и поддерживает только несколько типов.
using System;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class DataValueAttribute : Attribute
{
public DataValueAttribute(string xPath)
{
this.xPath = xPath;
}
public string XPath
{
get { return xPath; }
}
private string xPath;
}
Описание класса данных:
using System;
using System.Xml;
using System.Reflection;
using System.Collections.Generic;
public class DataObject
{
static DataObject()
{
parsers[typeof(int)] = delegate(string value) { return int.Parse(value); };
parsers[typeof(string)] = delegate(string value) { return value; };
}
public void InitialiseByXml(XmlNode node)
{
FieldInfo[] fields = GetType().GetFields(
BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
foreach (FieldInfo field in fields)
{
foreach (DataValueAttribute item in field.GetCustomAttributes(
typeof(DataValueAttribute), false))
{
XmlNode tergetNode = node.SelectSingleNode(item.XPath);
object value = parsers[field.FieldType](tergetNode.InnerText);
field.SetValue(this, value);
break;
}
}
}
private delegate object ParseHandle(string value);
private static Dictionary<Type, ParseHandle> parsers =
new Dictionary<Type, ParseHandle>();
}
Описание пользовательского класса:
using System;
public class TestDataObject1 : DataObject
{
[DataValue("Root/IntValue")]
public int Value1;
[DataValue("Root/StringValue")]
public string Value2;
}
Пример использования:
using System;
using System.Xml;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(
"<Root>" +
"<IntValue>42</IntValue>" +
"<StringValue>Douglas Adams</StringValue>" +
"</Root>");
TestDataObject1 test = new TestDataObject1();
test.InitialiseByXml(doc);
}
}
}
Клонирование
Предположим, у нас есть класс CloneableObject, который позволяет клонировать себя и всех своих наследников. Клонирование может быть как обычным так и глубоким. Пользователь наследуется от CloneableObject и размечает поля, которые необходимо клонировать, а так же указывает, использовать ли глубокое клонирования поля.
using System;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class CloneAttribute : Attribute
{
public bool Deep
{
get { return deep; }
set { deep = value; }
}
private bool deep;
}
Описание класса CloneableObject:
using System;
using System.Reflection;
public class CloneableObject : ICloneable
{
public object Clone()
{
object clone = Activator.CreateInstance(GetType());
FieldInfo[] fields = GetType().GetFields(
BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
foreach (FieldInfo field in fields)
{
foreach (CloneAttribute item in field.GetCustomAttributes(
typeof(CloneAttribute), false))
{
object value = field.GetValue(this);
if (item.Deep)
{
if (field.FieldType.IsArray)
{
Array oldArray = (Array)value;
Array newArray = (Array)oldArray.Clone();
for (int index = 0; index < oldArray.Length; index++)
newArray.SetValue(((ICloneable)oldArray.GetValue(index)).
Clone(), index);
value = newArray;
}
else
{
value = ((ICloneable)value).Clone();
}
}
field.SetValue(clone, value);
break;
}
}
return clone;
}
}
Описание пользовательского класса:
using System;
public class TestCloneableObject1 : CloneableObject
{
[Clone]
public CloneableObject Value1;
[Clone]
public CloneableObject[] ValueArray2;
[Clone(Deep = true)]
public CloneableObject Value3;
[Clone(Deep = true)]
public CloneableObject[] ValueArray4;
}
Пример использования:
using System;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
TestCloneableObject1 test = new TestCloneableObject1();
test.Value1 = new CloneableObject();
test.ValueArray2 = new CloneableObject[] { new CloneableObject() };
test.Value3 = new CloneableObject();
test.ValueArray4 = new CloneableObject[] { new CloneableObject() };
TestCloneableObject1 test2 = (TestCloneableObject1)test.Clone();
}
}
}
Все примеры имеют схожие черты и показывают классическое применение атрибутов.
Плюсы:
- Простота
Минусы:
- При каждой инициализации мы запрашиваем все поля
- При каждой инициализации мы создаем экземпляры атрибутов
- При каждой инициализации мы делаем ветвление по типу поля, хотя его тип никогда не меняется
Унификация
Основная идея унификации, это генерация инициализаторов для конкретного типа объекта и конкретного набора атрибутов. Для инициализации объекта мы запрашиваем необходимый инициализатор и передаем ему экземпляр объекта и данные, все остальное делает инициализатор. Генерировать инициализатор можно кодогенерацией, а можно переложить кодогенерацию на компилятор, используя анонимные делегаты.
Тестовый класс:
using System;
public class TestObject1
{
[Clone, DataValue("Root/IntValue")]
public int Value1;
[DataValue("Root/StringValue")]
public string Value2;
[Clone, WidgetName("Test1")]
public Widget Widget3;
}
Для тестового класса будет создан контейнер, внутри которого находятся 3 инициализатора:
- Инициализатор клонирования с двумя делегатами для полей Value1 и Widget3
- Инициализатор сериализации с двумя делегатами для полей Value1 и Value2
- Инициализатор привязки с одним делегатом на поле Widget3
Тестовый класс и созданный контейнер с инициализаторами:
Реализация
Текущая реализация является демонстрационной, не придерживается никаких принципов, не обрабатывает ошибок и не потокобезопасна. Основная задача реализации продемонстрировать работу системы.
Базовый класс атрибутов
Генерация делегатов для поля происходит внутри атрибутов. Все пользовательские атрибуты наследуются от атрибута FieldSetterAttribute и переопределяют метод Generate, где и происходит генерация делегата и добавления его в список инициализатора.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public abstract class FieldSetterAttribute : Attribute
{
public abstract void Generate(Initialiser initialiser, FieldInfo info);
}
Инициализатор
Задача инициализатора хранить в себе список делегатов конкретного набора инициализации и выполнить его по требованию.
using System;
using System.Collections.Generic;
public delegate void InitialiseHandle(object target, object data);
public class Initialiser
{
public void Add(InitialiseHandle handle)
{
handlers.Add(handle);
}
public void Initialise(object target, object data)
{
foreach (InitialiseHandle handler in handlers)
handler(target, data);
}
private List<InitialiseHandle> handlers = new List<InitialiseHandle>();
}
Контейнер
Контейнер содержит все инициализаторы для одного пользовательского типа класса. Доступ к инициализатору происходит по типу атрибута.
using System;
using System.Collections.Generic;
public class Container
{
public Initialiser GetInitialiser(Type type)
{
Initialiser result;
if (!initialisers.TryGetValue(type, out result))
{
result = new Initialiser();
initialisers.Add(type, result);
}
return result;
}
private Dictionary<Type, Initialiser> initialisers = new Dictionary<Type, Initialiser>();
}
Менеджер типов
Менеджер типов содержит в себе созданные контейнеры для обрабатываемых пользовательских типов. Генерация контейнеров происходит при запросе и производится только один раз.
using System;
using System.Collections.Generic;
using System.Reflection;
public static class TypeManager
{
public static Container GetContainer(Type type)
{
Container result;
if (!containers.TryGetValue(type, out result))
{
result = new Container();
containers.Add(type, result);
InitialiseContainer(type, result);
}
return result;
}
private static void InitialiseContainer(Type type, Container container)
{
FieldInfo[] fields = type.GetFields(
BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);
foreach (FieldInfo field in fields)
{
foreach (FieldSetterAttribute item in field.GetCustomAttributes(
typeof(FieldSetterAttribute), false))
{
Initialiser initialiser = container.GetInitialiser(item.GetType());
item.Generate(initialiser, field);
}
}
}
private static Dictionary<Type, Container> containers = new Dictionary<Type, Container>();
}
Принцип работы
Пользователь системы создает свой атрибут, наследуя его от FieldSetterAttribute и переопределяет метод Generate. В метод Generate передается инициализатор Initialiser привязанный к типу пользовательского атрибута и информация о поле FieldInfo. Внутри метода, пользователь должен создать анонимный делегат, который производит необходимые операции с полем и добавить его в инициализатор.
При запросе контейнера для пользовательского типа, происходит его поиск и если он не обнаружен, то запускается генерация контейнера и всех инициализаторов для пользовательского типа. Для этого обходятся все поля и если у них есть атрибут производный от FieldSetterAttribute то происходит вызов метода Generate. После этого информация об инициализаторах сохраняется в контейнере, а контейнер сохраняется в менеджере.
Для использования инициализатора необходимо запросить его по типу атрибута у контейнера и вызвать метод Initialise, передав туда экземпляр класса и необходимые данные. После этого будут вызваны все делегаты созданные для данного инициализатора.
Пример реализации пользовательских атрибутов
После понимания принципов работы системы мы можем реализовать пользовательские атрибуты для нашего тестового типа TestObject1. Новый тестовый тип назовем TestObject2.
using System;
public class TestObject2 : Form, ICloneable
{
[Clone, DataValue("Root/IntValue")]
public int Value1;
[DataValue("Root/StringValue")]
public string Value2;
[Clone(Deep = true), WidgetName("Test1")]
public Widget Widget3;
public object Clone()
{
object clone = Activator.CreateInstance(GetType());
TypeManager.GetContainer(GetType()).
GetInitialiser(typeof(CloneAttribute)).Initialise(clone, this);
return clone;
}
}
Реализация виджета с поддержкой клонирования:
using System;
public class Widget : ICloneable
{
public object Clone()
{
return new Widget();
}
}
Реализация формы:
using System;
public class Form
{
public void Initialise()
{
TypeManager.GetContainer(GetType()).
GetInitialiser(typeof(WidgetNameAttribute)).Initialise(this, null);
}
public Widget FindWidget(string name)
{
return new Widget();
}
}
Реализация атрибута для привязки:
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class WidgetNameAttribute : FieldSetterAttribute
{
public WidgetNameAttribute(string name)
{
this.name = name;
}
public override void Generate(Initialiser initialiser, FieldInfo info)
{
initialiser.Add(
delegate(object target, object data)
{
Form targetForm = (Form)target;
Widget widget = targetForm.FindWidget(name);
info.SetValue(targetForm, widget);
}
);
}
private string name;
}
Реализация атрибута для сериализации:
using System;
using System.Reflection;
using System.Xml;
using System.Collections.Generic;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class DataValueAttribute : FieldSetterAttribute
{
static DataValueAttribute()
{
parsers[typeof(int)] = delegate(string value) { return int.Parse(value); };
parsers[typeof(string)] = delegate(string value) { return value; };
}
public DataValueAttribute(string xPath)
{
this.xPath = xPath;
}
public override void Generate(Initialiser initialiser, FieldInfo info)
{
ParseHandle parser = parsers[info.FieldType];
initialiser.Add(
delegate(object target, object data)
{
XmlNode node = ((XmlNode)data).SelectSingleNode(xPath);
object value = parser(node.InnerText);
info.SetValue(target, value);
}
);
}
private string xPath;
private delegate object ParseHandle(string value);
private static Dictionary<Type, ParseHandle> parsers =
new Dictionary<Type, ParseHandle>();
}
Реализация атрибута для клонирования:
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class CloneAttribute : FieldSetterAttribute
{
public override void Generate(Initialiser initialiser, FieldInfo info)
{
if (deep)
{
if (info.FieldType.IsArray)
{
initialiser.Add(
delegate(object target, object data)
{
object value = info.GetValue(data);
Array oldArray = (Array)value;
Array newArray = (Array)oldArray.Clone();
for (int index = 0; index < oldArray.Length; index++)
newArray.SetValue(((ICloneable)oldArray.GetValue(index)).
Clone(), index);
value = newArray;
info.SetValue(target, value);
}
);
}
else
{
initialiser.Add(
delegate(object target, object data)
{
object value = info.GetValue(data);
value = ((ICloneable)value).Clone();
info.SetValue(target, value);
}
);
}
}
else
{
initialiser.Add(
delegate(object target, object data)
{
object value = info.GetValue(data);
info.SetValue(target, value);
}
);
}
}
public bool Deep
{
get { return deep; }
set { deep = value; }
}
private bool deep;
}
Итог
Как видно из примеров, вызывать инициализаторы можно как внутри классов так и снаружи. Сложные присвоения можно разбивать на простые, так как нам известны все параметры поля перед тем как мы создадим делегат. Запрос полей и атрибутов происходит один раз. Вся работа с атрибутами происходит однообразно.
Систему можно расширить для поддержки свойств, методов, событий и классов.
Автор: mynameco