.net ORM, который позволит мне расширить мой код, не изменяя его (принцип Open Closed) - PullRequest
0 голосов
/ 30 октября 2018

Отказ от ответственности: Ниже мое очень упрощенное описание проблемы. Пожалуйста, читая его, представьте себе какое-нибудь сложное модульное приложение (ERP, Visual Studio, Adobe Photoshop), которое развивается в течение многих лет, в течение которых его функции будут добавляться и удаляться в надежде, что оно не окажется в коде спагетти.

Допустим, у меня есть следующая сущность и соответствующая таблица в базе данных

class Customer
{
    public int Id { get; set; }
}

тогда я буду использовать ORM, соберу DataContext и создаю свое приложение

static void Main(string[] args)
{
    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

    //GUI
    foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}

Заявка выполнена, мой клиент доволен, дело закрыто.

Через полгода мой клиент просит меня добавить имя к клиенту. Я хочу сделать это, не касаясь предыдущего кода.

Итак, сначала я создам новую сущность и соответствующую таблицу в базе данных и добавлю ее в ORM (пожалуйста, игнорируйте тот факт, что я изменяю тот же самый DataContext, это легко исправить)

class CustomerName
{
    public int Id { get; set; }
    public string Name { get; set; }
}

CustomerName добавит новое свойство в Customer, но для получения полной информации о клиенте нам необходимо объединить их, поэтому давайте попробуем изменить наше приложение, не затрагивая предыдущий код

static void Main(string[] args)
{
    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

     //new code that doesn't even compile
    customers = from c in customers
                join cn in context.CustomerNames on c.Id equals cn.Id
                select new {c, cn}; //<-- what should be here??

    //GUI
    foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}

Как вы можете видеть, я понятия не имею, с какой информацией сопоставлять мою информацию из объединения, чтобы она все еще была действительным объектом Customer.

И нет, я не могу использовать наследование.

Почему?

Потому что в то же время у другого разработчика может быть запрошена функциональность для блокировки клиента и создания следующего объекта:

class BlockedCustomer
{
    public int Id { get; set; }
    public bool Blocked { get; set; }
}

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

static void Main(string[] args)
{
    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

     //new code that doesn't even compile
    customers = from c in customers
                join cn in context.CustomerNames on c.Id equals cn.Id
                select new {c, cn}; //<-- what should be here??

    customers = from c in customers
        join b in context.BlockedCustomers on c.Id equals b.Id
        select new { c, b }; //<-- what should be here??

            //GUI
            foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}

У меня есть 2 идеи, как ее решить

  1. Создайте некоторый контейнерный класс, который унаследует от Customer, и при необходимости поиграйте с приведением / преобразованием его в CustomerName или BlockedCustomer.

Примерно так:

class CustomerWith<T> : Customer
{
    private T Value;

    public CustomerWith(Customer c, T value) : base(c)
    {
        Value = value;
    }
}

, а затем

customers = from c in customers
            join cn in context.CustomerNames on c.Id equals cn.Id
            select new CustomerWith<CustomerName>(c, cn);
  1. Используйте ConditionalWeakTable для хранения (на уровне уровня данных) CustomerName и BlockedCustomer, связанных с Customer, и изменения (один раз) пользовательского интерфейса, чтобы быть в курсе таких вещей

Насколько мне известно, оба решения, к сожалению, требуют от меня написания собственного средства отображения LINQ (включая отслеживание изменений), и я хочу этого избежать.

  1. Знаете ли вы какие-либо ORM, которые знают, как справиться с такими требованиями?
  2. Или, может быть, есть намного лучшее / более простое решение для написания приложений и не нарушающее принцип Open / Closed?

Редактировать - Некоторые пояснения после комментариев:

  1. Я говорю о свойствах, которые имеют индивидуальные отношения с Заказчиком. Обычно такие свойства добавляются в таблицу как дополнительные столбцы.
  2. Я хочу отправить только один SQL-запрос к базе данных. Поэтому добавление 20 таких новых функций / свойств / столбцов не должно заканчиваться 20 запросами.
  3. То, что я показал, было упрощенной версией приложения, которое я имею в виду. Я имею в виду приложение, зависимости которого будут структурированы следующим образом [UI] -> [Business Logic] <- [Data]. Все 3 должны быть открыты для расширения и закрыты для модификации, но в этом вопросе я сосредоточусь на слое [Data]. [Business Logic] запросит слой [Data] (используя Linq) для клиентов. Таким образом, даже после его расширения Customer BL будет просто запрашивать Customer (в Linq), но для расширения [Business Logic] потребуется CustomerName. Вопрос в том, как расширить слой [Data], чтобы он по-прежнему возвращал Customer к Customer BL, но предоставлял CustomerName для имени BL клиента и по-прежнему отправлял один запрос в базу данных. </li>
  4. Когда я показал Joins в качестве предложенного решения, может быть неясно, что они не будут жестко закодированы на уровне [Data], а скорее уровень [Data] должен знать, что он должен вызывать некоторые методы, которые могут захотеть расширить запрос и который будет зарегистрирован модулем main (). Модуль Main () является единственным, который знает обо всех зависимостях, и для целей этого вопроса не важно, как.

1 Ответ

0 голосов
/ 06 ноября 2018

У меня есть следующая сущность и соответствующая таблица в базе данных

Я думаю, что эта основная предпосылка является основной причиной большинства проблем.

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

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

Полностью нормализованная реляционная база данных характеризуется отношениями. Таблицы относятся к другим таблицам через внешние ключи. Эти отношения обеспечиваются механизмом базы данных. Обычно в полностью нормализованной базе данных (почти) все таблицы (транзитивно) связаны со всеми другими таблицами. Другими словами, все связано.

Когда что-то связано, в программировании мы часто называем это связь , и это то, чего мы хотели бы избежать.

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

ОРМ только усугубляют это. Как учил нас Тед Ньюард более десяти лет назад, ORM - это Вьетнамская война информатики . Я отказался от них много лет назад, потому что они только усугубляют все, включая программную архитектуру.

Более перспективным способом разработки сложной модульной системы является CQRS (см., Например, моя собственная попытка объяснить концепцию еще в 2011 ).

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

Тогда на стороне чтения вы можете предоставлять данные с помощью (постоянных) проекций транзакционных данных. Однако в архитектуре CQRS транзакционные данные часто лучше соответствуют источникам событий или базе данных документов.

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

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

...