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

What do programmers mean when they say, “Code against an interface, not an object.”?

我已经开始了一个非常漫长和艰巨的任务,学习和应用TDD到我的工作流程。我的印象是TDD非常符合国际奥委会的原则。

在浏览了这里的一些带有TDD标签的问题之后,我读到了一个好主意,就是针对接口而不是对象进行编程。

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

非常感谢。


考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass
{
    //Implementation
    public void Foo() {}
}

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

由于MyMethod只接受MyClass,如果想用一个模拟对象替换MyClass进行单元测试,就不能了,最好是使用一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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(),它们可以获取任何接口,并动态地为您构建一个模拟对象。


这都是一个亲密的问题。如果您编码到一个实现(一个已实现的对象),那么作为"其他"代码的使用者,您与它有着非常密切的关系。这意味着您必须知道如何构造它(例如,它有哪些依赖项,可能作为构造函数参数,也可能作为setter),何时处理它,如果没有它,您可能做不了很多事情。

在已实现对象前面的接口允许您做一些事情-

  • 对于其中一个,您可以/应该利用工厂来构造对象的实例。IOC容器可以很好地为您服务,或者您可以自己制作。由于构建职责超出了您的职责范围,您的代码可以假设它得到了它需要的东西。在工厂墙的另一侧,您可以构建实际实例,也可以模拟类的实例。当然,在生产环境中,您将使用real,但对于测试,您可能希望创建存根实例或动态模拟实例来测试各种系统状态,而不必运行系统。
  • 你不必知道物体在哪里。这在分布式系统中很有用,在分布式系统中,您希望与之交谈的对象可能是或不是流程甚至系统的本地对象。如果你曾经编写过Java RMI或者旧的SKOOL EJB,你就知道了"与接口交谈"的例行公事,它隐藏了一个代理,该代理完成了你的客户不必关心的远程网络和编组任务。WCF有一个类似的"与接口对话"的理念,让系统决定如何与目标对象/服务通信。
  • **更新**有人要求提供一个IOC集装箱(工厂)的例子。几乎所有的平台都有很多,但在它们的核心,它们是这样工作的:

  • 在应用程序启动例程上初始化容器。有些框架通过配置文件或代码或两者来实现这一点。

  • 您可以将希望容器为您创建的实现"注册"为它们实现的接口的工厂(例如:为服务接口注册myServiceImpl)。在此注册过程中,通常可以提供一些行为策略,例如每次创建一个新实例或使用一个(吨)实例。

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

  • 伪代码可能如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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();


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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();
            }
        }
    }

    存储库对象是传入的,并且是接口类型。传入接口的好处是能够"交换"具体的实现,而不改变用法。

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


    它的意思是一般思维。不具体。

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

    1
    2
    3
    4
    interface IMessage
    {
        public void Send();
    }

    您可以自定义每个用户接收消息的方式。例如,某人希望通过电子邮件得到通知,因此您的IOC将创建一个电子邮件消息具体类。另一些人需要短消息,而您创建了一个smsmmessage实例。

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


    像阅读文档后使用代码的人一样测试代码。不要基于你所掌握的知识来测试任何东西,因为你已经编写或阅读了代码。您希望确保代码的行为符合预期。

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

    如果您遵循这些指导原则,那么改变实现不应该是一个问题。

    根据我的经验,测试应用程序的每个"层"也是一个很好的实践。您将拥有原子单元,它本身没有依赖性,并且您将拥有依赖于其他单元的单元,直到您最终到达应用程序,它本身就是一个单元。

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

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

    实际上,还有IO和外部世界的问题,所以您希望使用接口,以便在必要时创建模拟。

    在更动态的语言中,这并不是什么大问题,这里您可以使用duck类型、多重继承和混合来组成测试用例。如果你开始不喜欢继承,你可能做得对。


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

    我之前在这里提到过的一个例子是使用接口访问配置值。您可以提供一个或多个接口来访问配置值,而不是直接查看ConfigurationManager。通常情况下,您会提供一个从配置文件读取的实现,但是对于测试,您可以使用一个只返回测试值或抛出异常的实现。

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

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


    这个屏幕广播解释了敏捷开发和实践中的TDD。

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