![现代C++软件架构:方法与实践](https://wfqqreader-1252317822.image.myqcloud.com/cover/127/48894127/b_48894127.jpg)
1.7.5 依赖倒置原则
依赖倒置原则(DIP)可以用于解耦。本质上,这意味着高级模块不依赖于低级模块,两者都依赖于抽象。
C++允许用两种方法倒置类之间的依赖关系。第一种方法是常规的多态方法,第二种方法是使用模板。我们将看看如何在实践中应用它们。
假设你正在建模一个有前端和后端开发人员的软件开发项目。一种简单的方法是这样写:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/28_03.jpg?sign=1739201626-qPsw5j2cM2NT0MT3Cut2uTxghXHPjXLg-0-462c5c2921f477eb84f9d39949a94042)
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/29_01.jpg?sign=1739201626-GQyPAQAJ1GhenAkAvp1fQQzg7koQUyVD-0-60d9b97d33d877a292f9dd4ad9628c42)
每个开发人员(FrontEndDeveloper和BackEndDeveloper)都是由Project类构造的。然而,这种方法并不理想,因为现在高级概念(Project)依赖于低级概念——单个开发人员模块。我们来看使用多态实现的依赖倒置是如何改变这一点的。我们可以将开发人员定义为依赖如下接口:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/29_02.jpg?sign=1739201626-N4IjpH1UTV0x0ssIOMLSPj7AL4ENpp2q-0-4fe470c36b8c517136c1a05ec99f4e69)
现在,Project类就不再需要知道开发人员(Developer)的实现了。因此,Project必须接受它们作为构造函数的参数:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/29_03.jpg?sign=1739201626-fQqVBX6hYVAOpZDhBNzE2pM8RzExSN6L-0-ff770f2d5fdf37c4b102c9716564b6de)
在这种方法中,Project与具体的实现解耦了,只依赖于名为Developer的多态接口。“较低级别的”具体类也依赖于这个接口。这可以帮助你缩短构造时间,并让单元测试更简单——现在你可以轻松地将模拟(mock)对象作为参数传递到测试代码中。
然而,用虚分派(virtual dispatch)来实现依赖倒置是有代价的,因为我们处理的是内存分配,而动态分派(dynamic dispatch)本身就有开销。有时,C++编译器可以检测到只有一个实现被用于给定的接口,并通过去虚拟化(devirtualization)来消除开销(通常需要将函数标记为final才行)。但是,这里接口使用了两种实现,因此必须付出动态分派的代价(通常是通过虚函数表跳转,虚函数表也称为vtable)。
还有另一种倒置依赖关系的方法,它没有这些缺点。我们来看如何使用可变参数模板(variadic template)、C++14的泛型lambda和C++17或第三方库(如Abseil或Boost)中的变体(variant)来实现这一点。首先是开发人员(FrontEndDeveloper和BackEndDeveloper)类:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/30_01.jpg?sign=1739201626-HIRkvXF0sHXfjJBxAjLVzVYmTdOCjibA-0-f0fa41d43d86b37b8aca2c8694f5f8a1)
现在,我们不再依赖接口了,所以不会进行虚分派。Project类仍然接受一个Developers(FrontEndDeveloper和BackEndDeveloper)的vector:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/30_02.jpg?sign=1739201626-s5Q3AwwroY7QNIDaCn9vWWAwJ05oLpUr-0-fe9566d554919eb6469c622b88984716)
你可能不熟悉variant,它只是一个类,可以接受模板参数传递的任何类型。因为我们使用的是可变参数模板,所以我们可以传递任意多类型。要调用存储在variant中的对象的函数,我们可以使用std::get或std::visit和可调用对象来提取它——在本例中是泛型lambda。它展示了鸭子类型是什么样子的。由于所有的开发人员类都实现了develop函数,所以代码可以进行编译和运行。如果开发人员类有不同的方法,则可以创建一个函数对象,通过重载操作符()来处理不同类型。
因为Project现在是一个模板,所以我们必须在每次创建它时指定类型列表,或者提供一个类型别名。最后,我们可以像这样使用这个类:
![](https://epubservercos.yuewen.com/574F91/28606613407439306/epubprivate/OEBPS/Images/31_01.jpg?sign=1739201626-nDmQlTxWa8S2BeSqOQG1mZxKJ4TQUEJT-0-7edcd66fa07f7e0a492ac1cbab2fef4c)
这种方法保证不会为每个开发人员分配单独的内存或使用虚函数表。但是,在某些情况下,这种方法会导致可扩展性降低,因为一旦声明了variant,就不能向其添加其他类型了。
关于依赖倒置,最后想提一点,有一个名称类似的概念,即依赖注入(dependency injection),我们在示例中使用过这个概念。依赖注入指通过构造函数或设置函数(setter)注入依赖关系,这可能有利于代码的可测试性(例如,考虑注入模拟对象)。甚至有完整的框架用于在整个应用程序中注入依赖关系,比如Boost.DI。这两个概念是相关的,经常一起使用。