依赖倒置和普遍依赖

时间:2012-10-24 20:24:54

标签: c++ dependency-injection inversion-of-control

我正在尝试获取依赖项反转,或至少了解如何应用它,但我目前遇到的问题是如何处理普遍存在的依赖项。典型的例子是跟踪日志记录,但在我的应用程序中,我有许多服务,大多数(如果不是所有代码)将依赖于(跟踪日志记录,字符串操作,用户消息日志记录等)。

对此的解决方案似乎都不是特别适合:

  • 使用构造函数依赖注入意味着大多数构造函数都会有多个标准注入依赖项,因为大多数类明确需要这些依赖项(它们不仅仅是将它们传递给它们构造的对象)。
  • 服务定位器模式只是将依赖项驱动到地下,将它们从构造函数中删除但隐藏它们以便它甚至不需要显式依赖项是必需的
  • 单身人士服务是单身人士,也可以隐藏依赖关系
  • 将所有这些常见服务集中到一个CommonServices接口并注入a)违反了Demeter法则,b)实际上只是服务定位器的另一个名称,虽然是特定的而不是通用的。

有没有人对如何构建这些类型的依赖关系有任何其他建议,或者确实有任何上述解决方案的经验?

请注意,我没有考虑特定的DI框架,事实上我们正在使用C ++进行编程,并且会手动执行任何注入(如果确实注入了依赖项)。

3 个答案:

答案 0 :(得分:3)

class Base {
 public:
  void doX() {
    doA();
    doB();
  }

  virtual void doA() {/*does A*/}
  virtual void doB() {/*does B*/}
};

class LoggedBase public : Base {
 public:
  LoggedBase(Logger& logger) : l(logger) {}
  virtual void doA() {l.log("start A"); Base::doA(); l.log("Stop A");}
  virtual void doB() {l.log("start B"); Base::doB(); l.log("Stop B");}
 private:
  Logger& l;
};

现在,您可以使用了解记录器的抽象工厂创建LoggedBase。没有人知道记录器,也不需要了解LoggedBase。

class BaseFactory {
 public:
  virtual Base& makeBase() = 0;
};

class BaseFactoryImp public : BaseFactory {
 public:
  BaseFactoryImp(Logger& logger) : l(logger) {}
  virtual Base& makeBase() {return *(new LoggedBase(l));}
};

工厂实现保存在一个全局变量中:

BaseFactory* baseFactory;

并通过'main'或接近main的某个函数初始化为BaseFactoryImp的实例。只有该函数知道BaseFactoryImp和LoggedBase。其他人都对这一切都一无所知。

答案 1 :(得分:2)

  

服务定位器模式只是将依赖关系驱动到地下,   单身人士服务也是单身人士,也可以隐藏   依赖

这是一个很好的观察。隐藏依赖项并不会删除它们。相反,您应该解决类所需的依赖项数量。

  

使用构造函数依赖注入将意味着大多数   构造函数将具有多个标准注入依赖项   因为大多数类明确要求这些依赖

如果是这种情况,您可能违反了Single Responsibility Principle。换句话说,那些课程可能太大而且做得太多。既然你在谈论记录和追踪,你应该问问自己aren't logging too much。但总的来说,日志记录和跟踪是跨领域的问题,您不必将它们添加到系统中的许多类中。如果您正确应用SOLID原则,则此问题就会消失(正如here所述)。

答案 2 :(得分:2)

依赖性倒置原则是SOLID原则的一部分,是提升可测试性和重用高级算法的重要原则。

背景: 正如Bob叔叔的网页所示,依赖性倒置依赖于抽象而非结果。

实际上,会发生的事情是,您的类直接实例化另一个类的某些地方需要进行更改,以便调用者可以指定内部类的实现。

例如,如果我有一个Model类,我不应该硬编码它来使用特定的数据库类。如果我这样做,我不能使用Model类来使用不同的数据库实现。如果您有不同的数据库提供程序,或者您可能希望将数据库提供程序替换为假数据库以进行测试,那么这可能很有用。

而不是Model在Database类上执行“new”,它只是使用Database类实现的IDatabase接口。模型永远不会引用具体的数据库类。但那么谁实例化数据库类?一种解决方案是构造函数注入(依赖注入的一部分)。对于此示例,Model类被赋予一个新的构造函数,该构造函数接受要使用的IDatabase实例,而不是实例化它本身。

这解决了Model的原始问题,不再引用具体的Database类,并通过IDatabase抽象使用数据库。但它引入了问题中提到的问题,即它违反了德米特法则。也就是说,在这种情况下,Model的调用者现在必须知道IDatabase,而以前它没有。该模型现在向客户展示了如何完成工作的一些细节。

即使你对此感到满意,还有一个问题似乎让很多人感到困惑,包括一些培训师。假设任何时候类,例如Model,具体地实例化另一个类,那么它就打破了依赖性倒置原则,因此它很糟糕。但实际上,你不能遵循这些类型的硬性规则。有时你需要使用具体的类。例如,如果您要抛出异常,则必须“新建它”(例如,抛出新的BadArgumentException(...))。或者使用基本系统中的类,如字符串,词典等。

在所有情况下都没有简单的规则。你必须要了解你想要完成的是什么。如果您处于可测试性之后,那么Model类直接引用Database类这一事实本身并不是问题。问题是Model类没有其他方法可以使用另一个Database类。您可以通过实现Model类来解决此问题,使其使用IDatabase,并允许客户端指定IDatabase实现。如果客户端未指定一个,则模型可以使用具体实现。

这类似于许多库的设计,包括C ++标准库。例如,查看声明std :: set container:

template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T> >    // set::allocator_type
           > class set;

您可以看到它允许您指定比较器和分配器,但大多数情况下,您采用默认值,尤其是分配器。 STL有许多这样的方面,特别是在IO库中,可以为本地化,字节序,语言环境等增加流的详细方面。

除了可测试性之外,这允许重用更高级别的算法,使用与算法内部使用的类完全不同的实现。

最后,回到我之前关于你不想反转依赖关系的场景的断言。也就是说,有时您需要实例化一个具体的类,例如在实例化异常类时BadArgumentException。但是,如果你在可测试性之后,你也可以做出你所做的论证,实际上也想要反转它的依赖性。您可能希望设计Model类,以便将异常的所有实例化委托给一个类并通过抽象接口调用。这样,测试Model类的代码可以提供自己的异常类,然后测试可以监视它。

我有同事给我举例,他们抽象甚至系统调用的实例化,例如“获取系统时间”,这样他们就可以通过单元测试来测试夏令时和时区场景。

遵循YAGNI原则 - 不要仅仅因为您认为可能需要它而添加抽象。如果你正在练习测试优先开发,那么正确的抽象就会变得明显,只有足够的抽象才能通过测试。