【问题标题】:Dependency Injection mentioned by C++ Google Mock guideC++ Google Mock 指南提到的依赖注入
【发布时间】:2024-04-23 05:30:02
【问题描述】:

在这种情况下,依赖注入(控制反转)是什么意思 (Google Mock):

让我们看一个例子。假设你正在开发一个图形 依赖于类似 LOGO 的 API 进行绘图的程序。你会如何测试 它做正确的事吗?好吧,您可以运行它并比较 带有金色屏幕快照的屏幕,但让我们承认这一点:测试如 这运行起来很昂贵而且很脆弱(如果你刚刚升级到 具有更好抗锯齿功能的闪亮新显卡?突然你 必须更新你所有的黄金图像。)。如果是的话就太痛苦了 你所有的测试都是这样的。幸运的是,您了解了 依赖注入并知道正确的做法:而不是拥有 您的应用程序直接与绘图 API 对话,将 API 包装在 接口(比如 Turtle)和该接口的代码

class Turtle {
  ...
  virtual ~Turtle() {}
  virtual void PenUp() = 0;
  virtual void PenDown() = 0;
  virtual void Forward(int distance) = 0;
  virtual void Turn(int degrees) = 0;
  virtual void GoTo(int x, int y) = 0;
  virtual int GetX() const = 0;
  virtual int GetY() const = 0;
};

通过类在应用程序代码和绘图API之间添加另一层时与DI有什么关系?在许多关于依赖注入的 Java 示例中,通常不应在类中具体创建对象。相反,它应该在别处创建以分离两个对象之间的实现耦合。例如(来源codeproject):

解决方案:

当我在 * 上搜索有关 DI 的答案时,通常会在 Java 的上下文中询问它。一些示例使用了 Java GUI。通常这些例子是如此简单和如此明显,以至于我看不到它的意义,除了具有更好的设计和更少的耦合。但是,我想了解的是它背后的含义。正如wiki 中定义的那样,控制反转 (IoC) 意味着您反转代码的控制流。那么,它如何适用于 Google 案?与程序风格相比,实际流程如何反转?我以为代码是从上到下逐行顺序执行的,而不是从下到上?

【问题讨论】:

    标签: c++ dependency-injection


    【解决方案1】:

    *对这些有很好的定义:

    1. http://en.wikipedia.org/wiki/Inversion_of_control
    2. http://en.wikipedia.org/wiki/Dependency_injection

    依赖注入只是一种奇特的说法,即类通过接口(图形 API)与另一个类交互,并且它提供了一种改变接口指向的方法(即注入对另一个类的依赖项)。

    对于控制反转,*提到了工厂模式之类的东西。

    它还提到了 setter 注入(使用 setter 函数更改接口实现)、构造注入(从构造函数设置接口实现)或接口注入(从另一个接口请求接口实现)并指出这些是 Dependency 的类型注射。

    这就是这里发生的事情 - 程序可以使用 setter 方法(控制反转)更改海龟程序的绘图 API(依赖注入)。

    这允许你有一个这样的测试类:

    struct DrawingTester : public DrawingInterface
    {
        void move_to(long x, long y) { printf("moveto %d %d\n", x, y); }
        void line_to(long x, long y) { printf("lineto %d %d\n", x, y); }
    };
    

    并通过测试程序驱动它:

    int main(int argc, char **argv) {
        DrawingTester drawing;
        Turtle t;
        t.setDrawingApi(&drawing);
        t.runProgramFromFile(argv[0]);
        return 0;
    }
    

    然后,您可以让海龟/徽标测试程序获得来自 DrawingTester 的预期输出。例如:

    # test1.logo
    
    MOVE 5, 7
    
    # test1.calls
    
    moveto 5 7
    

    并通过测试套件(例如 https://github.com/rhdunn/cainteoir-engine/blob/master/tests/harness.pyhttps://github.com/rhdunn/cainteoir-engine/blob/master/tests/metadata.py)来驱动它。

    【讨论】:

    • +1,但您的示例会更好,不使用printf,而是将命令存储在列表中,并展示如何在实际单元测试中检查预期结果。
    • @DocBrown 是可能的,但是我更喜欢我提到的方法:(1)它将测试代码与测试数据分开,使没有编码人员更容易添加和理解测试; (2) 更容易维护; (3) 你可以用它来检查没有测试用例的问题源文件。第 (3) 点对我自己的项目非常有用。
    【解决方案2】:

    DI 在这里的意思是,你的程序没有对类 LOGO api 的硬编码依赖,但是这种依赖是在运行时通过接口“注入”的。这样,为了单元测试的目的,可以用 Mock API 替换 api。

    “控制反转”的意思是:如果您的程序中有一个函数需要类似 LOGO 的 API 的“图形上下文对象”,它不应该自行实例化该对象。相反,它应该获取作为参数给出的上下文对象(键入到“Turtle”接口),并且可以通过例如 IoC 框架来创建对象。

    这将与您在上面的“客户地址”示例中显示的方式相同,将 clsAddress 替换为 clsGraphicsContext

    【讨论】:

    • 谢谢。我理解不实例化对象本身,以及更多导致工厂模式的示例。但是,我在这里有一个问题:“您的程序没有对类 LOGO api 的硬编码依赖”是什么意思?在我看来,应用程序应该始终受到外部 API 的保护,通过添加我们自己的包装类(如 Turtle)。而不是硬编码,这意味着我们不是在我们的程序中调用类似 LOGO 的 API(以后可以更改 API),而是添加另一个层 Turtle,以免破坏我们的程序?
    • API(在本例中为 Turtle)定义了类/方法和您的程序之间的接口/契约(例如 ParseLogo(file, turtle))。这允许您的程序定义它如何响应接口事件,因此类/方法不必允许它被重用。该程序确实对接口(合同)有硬编码依赖,但具体实现被注入。如果徽标文件解析器也进行绘图,则仅限于此,而接口版本不是。
    • @Amumu:Turtle API 的主要目的是单元测试,而不是解耦 API 更改。您的模拟图形上下文可以检查它是否从程序的“被测对象”部分获得预期的 API 调用。在您在其他地方只能通过一些脆弱的位图比较来检查测试结果的情况下,附加层是有意义的。如果您的 LOGO api 提供了适合测试的自己的机制(例如,自定义的 graphix 上下文对象,您可以自己实现),则不需要“Turtle”层。
    • 这完全取决于您要达到的目标。示例中的海龟 API 用途有限(需要了解如何处理一些海龟命令)。如果您将 API 定义为绘图图元 (move_to/line_to/set_pen_color/...),除了测试之外,您还可以支持绘图到屏幕、位图文件、可缩放矢量图形文件、共享海龟逻辑的 windows 元文件。它还允许您为其他绘图算法重用绘图接口和处理程序。在创建 API 时考虑如何使用它。
    • @Amumu ParseLogo 是方法,Turtle 是合约(接口)。这允许ParseLogo 的调用者传递他们自己的接口实现——在这种情况下,支持测试(请参阅我的答案以了解如何做到这一点)。 ParseLogo 函数可以重复使用以支持对屏幕以外的事物进行绘图。如果ParseLogo 直接进行绘图,则不能重用它来绘制到 Windows 图元文件或不同的平台(即可能有 WinTurtleLinuxTurtleMacTurtle 实现在这些平台上绘制)。
    【解决方案3】:

    “控制反转”中的反转不是代码执行顺序的反转,而是对象创建的反转。在不使用“依赖注入”的架构中,对象将自己创建它们的依赖关系。相反,当使用DI时,对象会从外部接收它们的依赖,并使用接口(C++中的抽象类)来获得与实现的独立。

    在 LOGO 示例中,通过使用接口和设置器而不是直接创建 API 包装器,您允许调用您的代码提供接口的实现。这样,测试您的代码(通过提供一个记录所有调用的模拟实现)或使用其他实现更容易。

    【讨论】:

      【解决方案4】:

      在我看来,您有点“等同”依赖注入和控制反转。

      现在,据我了解,依赖注入是一种设计模式(粗略地说),您将指向一个对象的指针“注入”到另一个对象中;这将使第二个依赖于第一个(即,它需要知道前者的接口,并且会受到接口级别更改的影响)。因此,这是一个非常通用的概念,可以在许多不同的情况下提供帮助。

      从这个意义上说,依赖注入只是实现控制反转的一种方式。例如,当您将回调注入对象时,就会发生这种情况,以便在适当的时候调用它们。

      在您的具体示例中,我怀疑依赖注入被用作获得控制反转的一种方式,因为在我看来控制反转并不像它发生的那样,例如,当您有回调或使用框架时) .

      正如你所说的,更多的情况是添加一个中间层以更好地解耦(也可以通过依赖注入获得)。

      【讨论】: