2

我一直在阅读有关 DI 和最佳实践的信息,但仍然没有找到这个问题的答案。什么时候应该使用接口?

  1. 一些开发人员建议为每个被注入的对象添加接口。这将构成一个模块化应用程序。
  2. 其他一些人反对这一点。

所以我的问题是哪一个是正确的?

编辑:

下面是两个方面,我还是没有看到使用接口的好处。在这两种情况下,我都可以轻松地模拟类并更改实现

使用接口

bind(IUserStorage.class).to(UserStorage.class);
// Unit test
bind(IUserStorage.class).to(Mock(UserStorage.class));

不使用接口

bind(UserStorage.class).to(UserStorage.class);
// Unit test
bind(UserStorage.class).to(Mock(UserStorage.class));
4

4 回答 4

4

我不敢相信使用接口违反 OOP 原则!

在这种情况下,我肯定会使用接口。这意味着您将组件松散地耦合在一起,并且可以轻松地模拟和/或替代替代方案。许多 DI 框架将使用这些接口来提供附加功能(例如,创建映射到真实对象的代理对象,但具有附加功能)。

因此,除了最琐碎的注入对象外,我会尝试使用所有接口。在某个阶段,您将想要利用可替代性、框架代码生成等,而改进接口使用是一个额外的痛苦,在项目开始时很容易避免。

于 2012-12-17T17:36:53.300 回答
3

基于接口的设计是 IoC 的基石,这里是基于接口设计的简短描述(对不起,我引用了我自己的博客,但我刚刚完成了一篇关于此的文章,这是我的 MS 论文的摘录):

南迪甘等人。将基于接口的设计定义为“一种开发面向对象系统的方法,在这种方法中,人们在设计中尽可能有意识地、主动地定义和使用接口,以获得使用接口设计的好处”[Nan09]。基于接口设计的应用程序遵循“程序到接口,而不是实现”的原则。这一原则为最终的系统 [Dav03] 带来了以下好处:灵活性(描述了系统对更改的鲁棒性)、可扩展性(系统可以轻松地容纳添加)和可插入性(允许替换具有相同接口的对象的能力)运行)。

将界面设计与IoC混合后,您将获得以下好处:

  1. 任务与实现分离
  2. 增加模块性,其中模块仅依赖于其合同(接口)的其他模块。
  3. 增加可插拔性并且更换模块不会对其他模块产生级联影响。

为了回答你的问题,我会为不同类型的模块使用接口。例如,每个服务或存储库一个。

我不为控制器或模型类(MVC 应用程序)创建接口。

作为副作用,所有这些都有助于测试

于 2012-12-17T17:50:37.320 回答
2

如果您使用接口或至少使用抽象/可继承类,您可以通过在 DI/IoC 配置中轻松交换实现(注入另一个类)来更改程序的行为。 使用接口是一种很好的做法(恕我直言)。如果您正在编写需要模拟的单元测试,这一点尤其重要。如果您不使用接口,那么编写具有良好覆盖率的 UnitTest(并不是说在大多数“现实世界”情况下是不可能的)要困难得多。

如果注入的部分有可能发生变化,我认为您应该使用接口。扩展您的实现应该很容易,请参阅Open-Closed-Principle。=> 这将需要交换模块/部件/实现...问问自己如果您的类没有要覆盖的虚函数并且您被迫更改实现会发生什么。

至少会为你的代码的公共类/部分(其他程序员会使用的部分)使用接口。

看看你的样品。问题出在接线部分,而不仅仅是将类绑定为接口的(默认)实现(绑定有效,但接线可能会中断)。

例如,如果您有 2 个实现(此处的 C# 示例,在 Java 等中也应该相同):

public interface IUserStorage
{
  void Write(object something);
}

public class UserStorageTextFile : IUserStorage
{
   public void Write(object something) { ... }; // stores to text file
}

public class UserStorageDB : IUserStorage
{
   public void Write(object something) { ... }; // stores to DB
}

public class MyStorageClient
{
   public MyStorageClient(IUserStorage storage) { ... } // copy to private field and use it etc.
}

根据您的 IoC,将 MyStorageClient 的实例连接到您的 IUserStorage 绑定应该很容易。

bind(IUserStorage.class).to(UserStorageDB.class); // Java sample, eh?

但是,如果您的 MyStorageClient 已经被迫使用数据库...

public class MyStorageClient
{
   public MyStorageClient(UserStorageDB storage) { ... } // copy to private field and use it etc.
}

...除了 UserStorageTextFile 是从 UserStorageDB 继承的之外,不可能将它与 UserStorageTextFile 类连接起来......但是如果您只想编写一个简单的文本文件,为什么要依赖于例如 Oracle 驱动程序(UserStorageDB 需要) ?

我认为该示例足够清晰,并显示了使用接口的好处...

但如果不是...尝试这样做:

bind(UserStorageDB.class).to(UserStorageTextFile.class);

// and in another config/module/unitTest
bind(UserStorageTextFile.class).to(Mock(UserStorageDB.class));

// and try to wire it against your client class, too (both ways, meaning one config for TextFile and load a config for the DB after changing only the configuration)
于 2012-12-17T17:42:21.897 回答
-1

你的问题是“一些开发者[支持这个]”和“一些开发者[反对这个]”,所以没有正确的答案。但这就是为什么我同意接口被过度使用的原因

如果您正在创建库,选择何时使用接口很重要。当您无法控制代码的使用方式时,创建可维护的合约就更难了。

但是,如果您正在创建一个应用程序,则它不太可能需要接口,因为类的公共接口可以作为使用代码的可维护合同。假设版本 1 如下所示:

public class UserStorage
{
    public void Store(User user) { /* ... */ }
}

您甚至不需要重构工具将其更改为:

public interface UserStorage
{
    public void Store(User user);
}

class TheImplementation implements IUserStorage
{
    public void Store(User user) { /* ... */ }
}

然后,您可以轻松地使用重构工具将接口重命名为IUserStorage.

因此,当您编写非库代码时,通常可以使用类,直到您需要可交换的实现、装饰器等。当类的公共接口不适合您的需要时,您应该使用接口。(例如,见接口隔离原则

简而言之 - 在应用程序代码中拥有一个与类 1:1 的接口是不必要的间接。

于 2012-12-17T19:28:42.903 回答