我对接口的理解有误吗?

6

我正在维护一个ASP.NET MVC项目。在这个项目中,原始开发人员拥有大量的接口,例如:IOrderServiceIPaymentServiceIEmailServiceIResourceService。让我感到困惑的是,每个接口都只被一个类实现。换句话说:

OrderService : IOrderService
PaymentService : IPaymentService

我一直认为接口被用于创建一个架构,使得组件之间可以轻松地进行替换。就像这样:

Square : IShape
Circle : IShape

此外,我不明白这些是如何被创建和使用的。以下是OrderService示例代码:
public class OrderService : IOrderService
{
    private readonly ICommunicationService _communicationService;
    private readonly ILogger _logger;
    private readonly IRepository<Product> _productRepository;

    public OrderService(ICommunicationService communicationService, ILogger logger,
        IRepository<Product> productRepository)
    {
        _communicationService = communicationService;
        _logger = logger;
        _productRepository = productRepository;
    }
}

这些对象似乎从未直接被创建,例如OrderService orderService = new OrderService(),它总是使用接口。我不明白为什么要使用接口而不是实现接口的类,或者这是如何工作的。我是否遗漏了一些关于接口的重要信息?


2
也许最初的想法是使用接口,以便在单元测试时可以用简单的东西替换业务对象(但从未实现)? - Ilia G
2
如果一个接口只被一个类实现,那么似乎有点过度设计了。也许这是为一个从未到来的未来而设计的? - Polyfun
你所说的“让我困惑的是每个接口只有一个实现类”是指这一句话“public class OrderService : IOrderService”吗?如果是,那么在项目中是否有很多地方的构造函数都需要像Ilogger或其他接口作为参数? - Chris W
@ChrisW 是的,在项目的任何地方都会这样做。 - Nick
那么他似乎正在做一件非常正常的事情,使用IoC容器进行构造函数注入。如果您检查global.asax文件,您可能会找到容器的设置代码和绑定代码。 - Chris W
5个回答

4
这种设计模式通常用于方便单元测试,因为现在可以使用 TestOrderService 替换 OrderService,两者都被引用为 IOrderService。这意味着您可以编写 TestOrderService 来为要测试的类提供特定的行为,然后判断该类是否执行了正确的操作。
在实践中,以上通常通过使用模拟框架来完成,这样您就不必手动编码 TestOrderService,而是使用更简洁的语法描述其在典型测试中应该如何运作,然后让模拟框架动态地为您生成一个实现。
至于为什么在代码中从未看到“new OrderService”,可能是因为您的项目正在使用某种形式的控制反转容器,它促进了自动依赖注入。换句话说,您不必直接构建 OrderService,因为在某个地方您已经配置了任何对 IOrderService 的使用都应自动通过构造单例 OrderService 并将其传递给构造函数来满足。这里有很多微妙之处,我不确定您的依赖项注入是如何实现的(它不必是自动的;您也可以手动构造实例并通过构造函数将其传递进去)。

它特别适用于那些太贪心而不愿购买适当的单元测试框架的人 - 即大多数情况下是一个可以模拟非接口或非虚方法的适当模拟服务。一旦你得到了一个适当的模拟服务,你就可以编写既安全又设计良好的类以及良好的单元测试。同时还可以模拟静态方法调用和构造函数。 - TomTom
@TomTom,仅通过明确定义的接口访问服务有何不安全之处? - Dan Bryant
仅使用单元测试和模拟框架时,方法往往需要是虚拟的,而正常的面向对象编程实践则要求类是sealed的。因此,人们不得不编写糟糕的类来支持模拟框架,或者在不需要接口的情况下实现接口。我有大量内部IOC,并且看不到必须要有一个接口才能对另一个类进行单元测试的必要性,我可以传递该类并在需要时模拟SEALED类。 - TomTom
举个例子,创建一个 SEALED 的 OrderService,我还是可以进行模拟。好吧,我甚至可以模拟静态的 "Instance" 方法,用我的模拟实例来替换返回值 ;) - TomTom
所以,看起来它正在使用Windsor进行IoC(现在我需要阅读一些内容)。但是对于接口和实现它们的类之间的1-1关系,我应该继续这种方式吗?这是我将揭示的IoC方法的一部分,还是主要用于单元测试? - Nick
显示剩余3条评论

1

这不是接口的唯一用途,在MVC中它们被用于从实现中解耦契约。 要了解MVC,您需要阅读有关相关主题的一些内容,例如关注点分离和控制反转(IoC)。创建一个要传递给OrderService构造函数的对象的实际操作由IoC容器处理,基于某些预定义的映射。


1
这些对象似乎从未直接创建,如OrderService orderService = new OrderService()。
那又怎样?
重点是有人调用了OrderService构造函数,而调用者负责创建它们。他将它们交出去。
我不明白为什么要使用接口而不是实现接口的类。
因为你不想知道类 - 它可能会改变,是外部的,可以使用IOC容器进行配置,程序员决定甚至不需要一个共同的基类。对于使用的实用程序类,您做出的假设越少,就越好。
我的谷歌技能没有发现任何重大问题吗?
没有,但一本关于面向对象编程的好书比随机的谷歌片段更有帮助。这基本上属于架构领域和.NET基础知识(对于第一部分)。

2
考虑到上下文,我认为这里滥用了接口,可以归类为YAGNI。每次,原作者不是简单地创建一个类,而是创建了一个接口,然后在一个类中实现它。如果你问我,那有点臃肿:大多数类可能永远不会受到另一个实现的影响。 - CodeCaster
我不太确定,看代码片段只有services、loggers和reps有接口 - 这意味着(对我来说)正在使用一个IoC容器。用户关于在这个项目中使用接口的问题(Square: IShape部分)再次暗示该项目只使用接口作为可注入组件。但是,这里有很多“暗示”,如果不查看项目,就无法更好地了解正在发生的事情。 - Chris W

1

编程时最好针对接口而不是对象进行编程。 这个 问题提供了很好的理由,但一些原因包括允许实现更改(例如用于测试)。

仅因为当前只有一个类实现了接口并不意味着它将来不能更改。

此外,我不明白这些是如何被创建和使用的。

这被称为依赖注入,基本上意味着该类不需要知道如何或从哪里实例化其依赖项,其他人将处理它。


0

这些是服务接口,它们封装了某种外部性。通常情况下,在您的主项目中只有一个实现,但您的测试使用简单的实现,不依赖于那些外部内容。

例如,如果您的支付服务联系PayPal来验证付款,您不希望在与其无关的代码测试中执行此操作。相反,您可以将它们替换为始终返回“付款成功”的简单实现,并检查订单流程是否正常进行,以及另一个返回“付款失败”的实现,并检查订单流程是否失败。

为避免依赖于实现,您不会自己创建实例,而是在构造函数中接受它们。然后创建您的类的IoC容器将填充它们。查找控制反转

您的项目可能具有一些代码,在其启动代码中设置IoC容器。该代码包含有关要创建哪个类以获取特定接口实现的信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接