依赖倒置原则是否意味着我必须为每个模块创建一个接口?

Does dependency inversion principle mean that I have to create an interface for every module?

如果我希望我的代码遵循 SOLID 原则,特别是依赖倒置原则,这是否意味着我必须为每个模块创建一个接口(抽象),即使它只有一个实现?

在我看来,根据这些帖子:

http://josdejong.com/blog/2015/01/06/code-reuse/

http://blog.ploeh.dk/2010/12/02/Interfacesarenotabstractions/

为每个模块创建一个 "abstraction" 是一种代码混乱并且违反了 YAGNI 原则。

我的经验法则是:不要使用依赖注入,或者为模块创建接口,除非它有多个实现(第二个实现可以是模拟 class 用于单元测试,以防万一database/server/file 个模块)。

有人可以帮我解决这个问题吗? SOLID 是否意味着我必须注入每个模块并将其抽象化?如果是,那不是我们大多数时候根本不会使用的很多杂物吗?

依赖倒置原则指出:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

换句话说,每个依赖的模块(所以这就是应用程序中除了入口点模块之外的所有模块)都应该被抽象化。否则高层模块将不得不直接依赖低层模块,导致违反 DIP。

抽象的实现数量与 DIP 无关,因为它的目标是使模块能够抵抗变化。如果没有抽象,就不可能轻松更改实现或添加横切关注点而无需更改或重新编译高级组件。

但是,如果您发现自己仅用一个实现定义了许多抽象,那么您就违反了 Reused Abstraction Principle,正如 Mark Seemann 在您所引用的文章中所述:

Having only one implementation of a given interface is a code smell.

不过,这并不是说您根本不应该定义接口,而是您需要仔细检查您的设计并找出与行为相关的 类。那些相关的 类 通常可以放在相同的通用抽象(通用接口)后面,这不仅允许抽象重用,而且使应用横切关注点变得简单。

以下是您可以置于同一通用抽象背后的一些功能建议:

  • ICommandHandler<TCommand> for 类 代表用户对系统进行更改(用例)。
  • IQueryHandler<TQuery, TResult> 作为 类 的抽象,查询数据库(或文件系统、Web 服务,等等)和 return 数据。
  • IValidator<T> for 类 将检查报告验证错误返回给用户
  • ISecurityValidator<T> for 类 验证用户是否被允许执行某个操作。
  • IAuthorizationFilter<T> for 类 允许根据用户的权限和角色应用基于行的安全性。
  • IEventHandler<T> for 类 响应已发生的特定业务事件。

这些只是抽象的几个例子。这在很大程度上取决于您将获得哪些通用抽象的应用程序和设计。

我编写的应用程序使用了这些通用抽象,而这些应用程序只有几个接口和一个实现。系统中大约 90% 到 98% 的模块实现了这些通用抽象之一(取决于应用程序的大小;应用程序越大,百分比越高)。

这些通用抽象使得在 DI 库中的一行代码中注册所有实现变得非常容易(或者至少,如果您使用的是 .NET),但更重要的是,正如我之前所说,应用交叉减少顾虑变得非常容易。例如,无需对您的应用程序进行全面更改,您可以 运行 在数据库事务中使用案例,或应用死锁重试机制。或者您可以应用查询缓存,而无需对整个应用程序进行全面更改。