依赖性反转和普遍依赖性

Dependency inversion and pervasive dependencies

本文关键字:依赖性      更新时间:2023-10-16

我正在尝试获得依赖性反转,或者至少了解如何应用它,但是目前我遇到的问题是如何处理普遍存在的依赖关系。这样的经典示例是跟踪记录,但是在我的应用程序中,我有许多服务,大多数代码(如果不是全部)都将取决于(跟踪记录,字符串操纵,用户消息记录等)。

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

  • 使用构造函数依赖注入将意味着大多数构造函数都会具有多种标准注入的依赖项,因为大多数类都明确需要这些依赖项(它们不仅将它们传递到它们构造的对象)。
  • )。
  • 服务定位器模式只是将依赖项驱动到地下,将它们从构造函数中删除,但将它们隐藏起来,以便甚至没有明确要求依赖关系
  • Singleton Services是Singletons,也有助于隐藏依赖关系
  • 将所有这些共同的服务集中到一个单一的共同服务接口中,并注入a)a)违反demeter和b)的定律确实只是服务定位器的另一个名称,尽管是特定的,而不是通用的。

有人对如何构建这些依赖性或上述任何解决方案的任何经验有其他建议吗?

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

服务定位器模式只是将依赖项驱动到地下, Singleton服务是Singletons,也有助于隐藏 依赖项

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

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

如果是这种情况,您可能违反了单一责任原则。换句话说,这些课程可能太大了,做得太多了。由于您谈论的是记录和跟踪,因此您应该问自己是否没有登录太多。但总的来说,记录和跟踪是交叉切割的问题,您不必将它们添加到系统中的许多类中。如果您正确地应用了坚实的原则,则此问题消失了(如下所述)。

依赖性反转原理是稳固原理的一部分,是其他事物的重要原理,可以促进高级算法的可检测性和重复使用。

背景:如鲍勃叔叔的网页上所示,依赖性反转是关于依赖于抽象,而不是对具体的。

实际上,发生的事情是,您的班级直接实例化另一个类,需要更改,以便可以由呼叫者指定内部类的实现。

例如,如果我有一个模型类,则我不应该硬编码以使用特定的数据库类。如果这样做,我将无法使用模型类使用不同的数据库实现。如果您有不同的数据库提供商,这可能很有用,或者您可能需要用伪造的数据库替换数据库提供商。

而不是在数据库类上执行"新"的模型,它将仅使用数据库类实现的IDATABASE接口。该模型从不涉及混凝土数据库类。但是谁实例化数据库类?一种解决方案是构造函数注入(依赖注射的一部分)。在此示例中,为模型类提供了一个新的构造函数,该构造函数采用IDATABASE实例,而不是本身实例化。

这解决了模型的原始问题不再引用混凝土数据库类,并通过IDATABase抽象使用数据库。但这介绍了问题中提到的问题,即它违反了Demeter的法律。也就是说,在这种情况下,模型的呼叫者现在必须了解IDATABASE,而此前却没有。该模型现在正在向客户展示有关如何完成工作的细节。

即使您对此表示满意,还有另一个问题似乎使很多人感到困惑,包括一些培训师。有一个假设是,任何一个类,例如模型,都可以具体实例化,然后破坏依赖性反转原理,因此很糟糕。但是实际上,您不能遵循这些类型的硬性规则。有时您需要使用具体类。例如,如果您要抛出一个异常,则必须" new it up up"(例如,抛出新的badargumentException(...))。或使用基本系统中的类,例如字符串,词典等

在所有情况下都没有简单的规则。您必须了解您要完成的工作。如果您正在测试性,那么模型类引用数据库类直接的事实本身并不是问题。问题是,模型类没有其他使用其他数据库类的方法。您可以通过实现模型类使用IDATABASE来解决此问题,并允许客户端指定IDATABASE实现。如果客户端未指定一个,则可以使用具体实现。

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

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。但是,如果您要测试性,您也可以提出一个论点,实际上,您也想将其反转依赖性。您可能需要设计模型类,以便将所有异常的实例化委派给一个类并通过抽象接口调用。这样,测试模型类的代码可以提供其自己的异常类,其用法可以监视。

我有同事给我示例,在这些示例中,他们简单地将" getSystemtime"均匀实例化,例如" getSystemtime",以便他们可以通过单位测试来测试日光节省和时区场景。

遵循Yagni原则 - 不要仅仅因为您认为可能需要它而添加抽象。如果您正在练习测试优先开发,则正确的抽象变得显而易见,并且仅实现了足够的抽象来通过测试。

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;
};

现在,您可以使用了解记录器的抽象工厂来创建脚下底。没有其他人必须了解记录仪,也不需要了解栏杆。

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