5

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

对此的解决方案似乎都不是特别可口:

  • 使用构造函数依赖注入意味着大多数构造函数将有几个、许多标准注入的依赖项,因为大多数类都明确需要这些依赖项(它们不仅仅是将它们传递给它们构造的对象)。
  • 服务定位器模式只是将依赖项驱动到地下,将它们从构造函数中删除,但隐藏它们,因此甚至不需要明确的依赖项
  • 单例服务是单例,也可以用来隐藏依赖项
  • 将所有这些公共服务集中到一个 CommonServices 接口中并将其注入 a) 违反了德米特法则 b) 实际上只是服务定位器的另一个名称,尽管它是一个特定的而不是通用的。

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

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

4

3 回答 3

2
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。其他所有人都对他们一无所知。

于 2013-05-22T16:57:06.387 回答
2

服务定位器模式只是将依赖项驱动到地下,单例服务是单例,并且还用于隐藏依赖项

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

使用构造函数依赖注入意味着大多数构造函数将有几个、许多标准注入依赖项,因为大多数类都明确需要这些依赖项

如果是这种情况,您可能违反了单一职责原则。换句话说,那些类可能太大了,做的太多了。由于您正在谈论日志记录和跟踪,因此您应该问问自己是否没有记录太多。但总的来说,日志记录和跟踪是横切关注点,您不必将它们添加到系统中的许多类中。如果您正确地应用SOLID原则,这个问题就会消失(如此处所述

于 2012-10-25T14:25:45.093 回答
2

依赖倒置原则是 SOLID 原则的一部分,并且是促进更高级别算法的可测试性和重用的重要原则。

背景:正如鲍勃叔叔的网页所指出的,依赖倒置是关于依赖于抽象,而不是依赖于具体。

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

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

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

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

即使你对此没意见,还有一个问题似乎让很多人感到困惑,包括一些培训师。假设任何时候一个类,例如 Model,具体实例化另一个类,那么它就违反了依赖倒置原则,因此它是坏的。但在实践中,您不能遵循这些硬性规定。有时您需要使用具体的类。例如,如果你要抛出一个异常,你必须“更新它”(例如 throw new BadArgumentException(...))。或者使用来自基本系统的类,例如字符串、字典等。

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

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

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 时。但是,如果你追求可测试性,你也可以提出你所做的论点,事实上,你也想反转 this 的依赖关系。您可能希望设计 Model 类,以便将所有异常实例委托给一个类并通过抽象接口调用。这样,测试 Model 类的代码可以提供自己的异常类,然后测试可以监视其使用情况。

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

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

于 2013-05-17T18:33:30.177 回答