【问题标题】:Design question - resolving circular dependency between objects设计问题 - 解决对象之间的循环依赖
【发布时间】:2010-08-30 20:37:10
【问题描述】:

我让自己陷入了两个类之间的循环依赖,我正在尝试提出一个干净的解决方案。

这是基本结构:

class ContainerManager {
    Dictionary<ContainerID, Container> m_containers;

    void CreateContainer() { ... }
    void DoStuff(ContainerID containerID) { m_containers[containerID].DoStuff(); }
}

class Container {
    private Dictionary<ItemID, Item> m_items;

    void SetContainerResourceLimit(int limit) { ... }

    void DoStuff() {
        itemID = GenerateNewID();
        item = new Item();
        m_items[itemID] = item;
        // Need to call ResourceManager.ReportNewItem(itemID);
    }
}

class ResourceManager {
    private List<ItemID> m_knownItems;

    void ReportNewItem(ItemID itemID) { ... }

    void PeriodicLogic() { /* need ResourceLimit from container of each item */ }
}

ContainerManager 作为 WCF 服务公开:客户端可以通过它创建项目和容器的外部点。 ResourceManager 需要知道创建的新项目。它进行后台处理,有时它需要来自项目容器的信息。

现在,Container 需要有 ResourceManager(调用 ReportNewItem),它将从 ContainerManager 传递。 ResourceManager 需要来自 Container 的信息,它只能通过 ContainerManager 获取。这会创建一个循环依赖。

我更喜欢用接口初始化对象(而不是具体对象),以便以后可以为单元测试创​​建模拟对象(例如创建模拟 ResourceManager),但我仍然遇到 CM 需要的问题其 ctor 中有一个 RM,而 RM 需要其 ctor 中有一个 CM。

显然,这行不通,所以我正在尝试提出创造性的解决方案。到目前为止,我有:

1) 将要使用的Container 传递给ReportNewItem,并让ResourceManager 直接使用它。这很痛苦,因为 ResourceManager 会持续存储它知道的 ItemID。这意味着当在崩溃之后初始化 ResourceManager 时,我必须重新为其提供所需的所有容器。

2) 分两个阶段初始化 CM 或 RM:例如:RM = new RM(); CM = 新CM(RM); RM.SetCM(CM);但这很丑,我认为。

3) 使 ResourceManager 成为 ContainerManager 的成员。因此CM可以用“this”构造RM。这会起作用,但在我想创建一个 RM 模拟的测试期间会很痛苦。

4) 使用 IResourceManagerFactory 初始化 CM。让 CM 调用 Factory.Create(this),它将使用“this”初始化 RM,然后存储结果。为了测试,我可以创建一个模拟工厂,它将返回一个模拟 RM。我认为这将是一个很好的解决方案,但是为此创建一个工厂有点尴尬。

5) 将 ResourceManager 逻辑分解为特定于 Container 的逻辑,并在每个 Container 中具有不同的实例。不幸的是,逻辑确实是跨容器的。

我认为“正确”的方法是将一些代码提取到 CM 和 RM 都依赖的第三类中,但我想不出一个优雅的方法来做到这一点。我想出了要么封装“报告项”逻辑,要么封装组件信息逻辑,这两种方法似乎都不对。

任何见解或建议将不胜感激。

【问题讨论】:

  • 您是否考虑过使用System.Collections.ObjectModel 命名空间中的KeyedCollection&lt;TKey, TItem&gt;。它将帮助您简化 item 和 itemID 或 container 和 containerID 之间的类层次结构。
  • 还可以考虑使用事件而不是将函数调用链接到DoStuff()ReportNewItem()

标签: c# oop circular-dependency


【解决方案1】:

您正在寻找的是一个界面。接口允许您将共享对象的结构/定义提取到外部引用,从而可以独立于ContainerResourceManager 类进行编译,并且不依赖于任何一个。

当您创建 Container 时,您将拥有希望容器向其报告的 ResourceManager...将其传递给构造函数,或将其设置为属性。

public interface IResourceManager {
    void ReportNewItem(ItemID itemID);
    void PeriodicLogic();
}


public class Container {
    private Dictionary<ItemID, Item> m_items;

    //  Reference to the resource manager, set by constructor, property, etc.
    IResourceManager resourceManager;

    public void SetResourceManager (IResourceManager ResourceManager) {
        resourceManager = ResourceManager;
    }

    public void DoStuff() {
        itemID = GenerateNewID();
        item = new Item();
        m_items[itemID] = item;
        resourceManager.ReportNewItem(itemID);
    }
}


public class ResourceManager : IResourceManager {
    private List<ItemID> m_knownItems;

    public void ReportNewItem(ItemID itemID) { ... }
    public void PeriodicLogic() { ... }
}


//  use it as such:
IResourceManager rm = ResourceManager.CreateResourceManager(); // or however
Container container = new Container();
container.SetResourceManager(rm);
container.DoStuff();

将此概念扩展到您的每个循环引用。


* 更新 *

您不需要将所有依赖项都删除到一个接口中...这完全可以,例如,ResourceManager 了解/依赖于 Container

【讨论】:

    【解决方案2】:

    解决方案 5 怎么样,但让容器派生自实现您提到的跨容器逻辑的公共基类?

    【讨论】:

      【解决方案3】:

      只有你的短 sn-p(我敢肯定,这是一个必需的约束 - 但很难知道 ResourceManager 是否可以变成单例。)这是我的快速想法

      1) 当ReportNewItem()被调用时,你能不能不直接把item所在的容器传给ResourceManager?这样,RM 就不需要接触 containermanager。

      class Container {
          private IResourceManager m_rm; //.. set in constructor injection or property setter
      
          void DoStuff() {
              itemID = GenerateNewID();
              item = new Item();
              m_items[itemID] = item;
              m_rm.ReportNewItem(this, itemId);
          }
      }
      
      class ResourceManager {
          private List<ItemID> m_knownItems;
          private Dictionary<ItemID, Container> m_containerLookup;        
      
          void ReportNewItem(Container, ItemID itemID) { ... }
      
          void PeriodicLogic() { /* need ResourceLimit from container of each item */ }
      }
      

      2) 我是工厂的粉丝。一般来说,如果构造或检索一个类的正确实例不仅仅是new(),出于关注点分离的原因,我喜欢将它放在工厂中。

      【讨论】:

        【解决方案4】:

        谢谢大家的回答。

        jalexiou - 我会调查 KeyedCollection,谢谢(天哪,我真的需要注册才能发布 cmets)。

        James,正如我所写,我确实想使用接口(如果没有别的,它可以简化单元测试)。我的问题是,要初始化实际的 ResourceManager,我需要传递 ComponentManager,而要初始化 CM,我需要传递 RM。您建议的基本上是两阶段初始化,我将其称为解决方案 2。我宁愿避免这种两阶段初始化,但也许我在这里过于虔诚。

        Philip,我认为将 Component 传递给 ReportNewItem 会暴露给 ResourceManager 太多(因为 Component 支持我不希望 ResourceManager 访问的各种操作)。

        不过,再想一想,我可以采取以下方法:

        class ComponentManager { ... }
        
        class Component {
            private ComponentAccessorForResource m_accessor;
            private ResourceManager m_rm;
        
            Component(ResourceManager rm) {
                m_accessor = new ComponentAccessorForResource(this);
                m_rm = rm;
            }
            void DoStuff() {
                Item item = CreateItem();
                ResourceManager.ReportNewItem(item.ID, m_accessor);
            }
            int GetMaxResource() { ... }
         }
        
         class ComponentAccessorForResource {
             private Component m_component;
             ComponentAccessorForResource(Component c) { m_component = c; }
             int GetMaxResource() { return m_component.GetMaxResource(); }
         }
        
         ResourceManager rm = new ResourceManager();
         ComponentManager cm = new ComponentManager(rm);
        

        这对我来说似乎足够干净。希望没有人不同意:)

        我最初反对传递组件(或者实际上类似于我在这里提出的访问器)是我必须在初始化时将它们重新提供给 ResourceManager,因为 ResourceManager 会持续存储它拥有的项目。但事实证明,无论如何我都必须用 Items 重新初始化它,所以这不是问题。

        再次感谢您的精彩讨论!

        【讨论】:

        • 我有点困惑,因为我们从 ContainerManager 和 Container 切换到 ComponentManager 和 Component... 假设它们是同一个东西。您给出的代码中缺少很多细节......我能说的最好的是 ComponentManager 似乎是不必要的,或者至少不涉及 Component/ResourceManager 关系。一个组件知道它的 ResourceManager ......到目前为止,这是有道理的。 ResourceManager 了解组件,因为它与这些组件一起工作。还是可以的。 (更多)
        • 如果ComponentManager是管理组件所必需的,好吧,但是没有理由让它在Component和ResourceManager之间进行调解。不让 CM 了解 RM 可以解决您的循环依赖问题。我根本不清楚 ComponentAccessor 给你什么。它看起来像是一个不必要的包装器,只存储一个组件,包装一个函数而不增加任何值;您不妨公开 Component.GetMaxResource。如果您对我的建议不满意,我仍然鼓励您多考虑一下,想出另一种方法
        【解决方案5】:

        詹姆斯,

        是的,ComponentManager 和 ContainerManager 是一回事(我的真实代码中的名称完全不同,我试图为代码 sn-ps 选择“通用”名称 - 我把它们弄糊涂了)。如果您认为有任何其他细节会有所帮助,请告诉我,我会提供。我试图保持 sn-p 简洁。

        您是正确的,ComponentManager 没有直接参与 Component/ResourceManager 关系。我的问题是我希望能够使用不同的 ResourceManager 进行测试。实现这一点的一种方法是让 CM 将 RM 提供给 Component(实际上,只有一个 RM,因此它必须由每个 Component 以外的其他人构建)。

        除了隐藏我不想让 ResourceManager 知道的 Component 部分(同时允许使用 ComponentAccessorMock 测试 ResourceManager)之外,ComponentAccessor 几乎没有做任何事情。同样的事情可以通过让组件实现一个接口来实现,该接口只公开我希望 RM 使用的方法。这实际上是我在代码中所做的,我怀疑这就是您所说的“暴露 Component.GetMaxResource”。

        现在的代码大致如下:

        // Initialization:
        
        RM = new RM();
        CM = new CM(RM);   // saves RM as a member
        
        //
        // Implementation
        //
        
        // ComponentManager.CreateComponent
        C = new Component(m_RM);  // saves RM as a member
        
        // Component.CreateNewItem
        {
            Item item = new Item();
            m_RM.ReportNewItem(this, item);
        }
        

        并且 ReportNewItem 需要一个接口来公开它需要的方法。这对我来说似乎相当干净。

        一种可能的替代方法是使用策略模式使 ResourceManager 可定制,但我不确定这会给我带来什么。

        我很高兴听到您(当然,其他任何人)对这种方法的看法。

        【讨论】:

          猜你喜欢
          • 2017-06-24
          • 2020-09-25
          • 1970-01-01
          • 2019-02-23
          • 2021-08-25
          • 2020-11-12
          相关资源
          最近更新 更多