免责声明:以下是我对问题的非常简单的描述。请在阅读它时,想象一下一些复杂的模块化应用程序(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将为客户添加新属性,但是要获得完整的客户信息,我们需要将它们结合在一起,所以让我们尝试修改我们的应用程序而无需触及先前的代码
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个解决方法
类似这样的东西:
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);
据我所知,不幸的是,这两种解决方案都要求我编写自己的LINQ映射器(包括更改跟踪),而我想避免使用它。
编辑-评论后的一些说明:
答案 0 :(得分:0)
我在数据库中有以下实体和相应的表
我认为这个基本前提是大多数问题的根本原因。
要清楚,将应用程序体系结构建立在基础关系数据库架构上并没有本质上的坏处。有时,所有利益相关者都需要一个简单的CRUD应用程序。
但是,重要的是要意识到,无论何时做出选择,应用程序体系结构都会固有地是关系的(即,不是面向对象的)。
完全规范化的关系数据库的特征是关系。这些表通过外键与其他表相关。这些关系由数据库引擎强制执行。通常,在完全规范化的数据库中,(几乎)所有表都(以传递方式)连接到所有其他表。换句话说,一切都已连接。
当事物连接在一起时,在编程中我们通常将其称为 coupling ,这是我们要避免的事情。
您想将应用程序体系结构划分为模块,但仍将其建立在所有事物都耦合在一起的数据库上。我不知道这样做的实际方法。我认为这是不可能的。
ORM只会使情况变得更糟。正如泰德·纽德(Ted Neward)十多年前教给我们的那样,ORMs is the Vietnam war of computer science。几年前,我已经放弃了它们,因为它们会使包括软件体系结构在内的所有方面变得更糟。
设计复杂的模块化系统的一种更有希望的方法是CQRS(例如,参见my own attempt at explaining the concept back in 2011)。
这将使您能够将域名事件(例如命名客户或阻止客户)视为不仅封装 how ,还有为什么会发生什么事情。
那么,在读取侧,您可以通过(持久)事务数据投影提供数据。但是,在CQRS体系结构中,事务数据通常更适合事件源或文档数据库。
这种架构非常适合微服务,但是您也可以通过这种方式编写更大的模块化应用程序。
像OOD这样的关系数据库在一段时间内听起来像是个好主意,但最终我的经验是这两个主意都是死胡同。它们无助于我们降低复杂性。它们不会使事情更易于维护。