【问题标题】:What is the proper object relationship? (C#)什么是正确的对象关系? (C#)
【发布时间】:2010-12-20 03:09:07
【问题描述】:

我有一个关于我应该为这种情况设置的正确对象关系的快速问题:

我有一个带有关联参数的客户对象和一个带有关联参数的仓库对象。每个仓库服务于一组客户,客户需要访问他们各自仓库的特定信息。

我想知道我应该建立什么样的正确关系,以便一组客户对象都引用特定仓库对象的同一个实例。我想确保它没有为每个客户创建重复的 Depot 对象。此外,我希望能够在不通过客户本身的情况下更改 Depot 的属性。

我知道这可能是一个相当基本的问题,但 C# 有这么多不同的“特性”,有时会让人感到困惑。

感谢您的帮助!

  • 查理

【问题讨论】:

  • 只是一些想法: 1. TreeView 范式:每个客户都是仓库节点的客户节点的子节点。 2. 具有相互引用的复合对象:每个 Depot 对象都是一个维护 List 的对象:每个 Customer 都有一个字段维护对其 Depot 的引用。
  • 我建议您阅读 Martin Fowler 的企业架构模式。这是一本很棒的书,涵盖了您所问的很多内容。

标签: c# oop data-modeling


【解决方案1】:

如果我正确理解您的问题,我认为您的问题的解决方案可能是 OR 映射器。 Microsoft 目前提供了两个 OR 映射器,LINQ to SQL 和实体框架。如果您使用的是 .NET 3.5,我建议使用 LINQ to SQL,但如果您能够尝试使用 .NET 4.0,我强烈建议您研究 Entity Framework。 (我不鼓励在 .NET 3.5 中使用 Entity Framework,因为它发布得太早并且存在很多问题。)

这两个 OR 映射器都提供可视化建模工具,允许您构建概念实体模型。使用 LINQ to SQL,您可以从数据库生成模型,该模型将为您提供实体类以及这些类之间的关联(表示来自 DB 模式的外键)。 LINQ to SQL 框架将为您处理生成 SQL 查询,并将自动将数据库查询结果映射到对象图中。诸如您所描述的关系,一组中的多个客户引用同一个部门的关系会自动为您处理,您根本不需要担心它们。您还可以使用 LINQ 查询数据库,并且可以避免编写大量存储过程和管道/映射代码。

如果您使用 .NET 4.0,实体框架实际上就是 LINQ to SQL 的强项。它支持 LINQ to SQL 所做的一切,以及更多。它支持模型驱动设计,允许您构建一个概念模型,从中生成代码和数据库模式。它支持更广泛的映射,提供更灵活的平台。它还提供实体 SQL (eSQL),它是一种基于文本的查询语言,除了 LINQ to Entities 之外,还可用于查询模型。 Line LINQ to SQL,它将解决您用作示例的场景以及许多其他场景。

OR 映射器可以节省大量时间、金钱和精力,大大减少与关系数据库交互所需的工作量。它们既提供动态查询,也提供动态、乐观的更新/插入/删除,并解决冲突。

【讨论】:

  • Linq to SQL 只有在您知道正在使用 SQL 时才建议使用。此外,它似乎没有回答基本问题“我如何在内存中跟踪它,这样我就不必通过客户对象来到达仓库 [反之亦然]?”
  • @Jason D:L2S 只是其中一种选择。这篇文章的一般要点是使用 OR 映射器,它绝对解决了所提出问题的问题,即跟踪对象实例,这样每个客户对象都不会使用共享实例引用它自己的仓库实体副本。该问题有多种手动解决方案,但它们需要付出相当大的努力来实施和维护。 ORM 是最具成本效益的解决方案,不会破坏单一职责或关注点分离。 L2S 和 EF 随时可用、免费且易于使用。
  • 它也不一定必须是 L2S 或 EF。我可能应该提到 nHibernate、SubSonic、Telerik OpenAccess。还有其他 ORM 选项通常与 L2S(或未来的 EF)做同样的事情,尽管没有那么简单和强大。 ORM 负责映射、域和持久性关注点的分离、延迟加载(或根据需要显式加载)、状态跟踪以及用于检索和更新的 sql 生成。无需复制对象实例即可有效利用内存。在 L2S 和 EF v4 的情况下,SQL 的效率非常高。
【解决方案2】:

这听起来像是在进行多对多关系。 (客户知道他们的仓库,反之亦然)

理想情况下,这似乎最适合您定义弱实体表的数据库应用程序...当然,如果我们谈论的是 10 个客户和 10 个仓库... >

假设一个数据库是多余的,这可以用一些字典在代码中建模。假设您使用 int 作为 Depot 和 Customer 的唯一标识符,您可以创建如下内容:

// creating a derived class for readability.
public class DepotIDToListOfCustomerIDs : Dictionary<int,List<int>> {}
public class CustomerIDToListOfDepotIDs : Dictionary<int,List<int>> {}
public class DepotIDToDepotObject : Dictionary<int,Depot>{}
public class CustomerIDToCustomerObject : Dictionary<int, Customer>{}
//...
// class scope for a class that manages all these objects...
DepotIDToListOfCustomerIDs _d2cl = new DepotIDToListOfCustomerIDs();
CustomerIDToListOfDepotIDs _c2dl = new CustomerIDToListOfDepotIDs();
DepotIDToDepotObject _d2do = new DepotIDToDepotObject();
CustomerIDToCustomerObject _c2co = new CustomerIDToCustomerObject();
//...
// Populate all the lists with the cross referenced info.
//...
// in a method that needs to build a list of depots for a given customer
// param: Customer c
if (_c2dl.ContainsKey(c.ID))
{
    List<int> dids=_c2dl[c.ID];
    List<Depot> ds=new List<Depot>();
    foreach(int did in dids)
    {
        if (_d2do.ContainsKey(did))
            ds.Add(_d2do[did]);
    }
}
// building the list of customers for a Depot would be similar to the above code.

编辑 1:请注意,使用上面的代码,我精心设计了它以避免循环引用。让客户引用一个也引用同一客户的仓库将防止这些被快速垃圾收集。如果这些对象将在整个应用程序生命周期中持续存在,那么当然可以采用更简单的方法。在这种方法中,您将有两个列表,一个是 Customer 实例,另一个是 Depot 实例的列表。客户和仓库将分别包含仓库和客户的列表。但是,您仍然需要两个字典才能为客户解析仓库 ID,反之亦然。生成的代码将与上述代码 99% 相同。

编辑 2: 正如其他回复中所述,您可以(并且应该)拥有一个对象代理模型来建立关系并回答有关关系的问题。 对于那些误读我的代码的人;它绝不是打算为这种情况制作绝对和完整的对象模型。 但是,它旨在说明对象代理如何以防止循环引用的方式管理这些关系。您对它在第一次运行时造成的混乱表示歉意。感谢您展示了一个很好的 OO 演示文稿,该演示文稿很容易被其他人使用。

【讨论】:

  • OO 系统可以轻松实现多对多关系。在代码之前,您的答案是好的。
  • 罗伯特,你能说得更准确一点吗?
  • 您可以将其更改为仅通过静态方法构造Customers和Depots,这可以保证有关唯一ID的单例语义。这样你就不需要奇怪的字典集,@Robert 可能会指出这一点。
  • Sixlet:那么,在内部,静态方法将使用什么来将 Depot 和 Customer ID 映射到它们各自对象的特定实例——从而满足不创建多个实例的要求?
  • 另外,sixlet,您将如何使用您提出的技术防止循环对象引用?
【解决方案3】:

回复@Jason D,并为@Nitax 着想:我真的是在略过表面,因为虽然它基本上很容易,但它也可能变得复杂。我也不会比 Martin Fowler 更好地重写它(当然不会在 10 分钟内)。

您首先必须解决内存中只有 1 个对象引用特定仓库的问题。我们将通过称为存储库的东西来实现这一点。 CustomerRepository 有一个GetCustomer() 方法,DepotRepository 有一个GetDepot() 方法。我要挥挥手,假装这一切都发生了。

其次,您需要编写一些测试来表明您希望代码如何工作。我不知道,但不管怎样,请耐心等待。

// sample code for how we access customers and depots
Customer customer = Repositories.CustomerRepository.GetCustomer("Bob");
Depot depot = Repositories.DepotRepository.GetDepot("Texas SW 17");

现在困难的部分是:想如何塑造这种关系?在 OO 系统中,您实际上不必做任何事情。在 C# 中,我可以执行以下操作。

客户保留他们所在仓库的列表

class Customer
{
    public IList<Depot> Depots { get { return _depotList; } }
}

或者,仓库会保留一份与他们合作的客户的列表

class Depot
{
    public IList<Customer> Customers { get { return _customerList; } }
}
// * code is very brief to illustrate.

在最基本的形式中,任意数量的客户可以引用任意数量的仓库。 m:n 解决了。 OO 中的引用很便宜。

请注意,我们遇到的问题是,虽然客户可以保留对其关心的所有仓库的引用列表(第一个示例),但对于仓库来说,枚举所有客户并不容易。

要获取仓库的所有客户列表(第一个示例),我们必须编写代码来迭代所有客户并检查 customer.Depots 属性:

List<Customer> CustomersForDepot(Depot depot)
{
    List<Customer> allCustomers = Repositories.CustomerRepository.AllCustomers();
    List<Customer> customersForDepot = new List<Customer>();

    foreach( Customer customer in allCustomers )
    {
        if( customer.Depots.Contains(depot) )
        {
            customersForDepot.Add(customer);
        }
    }
    return customersForDepot;
}

如果我们使用的是 Linq,我们可以这样写

var depotQuery = from o in allCustomers
                 where o.Depots.Contains(depot)
                 select o;

return query.ToList();

有 10,000,000 个客户存储在数据库中吗? 哎呀! 您真的不想每次 Depot 需要确定其客户时都加载所有 10,000,000 个客户。另一方面,如果您只有 10 个 Depot,则不时加载所有 Depot 的查询并不是什么大问题。 您应该始终考虑您的数据和数据访问策略。

我们可以CustomerDepot 中都有列表。当我们这样做时,我们必须小心执行。添加或删除关联时,我们需要同时更改两个列表。否则,我们的客户会认为他们与仓库相关联,但仓库对客户一无所知。

如果我们不喜欢这样,并决定我们真的不需要将对象耦合得如此紧密。我们可以删除显式列表并引入第三个对象,它只是关系(并且还包括另一个存储库)。

class CustomerDepotAssociation
{
    public Customer { get; }
    public Depot { get; }
}

class CustomerDepotAssociationRepository
{
    IList<Customer> GetCustomersFor(Depot depot) ...
    IList<Depot> GetDepotsFor(Customer customer) ...
    void Associate(Depot depot, Customer customer) ...
    void DeAssociate(Depot depot, Customer customer) ...
}

这是另一种选择。关联的存储库不需要公开它如何将客户与仓库关联(顺便说一下,据我所知,这是@Jason D 的代码试图做的)

在这种情况下,我可能更喜欢单独的对象,因为我们所说的是 Customer 和 Depot 的关联本身就是一个实体。

所以继续阅读一些领域驱动设计书籍,并购买 Martin Fowlers PoEAA(企业应用程序架构模式)

【讨论】:

  • 在其核心,您将拥有一个字典(或哈希表)来管理这些关系。但鉴于 OP 的要求,在我看来,这更像是一个“哪些对象有利于管理 m:n 关系”的问题;而不是“我如何隐藏关系经理的内部结构,以便它们易于使用。” (但这是一个很好的问题,你给出了一个很好的答案......)
  • 关于使用 linq 的注意事项,它通常存在性能问题。很多人都贴了关于它的文章。我最初想发布一个 linq 答案,但很快意识到我会告诉人们在我的工作中做目前正在咬我们的事情......
  • 是的 Linq (to Sql) 可能存在性能问题,但我不会立即将其忽略。我们一直在使用它,是的,您必须小心,因为就像 sql 一样,您可以编写一些性能极差的查询。 Linq 本身虽然很棒。
  • @Robert,对于所描述的情况,除非数据集很小,否则我建议不要使用它...学习 SQL 并学习正确编写它。这样你会做更多的事情来提高性能。与手写代码相比,自动生成的高级语言代码一直存在性能问题。组装也是如此。我们不在汇编中编码的原因是处理器能力的数量级性能增加。在网络吞吐量和处理器能力增加几个数量级之前,自动生成的高级代码也会增加。
  • @Jason,您过早地进行了优化,虽然您的担忧是有道理的,但他们在这个阶段是在抢先一步。 OP 在询问如何在代码中建模关系,这就是我的回答。我试图以 OP 考虑它们如何实现的方式来回答,因为数据访问模式通常是驱动程序。也就是说,除了显示 linq 查询之外,我所写的任何内容都不一定使用自动生成的代码,所以我不知道你的痛点是什么。无论如何,OP 应该为性能编写单元测试并在必要时进行优化。
【解决方案4】:

希望这是不言自明的。

OO

ER

【讨论】:

    猜你喜欢
    • 2010-09-06
    • 2016-08-16
    • 2015-09-03
    • 1970-01-01
    • 2011-10-02
    • 2021-07-30
    • 2011-03-03
    • 2016-09-11
    • 1970-01-01
    相关资源
    最近更新 更多