【问题标题】:What do programmers mean when they say, "Code against an interface, not an object."?当程序员说“针对接口而不是对象的代码”时,他们是什么意思?
【发布时间】:2011-05-26 06:42:50
【问题描述】:

我已经开始了漫长而艰巨的任务,以学习并将 TDD 应用到我的工作流程中。我的印象是 TDD 非常符合 IoC 原则。

在 SO 中浏览了一些 TDD 标记的问题后,我读到针对接口而不是对象进行编程是个好主意。

您能否提供简单的代码示例来说明这是什么,以及如何在实际用例中应用它?简单的例子是我(以及其他想要学习的人)掌握概念的关键。

【问题讨论】:

  • 这是一种面向对象编程的东西,而不是特定于 C# 的东西......
  • @Billy ONeal:可能是,但由于接口在 C#/Java 中的作用不同,我想先学习我最熟悉的语言。
  • @Sergio Boombastic:Interface 编程的概念与 Java 或 C# 中的 interface 无关。事实上,在编写引用这句话的那本书时,Java 和 C# 甚至都不存在。
  • @Jörg:嗯,这与它有一些东西有关。最近的 OOP 语言中的接口肯定是按照引用中的描述来使用的。
  • @Michael Petrotta:不过,他们并不擅长。例如List接口 表示add 将一个元素加入列表后,该元素在列表中,列表长度增加1。在 [interface List](Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add) 中实际上是在哪里说的?

标签: c# .net tdd inversion-of-control


【解决方案1】:

考虑:

class MyClass
{
    //Implementation
    public void Foo() {}
}

class SomethingYouWantToTest
{
    public bool MyMethod(MyClass c)
    {
        //Code you want to test
        c.Foo();
    }
}

因为MyMethod 只接受MyClass,如果您想用模拟对象替换MyClass 以进行单元测试,则不能。更好的是使用接口:

interface IMyClass
{
    void Foo();
}

class MyClass : IMyClass
{
    //Implementation
    public void Foo() {}
}

class SomethingYouWantToTest
{
    public bool MyMethod(IMyClass c)
    {
        //Code you want to test
        c.Foo();
    }
}

现在你可以测试MyMethod,因为它只使用一个接口,而不是一个特定的具体实现。然后,您可以实现该接口以创建您想要用于测试目的的任何类型的模拟或伪造。甚至还有像 Rhino Mocks 的 Rhino.Mocks.MockRepository.StrictMock<T>() 这样的库,它们可以采用任何接口并即时为您构建一个模拟对象。

【讨论】:

  • 旁注:在您使用的语言使用单词的意义上,它并不总是必须成为实际界面。考虑到 Java 或 C# 对继承的限制,它也可以是一个并非完全不合理的抽象类。
  • @Joey:您可以使用抽象类,是的,但是如果您这样做,您必须将类设计为可继承,这可能需要更多的工作。当然,在像 C++ 这样没有语言级接口的语言中,您正是这样做的——创建一个抽象类。请注意,尽管在 Java 和 C# 中使用接口仍然更好,因为您可以从多个接口继承,但只能从一个类继承。允许多个接口继承鼓励您使您的接口更小,这是一件好事 TM :)
  • @Joey:你甚至不必使用 anything。例如,在 Ruby 中,您编写程序所针对的 Interface 通常只用英文记录,如果有的话。不过,这并没有使它不再是一个 Interface。相反,在您的代码中使用 interface 关键字并不意味着它是针对 Interface 进行编程的。思想实验:采用可怕的紧密耦合的非内聚代码。对于每个类,只需复制并粘贴它,删除所有方法主体,将 class 关键字替换为 interface 并将代码中的所有引用更新为该类型。现在的代码更好了吗?
  • Jörg,这是一个很好的看待它的方式,确实:-)
  • 除了模拟之外,隐藏实现细节允许您更改下面的代码以提高性能等,而无需更改调用代码。例如,Java 使用 List 来做到这一点。下面的实现可以是数组、栈等。
【解决方案2】:

这都是亲密关系的问题。如果您对实现(实现的对象)进行编码,那么作为它的使用者,您与“其他”代码的关系非常密切。这意味着您必须知道如何构造它(即,它具有哪些依赖项,可能作为构造函数参数,可能作为设置器),何时处置它,并且您可能没有它就无法做很多事情。

实现对象前面的接口可以让你做一些事情 -

  1. 您可以/应该利用工厂来构造对象的实例。 IOC 容器非常适合您,或者您可以自己制作。由于您的责任之外的建设职责,您的代码可以假设它正在得到它需要的东西。在工厂墙的另一边,你可以构造真实的实例,也可以模拟类的实例。在生产环境中,您当然会使用 real,但对于测试,您可能希望创建存根或动态模拟实例来测试各种系统状态,而无需运行系统。
  2. 您不必知道对象在哪里。这在分布式系统中很有用,在这些系统中,您要与之交谈的对象可能在您的进程甚至系统本地,也可能不在本地。如果您曾经编写过 Java RMI 或 old skool EJB,那么您就会知道“与接口对话”的例程,它隐藏了一个代理,该代理执行您的客户端不必关心的远程网络和编组职责。 WCF 也有类似的“与接口对话”的理念,让系统决定如何与目标对象/服务进行通信。

** 更新 ** 有一个 IOC 容器(工厂)示例的请求。几乎所有平台都有很多,但它们的核心是这样工作的:

  1. 在应用程序启动例程中初始化容器。一些框架通过配置文件或代码或两者兼而有之。

  2. 您“注册”您希望容器为您创建的实现作为它们实现的接口的工厂(例如:为服务接口注册 MyServiceImpl)。在此注册过程中,您通常可以提供一些行为策略,例如是否每次创建新实例或使用单个(吨)实例​​

  3. 当容器为您创建对象时,它会将任何依赖项注入到这些对象中,作为创建过程的一部分(即,如果您的对象依赖于另一个接口,则依次提供该接口的实现,依此类推)。

伪代码可能看起来像这样:

IocContainer container = new IocContainer();

//Register my impl for the Service Interface, with a Singleton policy
container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON);

//Use the container as a factory
Service myService = container.Resolve<Service>();

//Blissfully unaware of the implementation, call the service method.
myService.DoGoodWork();

【讨论】:

  • +1 表示第 1 点。这非常清楚易懂。第 2 点 woosh 超出了我的想象。 :P
  • @Sergio:hossrdude 的意思是,在很多情况下,您的代码永远不知道对象的实际实现者是什么,因为它是由框架或其他库在您的代表,自动。
  • 您能举一个 IoC 容器工厂的基本示例吗?
【解决方案3】:

在针对接口进行编程时,您将编写使用接口实例而不是具体类型的代码。例如,您可以使用以下模式,其中包含构造函数注入。构造函数注入和控制反转的其他部分不需要能够针对接口进行编程,但是由于您来自 TDD 和 IoC 的角度,我已经将其连接起来,以便为您提供一些您希望的上下文熟悉。

public class PersonService
{
    private readonly IPersonRepository repository;

    public PersonService(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public IList<Person> PeopleOverEighteen
    {
        get
        {
            return (from e in repository.Entities where e.Age > 18 select e).ToList();
        }
    }
}

repository对象传入,是一个接口类型。传入接口的好处是能够在不改变用法的情况下“换出”具体实现。

例如,假设 IoC 容器在运行时将注入一个连接到数据库的存储库。在测试期间,您可以传入一个 mock 或 stub 存储库来练习您的 PeopleOverEighteen 方法。

【讨论】:

  • +1 -- 应该注意的是,你没有理由真的需要像 IoC 容器这样的东西来有效地使用接口。
  • 所以基本上,我获得的唯一好处就是能够使用 Mocking 框架?
  • 和可扩展性。可测试性是低耦合、高内聚系统的一个很好的副作用。通过传递接口,您可以消除对实际类的作用的担忧。您不再关心它如何 做到这一点,而只关心它声称它确实 做到了。这会强制分离关注点,并允许您专注于当前工作的特定需求。
  • @Sergio:你需要这样的东西来做任何类型的模拟。不仅仅是框架。 @Michael:我认为“DDD”是“开发人员驱动的开发”……这应该是 TDD 吗?
  • @Billy - 我假设领域驱动开发
【解决方案4】:

这意味着认为通用。不具体。

假设您有一个应用程序通知用户向他发送一些消息。例如,如果您使用接口 IMessage 工作

interface IMessage
{
    public void Send();
}

您可以为每个用户自定义他们接收消息的方式。例如,有人想通过电子邮件收到通知,因此您的 IoC 将创建一个 EmailMessage 具体类。其他人想要 SMS,而您创建 SMSMessage 的一个实例。

在所有这些情况下,通知用户的代码永远不会改变。即使您添加另一个具体类。

【讨论】:

【解决方案5】:

在执行单元测试时针对接口进行编程的一大优势在于,它允许您将一段代码与您想要单独测试或在测试期间模拟的任何依赖项隔离开来。

我之前在这里提到过的一个例子是使用接口来访问配置值。您可以提供一个或多个接口来访问配置值,而不是直接查看 ConfigurationManager。通常你会提供一个从配置文件中读取的实现,但是为了测试你可以使用一个只返回测试值或抛出异常或其他任何东西的实现。

还要考虑您的数据访问层。让您的业务逻辑与特定的数据访问实现紧密耦合,如果没有一个完整的数据库来处理您需要的数据,就很难进行测试。如果您的数据访问隐藏在接口后面,您可以只提供测试所需的数据。

使用接口增加了可用于测试的“表面积”,从而允许进行更细粒度的测试,从而真正测试代码的各个单元。

【讨论】:

    【解决方案6】:

    像阅读文档后会使用它的人一样测试您的代码。不要因为你已经编写或阅读过代码而基于你所拥有的知识来测试任何东西。您希望确保您的代码表现符合预期。

    在最好的情况下,您应该能够将您的测试用作示例,Python 中的 doctest 就是一个很好的示例。

    如果您遵循这些准则,更改实施应该不是问题。

    根据我的经验,测试应用程序的每个“层”也是一种很好的做法。您将拥有原子单元,它们本身没有依赖关系,并且您将拥有依赖于其他单元的单元,直到您最终到达本身就是一个单元的应用程序。

    您应该测试每一层,不要依赖这样一个事实,即通过测试单元 A,您还测试单元 A 所依赖的单元 B(该规则也适用于继承。)这也应该被视为一种实现细节,即使您可能会觉得自己在重复自己。

    请记住,一旦编写的测试不太可能改变,而他们测试的代码几乎肯定会改变。

    在实践中也存在IO和外界的问题,所以要使用接口,以便在需要时可以创建mock。

    在更动态的语言中,这不是什么大问题,在这里您可以使用鸭子类型、多重继承和混合来编写测试用例。如果您开始不喜欢继承,那么您可能做对了。

    【讨论】:

      【解决方案7】:

      This screencast 解释了 c# 实践中的敏捷开发和 TDD。

      通过针对接口进行编码意味着在您的测试中,您可以使用模拟对象而不是真实对象。通过使用一个好的模拟框架,你可以在你的模拟对象中做任何你喜欢的事情。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-05-31
        • 2011-06-22
        • 2011-10-18
        • 1970-01-01
        • 2019-12-13
        • 1970-01-01
        • 2011-06-12
        相关资源
        最近更新 更多