依赖倒置原则是 SOLID 原则的一部分,并且是促进更高级别算法的可测试性和重用的重要原则。
背景:正如鲍勃叔叔的网页所指出的,依赖倒置是关于依赖于抽象,而不是依赖于具体。
实际上,发生的情况是,您的类直接实例化另一个类的某些地方需要更改,以便调用者可以指定内部类的实现。
例如,如果我有一个 Model 类,我不应该硬编码它以使用特定的数据库类。如果我这样做,我不能使用 Model 类来使用不同的数据库实现。如果您有不同的数据库提供程序,这可能很有用,或者您可能想用假数据库替换数据库提供程序以进行测试。
模型不会在 Database 类上执行“新”操作,而是简单地使用 Database 类实现的 IDatabase 接口。模型从不引用具体的数据库类。但是谁实例化了 Database 类呢?一种解决方案是构造函数注入(依赖注入的一部分)。对于此示例,模型类被赋予了一个新的构造函数,该构造函数接受一个要使用的 IDatabase 实例,而不是实例化一个实例本身。
这解决了Model不再引用具体Database类,通过IDatabase抽象来使用数据库的问题。但它引入了问题中提到的问题,即它违反了Demeter法则。也就是说,在这种情况下,Model 的调用者现在必须知道 IDatabase,而以前它不知道。该模型现在向其客户公开其如何完成工作的一些细节。
即使你对此没意见,还有一个问题似乎让很多人感到困惑,包括一些培训师。假设任何时候一个类,例如 Model,具体实例化另一个类,那么它就违反了依赖倒置原则,因此它是坏的。但在实践中,您不能遵循这些硬性规定。有时您需要使用具体的类。例如,如果你要抛出一个异常,你必须“更新它”(例如 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 原则——不要仅仅因为你认为你可能需要它就添加抽象。如果您正在练习测试优先的开发,那么正确的抽象就会变得很明显,并且只有实现了足够的抽象才能通过测试。