Работаю в проекте, реализованном на C#. Предлагаю решение задачи, с которой столкнулся в проекте.
Условия. Есть классы-сущности, описывающие данные предметной области. При этом бывает и наследование — естественным образом организованное отношение «является». Эти классы описывают данные. Они лежат в отдельной сборке. Есть уровень клиента и уровень сервера. И ими используется эта сборка.
Возникновение задачи. Сборка, описывающая сущности, может описывать поведение только предметной области. Но, возникла задача, когда в слое сервера нужно работать с объектами, описанными в сборке сущностей, у которых уже есть иерархия наследования, полиморфно. При этом нужно сохранять ту же иерархию наследования. Требуемая работа с объектами на сервере не относится к предметной области.
Задача. Есть классы, распложенные в некоторой иерархии наследования. Как динамически менять поведение классов или работу с ними, повторяя эту иерархию?
Решение.
Отсекаю всё, что меня не устраивает:
1. Не хочу создавать громоздкий менеджер, хранящий маппинг классов на методы, которые их обрабатывают. В таком решении пришлось бы использовать отражение.
2. Не хочу создавать повторно иерархию классов для сервера. В этом случае пришлось бы или использовать отражение или просто продублировать классы и не использовать общую сборку для сущностей.
3. Не хочу цеплять к экземплярам классов никакие другие объекты. Работа классов должна быть реализована приблизительно так, как реализовано обычное наследование. Т.е. методы связаны не с экземпляром, а с классом.
И примерный код:
interface IImplementor {}
class A
{
public static IImplementor Implementor { private get; set; }
public virtual IImplementor GetImplementor()
{
return Implementor;
}
public override string ToString()
{
return "Instance of A";
}
}
class B: A
{
public static IImplementor Implementor { private get; set; }
public override IImplementor GetImplementor()
{
return Implementor;
}
public override string ToString()
{
return "Instance of B";
}
}
Интерфейс IImplementor создан для реализации любого поведения. Класс A и B – классы, к которым будет «цепляться» поведение. Теперь, например, в другой сборке нам нужна специфическая обработка объектов, поэтому добавляем поведение в IImplementor:
interface IConcreteImplementor : IImplementor
{
void Foo(A a);
}
class ConcreteImplementorA : IConcreteImplementor
{
public void Foo(A a)
{
Console.WriteLine("ConcreteImplementorA. " + a.ToString());
}
}
class ConcreteImplementorB : IConcreteImplementor
{
public void Foo(A a)
{
Console.WriteLine("ConcreteImplementorB. " + a.ToString());
}
}
Классы-обработчики созданы, а теперь «цепляем» к обрабатываемым классам:
A.Implementor = new ConcreteImplementorA();
B.Implementor = new ConcreteImplementorB();
A a1 = new A();
A a2 = (A)new B();
((IConcreteImplementor)a1.GetImplementor()).Foo(a1);
((IConcreteImplementor)a2.GetImplementor()).Foo(a2);
Получаем такой вывод:
ConcreteImplementorA. Instance of A
ConcreteImplementorB. Instance of B
То, что нужно.
По сути, это чем-то похоже на паттерн «Мост», но с виртуальными методами и отдельной от абстракции не только реализации, но и описанием поведения. Поведение можно задавать динамически. Суть работы в том, что обработчики присваиваются статическим классам. Но когда у нас есть ссылка экземпляра, приведенная к базовому классу, то используется механизм виртуальных методов экземпляра, который находит свой класс и его обработчик.
Минусами такого подхода можно считать то, что все-таки в классах, к которым «цепляется» поведение, нужно дополнительное кодирование. Которое никак не связано с предметной областью. Полностью разделить абстракцию и поведение мне не удалось. Также, минус в том, что на этапе компиляции не определяется, всё ли поведение «прицепили» и не используют ли в коде вызов неопределенной ссылки. И главный минус в том, что такое разделение классов и реализации разрушает строгую типизацию. Т.е. приведение к IConcreteImplementor может и не срабатывать. Мало ли какой интерфейс унаследуется от IImplementor.
Есть в примере кода одно неудобство. При вызове реализации два раза повторяется имя переменной. При этом переменная обязана быть одной и той же.
((IConcreteImplementor)a1.GetImplementor()).Foo(a1);
Было бы удобнее писать так:
((IConcreteImplementor)a1.GetImplementor()).Foo();
Но я в примере кода хотел передать только суть. А эта проблема решается с созданием копии IImplementor каждый раз при вызове GetImplementor() и сохранении в нем this.
Жду ваших комментариев о том, не создал ли я велосипед. Или может, есть решения такой задачи лучше. Как подсказывает опыт, если что-то делать неудобно, скорее всего, это не нужно, а есть более естественные пути.
PS. Для этой задачи я все же выбрал другое решение. Не нравится не строгость типизации. Но если работать с IImplementor и в нем описывать интерфейс поведения, то такой подход, думаю, имеет право на жизнь
Автор: m36