设计问题 - 解决对象之间的循环依赖关系

时间:2010-08-30 20:37:11

标签: c# oop circular-dependency

我让自己陷入两个班级之间的循环依赖,我正试图想出一个干净的解决方案。

这是基本结构:

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需要知道创建的新项目。它进行后台处理,有时它需要来自Item的容器的信息。

现在,Container需要具有ResourceManager(调用ReportNewItem),它将从ContainerManager传递。 ResourceManager需要来自Container的信息,它只能使用ContainerManager获取。这会产生循环依赖。

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

显然,这不起作用,所以我试图提出创造性的解决方案。到目前为止,我有:

1)传递给ReportNewItem要使用的Container,让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都依赖的第三类中,但我无法想出一种优雅的方法。我想出了封装“报告的项目”逻辑,或封装组件信息逻辑,这两者似乎都没有。

非常感谢任何见解或建议。

5 个答案:

答案 0 :(得分:2)

您正在寻找的是界面。接口允许您将共享对象的结构/定义提取到外部引用,允许它独立于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

答案 1 :(得分:0)

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

答案 2 :(得分:0)

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

1)调用ReportNewItem()时,你能不能只将项目所在的容器传递给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(),我喜欢将它放在工厂中以分离关注原因。

答案 3 :(得分:0)

谢谢大家的答案。

jalexiou - 我将调查KeyedCollection,谢谢(谢谢,我真的需要注册,所以我可以发表评论)。

詹姆斯,正如我写的那样,我确实想要使用接口(如果没有别的话,它简化了单元测试)。我的问题是初始化实际的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);

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

我最初反对传递Component(或者实际上类似我在这里提出的访问器)的原因是我必须在初始化时将它们重新提供给ResourceManager,因为ResourceManager会持久存储它拥有的Items。但事实证明我必须使用Items重新初始化它,所以这不是问题。

再次感谢您的讨论!

答案 4 :(得分:0)

詹姆斯

是的,ComponentManager和ContainerManager是同一个(我的真实代码中的名称完全不同,我试图为代码片段选择“通用”名称 - 我让他们感到困惑)。如果您认为有任何其他细节会有所帮助,请告诉我,我会提供给他们。我试图让代码片保持简洁。

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

除了隐藏我不希望ResourceManager知道的Component部分(同时允许使用ComponentAccessorMock测试ResourceManager)之外,ComponentAccessor几乎没有什么作用。通过让Component实现一个只暴露我希望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可自定义,但我不确定这会给我带来什么。

我很高兴听到你(或其他任何人)当然会想到这种方法。