Отказ от ответственности: Ниже мое очень упрощенное описание проблемы. Пожалуйста, читая его, представьте себе какое-нибудь сложное модульное приложение (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 идеи, как ее решить
- Создайте некоторый контейнерный класс, который унаследует от 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);
- Используйте ConditionalWeakTable для хранения (на уровне уровня данных) CustomerName и BlockedCustomer, связанных с Customer, и изменения (один раз) пользовательского интерфейса, чтобы быть в курсе таких вещей
Насколько мне известно, оба решения, к сожалению, требуют от меня написания собственного средства отображения LINQ (включая отслеживание изменений), и я хочу этого избежать.
- Знаете ли вы какие-либо ORM, которые знают, как справиться с такими требованиями?
- Или, может быть, есть намного лучшее / более простое решение для написания приложений и не нарушающее принцип Open / Closed?
Редактировать - Некоторые пояснения после комментариев:
- Я говорю о свойствах, которые имеют индивидуальные отношения с Заказчиком. Обычно такие свойства добавляются в таблицу как дополнительные столбцы.
- Я хочу отправить только один SQL-запрос к базе данных. Поэтому добавление 20 таких новых функций / свойств / столбцов не должно заканчиваться 20 запросами.
- То, что я показал, было упрощенной версией приложения, которое я имею в виду. Я имею в виду приложение, зависимости которого будут структурированы следующим образом [UI] -> [Business Logic] <- [Data]. Все 3 должны быть открыты для расширения и закрыты для модификации, но в этом вопросе я сосредоточусь на слое [Data]. [Business Logic] запросит слой [Data] (используя Linq) для клиентов. Таким образом, даже после его расширения Customer BL будет просто запрашивать Customer (в Linq), но для расширения [Business Logic] потребуется CustomerName. Вопрос в том, как расширить слой [Data], чтобы он по-прежнему возвращал Customer к Customer BL, но предоставлял CustomerName для имени BL клиента и по-прежнему отправлял один запрос в базу данных. </li>
- Когда я показал Joins в качестве предложенного решения, может быть неясно, что они не будут жестко закодированы на уровне [Data], а скорее уровень [Data] должен знать, что он должен вызывать некоторые методы, которые могут захотеть расширить запрос и который будет зарегистрирован модулем main (). Модуль Main () является единственным, который знает обо всех зависимостях, и для целей этого вопроса не важно, как.