当程序员说“代码对接口而不是对象”时,他们的意思是什么?

时间:2010-12-16 00:39:47

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

我已经开始了长时间艰苦的学习和应用 TDD到我的工作流程的任务。我认为TDD与IoC原则非常吻合。

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

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

非常感谢。

7 个答案:

答案 0 :(得分:81)

考虑:

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>()这样的库,可以使用任何界面并在运行中为你构建一个模拟对象。

答案 1 :(得分:18)

这完全是亲密的问题。如果你编写一个实现(一个已实现的对象),你就会与那个“其他”代码保持非常亲密的关系,作为它的消费者。这意味着你必须知道如何构造它(即它有什么依赖关系,可能是构造函数params,可能是setter),什么时候处理它,如果没有它你可能做不了多少。

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

  1. 您可以/应该利用工厂来构建对象的实例。 IOC容器非常适合您,或者您可以自己制作。由于施工职责不在你的责任范围内,你的代码可以假设它正在满足它的需求。在工厂墙的另一侧,您可以构建实例,也可以模拟类的实例。在生产中,您当然会使用real,但是对于测试,您可能希望创建存根或动态模拟的实例来测试各种系统状态,而无需运行系统。
  2. 您不必知道对象的位置。这在分布式系统中很有用,在这种分布式系统中,您要与之通信的对象可能是也可能不是您的进程甚至系统的本地对象。如果您曾经编写过Java RMI或旧的skool EJB,那么您就知道“与界面交谈”的例程,该例程隐藏了代理,该代理完成了客户端不必关心的远程网络和编组任务。 WCF有一个类似的“与界面交谈”的理念,让系统决定如何与目标对象/服务进行通信。
  3. **更新** 有人要求提供IOC容器(工厂)的示例。几乎所有平台都有很多,但它们的核心是这样的:

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

    2. 您“注册”您希望容器为您创建的实现(例如:为Service接口注册MyServiceImpl)。在此注册过程中,通常会提供一些行为策略,例如每次创建新实例或使用单个(吨)实例​​时

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

    4. 伪编码地看起来像这样:

      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();
      

答案 2 :(得分:9)

对接口编程时,您将编写使用接口实例的代码,而不是具体类型。例如,您可以使用以下模式,其中包含构造函数注入。构造函数注入和控制反转的其他部分并不需要能够针对接口进行编程,但是由于您是从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();
        }
    }
}

传入存储库对象并且是接口类型。传递界面的好处是能够换掉&#39;具体实现而不改变用法。

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

答案 3 :(得分:3)

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

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

interface IMessage
{
    public void Send();
}

您可以按用户自定义接收邮件的方式。例如,有人希望通过电子邮件通知您,因此您的IoC将创建一个EmailMessage具体类。其他人想要短信,你创建一个SMSMessage实例。

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

答案 4 :(得分:2)

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

我之前在某处提到的一个例子是使用接口来访问配置值。您可以提供一个或多个允许您访问配置值的接口,而不是直接查看ConfigurationManager。通常,您将提供从配置文件读取的实现,但是为了测试,您可以使用仅返回测试值或抛出异常或其他内容的实现。

还要考虑您的数据访问层。将业务逻辑与特定的数据访问实现紧密结合使得很难在没有整个数据库的情况下测试您需要的数据。如果您的数据访问隐藏在接口后面,则只能提供测试所需的数据。

使用接口增加了可用于测试的“表面区域”,允许进行更精细的测试,这些测试确实可以测试代码的各个单元。

答案 5 :(得分:2)

测试您的代码,就像在阅读文档后会使用它的人一样。不要根据您的知识测试任何东西,因为您已经编写或阅读了代码。您希望确保代码按预期行为

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

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

另外根据我的经验,测试应用程序的每个“层”都是一种好习惯。你将拥有原子单位,它本身没有依赖关系,你将拥有依赖于其他单位的单位,直到你最终到达本身就是一个单位的应用程序。

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

请记住,一旦书面测试不太可能改变,而他们测试的代码几乎肯定会发生变化。

在实践中还存在IO和外部世界的问题,因此您希望使用接口以便在必要时创建模拟。

在更动态的语言中,这不是一个很大的问题,在这里你可以使用duck typing,multiple inheritance和mixins来组成测试用例。如果您开始不喜欢继承,那么您可能正确地做到了。

答案 6 :(得分:1)

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

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