«Мост» с наследованием

в 17:10, , рубрики: паттерны, проектирование, Проектирование и рефакторинг, метки: ,

Работаю в проекте, реализованном на 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js