我试图通过this tutorial中一些简单但具体的代码和类(用Python实现)来理解依赖反转原理(DIP).我(用我自己的 comments 和理解)对此进行了总结,以节省您经历整个过程的痛苦.

基本上,我们正在构建一个货币转换器应用程序,其中我们将main应用程序逻辑与货币转换器本身分开.代码(一些注释和我的文档字符串)如下所示.

Snippet 1
#!/usr/bin/env python3
# encoding: utf-8

"""Currency converter application using some exchange API."""
class FXConverter:
    """The converter class."""
    def convert(self, from_currency, to_currency, amount):
        """
        Core method of the class. Assume the magic number 1.2 is from some API like 
        Oanda
        """
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2


class App:
    """The Application"""
    def start(self):
        """The main method to create and invoke the converter object."""
        converter = FXConverter()
        converter.convert('EUR', 'USD', 100)


if __name__ == '__main__':
    app = App()
    app.start()

现在,教程声称(直接引用)

future ,如果FX的API发生变化,它将破解代码.此外,如果您想使用不同的API,则需要更改App类.

所以他们提出了这一点.

Snippet 2
#!/usr/bin/env python3
# encoding: utf-8

"""Currency converter application using dependency inversion."""
from abc import ABC


class CurrencyConverter(ABC):
    def convert(self, from_currency, to_currency, amount) -> float:
        pass

class FXConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using FX API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2 # The tutorial seems to have a typo here. 

class App:
    def __init__(self, converter: CurrencyConverter):
        self.converter = converter

    def start(self):
        self.converter.convert('EUR', 'USD', 100)

if __name__ == '__main__':
    converter = FXConverter()
    app = App(converter)
    app.start()

Question

在我看来,这句话听起来并不真实,这切中了我为什么不能理解股市下跌的核心原因.即使FXConverter使用不同的交易所API(假设彭博而不是Oanda),这种变化不会局限于convert方法吗?只要convert方法维护签名

convert(str, str, float)->float # The strings must be valid currency names 

App.start人应该是幸福的.维护此有效方法签名的必要性是

  • 即使是在浸渍版中也没有被go 除.
  • 在更安全的语言(如Rust或C++)中自动强制执行.在更严格的语言中,我可能会列举可能的货币范围,以确保字符串变量不是像US$£等这样的自由形式.

这就是为什么我根本看不出DIP如何有助于更好地解耦,而我们实际上需要的是遵守静态类型语言中的函数/方法签名?

通常,当A调用B(对象方法或函数等)时,我们能假定

  • A不知道B的内部工作原理
  • B不知道A对结果做了什么

以便自动实施所需的脱钩?

推荐答案

好的,所以首先请注意,您的两个代码片段并不完全相同.第二个将converterself相加.虽然这只是很小的区别,但你应该注意到这只会分散你对主要问题的注意力.它们都可以以任何方式实现(无论是否附加到self),解耦和依赖注入的概念将保持不变.也就是说,在构造函数中将依赖项附加到self是DI中的一种典型方法.

第二个不同之处在于,第一个代码在其内部构建了FXConverter.这导致了现在FXConverter是内部细节的问题,因此不可能用其他任何东西优雅地替换它.除非你知道AppFXConverter的一切.这就是为什么这些现在是"耦合"的原因.依赖项应该传递,而不是在内部构建.

但即使是这也不是真正的意义所在.考虑第二个代码片段,但略微修改了构造函数:

def __init__(self, converter: FXConverter):
    self.converter = converter

所以我现在宣布converterFXConverter.这导致了同样的问题.构造函数的签名不允许传递不同的实现,你还是加上FxConverter,这是一个具体的实现.

在所有这些片段中,App实际上依赖于CurrencyConverter接口/抽象类.这很好.编写依赖于接口和/或具体签名的代码是正常的.编写依赖于具体实现的代码是不好的.

所以这都是关于converter个变量,而不是真正关于.convert('EUR', 'USD', 100)个呼叫.


Example.

一个具体的优势是:假设您想要为App个人编写自动测试.但转换器会做一些繁重的事情,例如,它会进行网络调用以检索当前汇率.你不想在测试中进行这样的调用,这会显著减慢一切,特别是当你想要测试App00个 case 的时候.此外,对于外部资源,这样的测试不一定是确定性的.

但更重要的是:你不会想知道FxConverter到底做了什么.我的意思是,您最终确实关心,但出于测试的目的,您并不关心.您不想考虑它是否在内部调用HTTP.这对App来说无关紧要.App只关心输入/输出,而不关心FxConverter的内部 struct .此外,如果有必要,性能可以在以后进行调整.

那么你能做些什么呢?对于代码段1(甚至使用我修改后的构造函数),您不能做任何事情,除非您了解App的内部 struct ,并知道如何模拟(实际上是to duck type个)内部 struct 以使其工作.但是,您有一个实现的泄漏--内部的每一个微小的变化都可能影响测试.例如,假设App添加了缓存.现在,您必须修改测试.因为模拟,例如,判断.convert()个调用的数量,不幸的是,这是一种非常流行的测试方式.即使这可能只是一个不影响输出的优化.

您不会相信我需要重写数十个测试的频率有多高,因为我对实现做了一点小小的更改,而这对输入/输出没有任何影响.引用库尔茨上校的话:"恐怖……恐怖……"

现在,使用第二个代码片段,您可以编写自己的CurrencyConverter实现.在内存中以确定性的方式进行操作的人.然后使用该实现实例化App.构造函数被声明为接受任何CurrencyConverter个实现,因此它声明可以很好地与您的测试实现一起工作.现在您可以运行测试了,一切都按预期运行.见鬼,如果您不想费心于测试实现,您甚至可以将mock当作CurrencyConverter.关键是,您不再局限于修补.

当然,最终您仍然需要进行集成测试,以确保具体的实现不仅做了它们声称做的事情,而且还能很好地协同工作.然而,无论如何您都需要这样的测试.而且集成测试通常需要花费大量时间.但以解耦的方式组织代码会提高编码性能.例如,您可以一天运行500次快速初始测试,而每周只运行一次集成测试(例如在最终构建期间),并对它们将通过抱有很高的信心.

顺便说一句,对于其他对模仿的支持很差(或根本没有)的语言,这种测试方式也是必要的.就像大多数强类型语言一样.

这就是为什么我根本看不出DIP如何有助于更好地解耦,而我们实际上需要的是遵守静态类型语言中的函数/方法签名?

是的,这正是DIP的意义所在:坚持签名.不是针对具体实现:

Coupled variant:A呼叫B,所以A取决于B.

Decoupled variant:A依赖并调用接口C,B实现C并作为依赖传递给A.A和B都依赖于抽象接口C,但彼此不依赖.

关键是接口什么也做不了,它们只是声明,丰富的签名.具体的实现是有作用的.最好不要把它作为依赖.

最后,一种语言是否为静态类型并不重要.同样的道理也适用于C++(传递抽象类而不是具体类)或Rust(传递特征而不是 struct ).你不会经常看到像C++或Rust这样的低级语言使用DI,因为(运行时)DI实际上并不是免费的.它需要虚拟调用(这通常会扼杀inling等优化),还需要一些簿记.对性能的影响很小,但不是零.有了高级应用程序(比如网络服务器),这就不会引起注意了.但当您使用C++或Rust时,您可能编写了一些需要性能的东西,并且您确实非常关心它,并希望尽可能地从柠檬中榨取一切.这就是那些语言设计的目的.尽管如此,在非性能关键部分使用依赖注入仍然是可以的,而且更可取.

Python相关问答推荐

线性模型PanelOLS和statmodels OLS之间的区别

Pystata:从Python并行运行stata实例

将整组数组拆分为最小值与最大值之和的子数组

如何检测背景有噪的图像中的正方形

当使用keras.utils.Image_dataset_from_directory仅加载测试数据集时,结果不同

可变参数数量的重载类型(args或kwargs)

运行终端命令时出现问题:pip start anonymous"

ODE集成中如何终止solve_ivp的无限运行

如何使用pytest来查看Python中是否存在class attribution属性?

如何在Raspberry Pi上检测USB并使用Python访问它?

pandas:排序多级列

Odoo 16使用NTFS使字段只读

如何防止Pandas将索引标为周期?

python中csv. Dictreader. fieldname的类型是什么?'

如何在Python Pandas中填充外部连接后的列中填充DDL值

应用指定的规则构建数组

解决Geopandas和Altair中的正图和投影问题

如何在Polars中将列表中的新列添加到现有的数据帧中?

在Django REST框架中定义的URL获得404分

为什么在安装了64位Python的64位Windows 10上以32位运行?