【问题标题】:Dependency inversion and pervasive dependencies依赖倒置和普遍依赖
【发布时间】:2012-10-24 20:24:53
【问题描述】:

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

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

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

是否有人对如何构建这些依赖项有任何其他建议,或者确实对上述任何解决方案有任何经验?

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

【问题讨论】:

  • 做任何能让你完成工作的事情。
  • @Petah 好的,所以我将使用全局变量和函数来完成。这不是问题的重点。我希望一个有经验的依赖注入专家可能有一些有用的智慧。
  • 有 C++ 的 DI 框架。你可能想看看:stackoverflow.com/questions/79825/…
  • 我知道有 C++ 的 DI 框架,但是此时使用它们对我们来说并不实用。

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


【解决方案1】:

服务定位器模式只是将依赖项驱动到地下, 单例服务是单例服务,也可以用来隐藏 依赖关系

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

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

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

【讨论】:

  • 我期待“日志记录是一个横切关注点,因此您只使用方面”的答案,并且几乎为此问题添加了一些内容。实际上,尽管您确实希望在代码中跟踪日志记录,但似乎没有必要将其从相关的源代码行移开。
  • 虽然可能存在违反 SRP 的情况,但我并不完全认同这个论点。然而,即使这是真的,你最终要做的只是加载更多的类,每个类都有更少的依赖关系,但是你必须构建所有这些东西的复杂树,传递给构造函数的参数总数实际上会增加构建该图比以前更难。
  • @TomQuarendon:你确定你读过referenced answer,尤其是那里完整引用的the article吗?所描述的设计明确地试图通过定义一些包含该逻辑的装饰器来防止“加载更多类”。而且不要忘记,许多小班并不是一件坏事。如果设计得当,它实际上会使系统更简单。并且不要忘记构建复杂的对象图不是您通常手工完成的事情。这就是 DI 框架的用武之地。
  • 我读过引用的文章是的,但直到现在我才看到对你提到的文章的引用(在评论的底部)。我们正在使用 C++,而不是使用 DI 框架,我只是在尝试设计最好的系统,并以最好的方式应用依赖倒置。我并没有说小类不好,只是很多小类每个都有较少的依赖项只是意味着你的对象构造更难,整个系统仍然有相同数量的依赖项。
  • 引用的答案专门针对.net,但类似的原则也可用于c++。实际上,对于您给出答案的示例(安全性、审计、性能日志记录),装饰器很好。尽管有许多因素在 c++ 中有所缓解。毫无疑问,许多小的堆分配类会导致性能问题。此外,对于我所考虑的装饰器来说,这些东西是不实用的。现在这可能是因为设计一开始就不正确,但我不相信。
【解决方案2】:

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

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

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

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

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

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

即使您对此表示满意,还有一个问题似乎让很多人感到困惑,包括一些培训师。假设任何时候一个类,例如模型,具体实例化另一个类,那么它就违反了依赖倒置原则,因此它是坏的。但在实践中,您不能遵循这些硬性规定。有时您需要使用具体的类。例如,如果你要抛出一个异常,你必须“更新它”(例如 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 原则——不要仅仅因为你认为你可能需要它就添加抽象。如果您正在练习测试优先的开发,那么正确的抽象就会变得很明显,并且只有实现了足够的抽象才能通过测试。

【讨论】:

    【解决方案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。其他所有人都对他们一无所知。

    【讨论】:

    • 使工厂成为全局变量会稍微颠覆依赖注入,但我明白你的意思。
    • 其实想想,这并不是我所说的日志记录。我不购买内部跟踪记录的“装饰器”或“方面”方法。如果您想要的是进入/退出/参数/返回跟踪,这种方法很好,但这很少是我想要的。更有用的是对某些方法执行的内部跟踪。这样做你只能在公共接口点写入跟踪条目到方法。如果方法实现比较复杂并且有很多私有子方法,那么在其执行过程中你不能写任何跟踪条目。
    • 我所说的用户日志是针对用户的输出。我想抽象出它的去向,我不想硬编码写入 std::cout、System.out 等,我不想要一个全局变量,我不想传递一个对象对所有人和杂物。我想我现在明白了纯依赖注入是如何解决这个问题的,并且会在可能的时候写出来。
    猜你喜欢
    • 2022-11-20
    • 2015-10-04
    • 1970-01-01
    • 1970-01-01
    • 2019-03-06
    • 2017-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多