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

发布于 2024-10-07 16:19:46 字数 206 浏览 0 评论 0原文

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

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

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

I've started the very long and arduous quest to learn and apply TDD to my workflow. I'm under the impression that TDD fits in very well with IoC principles.

After browsing some of TDD tagged questions here in SO, I read it's a good idea to program against interfaces, not objects.

Can you provide simple code examples of what this is, and how to apply it in real use cases? Simple examples is key for me (and other people wanting to learn) to grasp the concepts.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

鹿港巷口少年归 2024-10-14 16:19:46

考虑一下:

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() 这样的库,它可以接受任何接口并动态构建模拟对象。

Consider:

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

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

Because MyMethod accepts only a MyClass, if you want to replace MyClass with a mock object in order to unit test, you can't. Better is to use an interface:

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

Now you can test MyMethod, because it uses only an interface, not a particular concrete implementation. Then you can implement that interface to create any kind of mock or fake that you want for test purposes. There are even libraries like Rhino Mocks' Rhino.Mocks.MockRepository.StrictMock<T>(), which take any interface and build you a mock object on the fly.

只是在用心讲痛 2024-10-14 16:19:46

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

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

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

** 更新 **
有人请求提供 IOC 容器(工厂)示例。有许多适用于几乎所有平台的容器,但它们的核心工作原理如下:

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

  2. 您“注册”您希望容器为您创建的实现作为它们实现的接口的工厂(例如:为 Service 接口注册 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();

It's all a matter of intimacy. If you code to an implementation (a realized object) you are in a pretty intimate relationship with that "other" code, as a consumer of it. It means you have to know how to construct it (ie, what dependencies it has, possibly as constructor params, possibly as setters), when to dispose of it, and you probably can't do much without it.

An interface in front of the realized object lets you do a few things -

  1. For one you can/should leverage a factory to construct instances of the object. IOC containers do this very well for you, or you can make your own. With construction duties outside of your responsibility, your code can just assume it is getting what it needs. On the other side of the factory wall, you can either construct real instances, or mock instances of the class. In production you would use real of course, but for testing, you may want to create stubbed or dynamically mocked instances to test various system states without having to run the system.
  2. You don't have to know where the object is. This is useful in distributed systems where the object you want to talk to may or may not be local to your process or even system. If you ever programmed Java RMI or old skool EJB you know the routine of "talking to the interface" that was hiding a proxy that did the remote networking and marshalling duties that your client didn't have to care about. WCF has a similar philosophy of "talk to the interface" and let the system determine how to communicate with the target object/service.

** UPDATE **
There was a request for an example of an IOC Container (Factory). There are many out there for pretty much all platforms, but at their core they work like this:

  1. You initialize the container on your applications startup routine. Some frameworks do this via config files or code or both.

  2. You "Register" the implementations that you want the container to create for you as a factory for the interfaces they implement (eg: register MyServiceImpl for the Service interface). During this registration process there is typically some behavioral policy you can provide such as if a new instance is created each time or a single(ton) instance is used

  3. When the container creates objects for you, it injects any dependencies into those objects as part of the creation process (ie, if your object depends on another interface, an implementation of that interface is in turn provided and so on).

Pseudo-codishly it could look like this:

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();
情何以堪。 2024-10-14 16:19:46

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

传入的存储库对象是一个接口类型。传入接口的好处是能够“交换”具体实现而不改变用法。

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

When programming against an interface you will write code that uses an instance of an interface, not a concrete type. For instance you might use the following pattern, which incorporates constructor injection. Constructor injection and other parts of inversion of control aren't required to be able to program against interfaces, however since you're coming from the TDD and IoC perspective I've wired it up this way to give you some context you're hopefully familiar with.

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

The repository object is passed in and is an interface type. The benefit of passing in an interface is the ability to 'swap out' the concrete implementation without changing the usage.

For instance one would assume that at runtime the IoC container will inject a repository that is wired to hit the database. During testing time, you can pass in a mock or stub repository to exercise your PeopleOverEighteen method.

靖瑶 2024-10-14 16:19:46

这意味着通用思维。不具体。

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

interface IMessage
{
    public void Send();
}

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

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

It means think generic. Not specific.

Suppose you have an application that notify the user sending him some message. If you work using an interface IMessage for example

interface IMessage
{
    public void Send();
}

you can customize, per user, the way they receive the message. For example somebody want to be notified wih an Email and so your IoC will create an EmailMessage concrete class. Some other wants SMS, and you create an instance of SMSMessage.

In all these case the code for notifying the user will never be changed. Even if you add another concrete class.

森罗 2024-10-14 16:19:46

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

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

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

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

The big advantage of programming against interfaces when performing unit testing is that it allows you to isolate a piece of code from any dependencies you want to test separately or simulate during the testing.

An example I've mentioned here before somewhere is the use of an interface to access configuration values. Rather than looking directly at ConfigurationManager you can provide one or more interfaces that let you access config values. Normally you would supply an implementation that reads from the config file but for testing you can use one that just returns test values or throws exceptions or whatever.

Consider also your data access layer. Having your business logic tightly coupled to a particular data access implementation makes it hard to test without having a whole database handy with the data you need. If your data access is hidden behind interfaces you can supply just the data you need for the test.

Using interfaces increases the "surface area" available for testing allowing for finer grained tests that really do test individual units of your code.

野稚 2024-10-14 16:19:46

像阅读文档后使用代码的人一样测试您的代码。不要根据您编写或阅读代码所掌握的知识来测试任何内容。您希望确保您的代码行为符合预期。

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

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

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

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

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

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

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

Test your code like someone who would use it after reading the documentation. Do not test anything based on knowledge you have because you have written or read the code. You want to make sure that your code behaves as expected.

In the best case you should be able to use your tests as examples, doctests in Python are a good example for this.

If you follow these guidelines changing the implementation shouldn't be an issue.

Also in my experience it is good practice to test each "layer" of your application. You will have atomic units, which in itself have no dependencies and you will have units which depend on other units until you eventually get to the application which in itself is a unit.

You should test each layer, do not rely on the fact that by testing unit A you also test unit B which unit A depends on (the rule applies to inheritance as well.) This, too, should be treated as an implementation detail, even though you might feel as if you are repeating yourself.

Keep in mind that once written tests are unlikely to change while the code they test will change almost definitely.

In practice there is also the problem of IO and the outside world, so you want to use interfaces so that you can create mocks if necessary.

In more dynamic languages this is not that much of an issue, here you can use duck typing, multiple inheritance and mixins to compose test cases. If you start disliking inheritance in general you are probably doing it right.

流年里的时光 2024-10-14 16:19:46

此截屏视频解释了 C# 实践中的敏捷开发和 TDD。

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

This screencast explains agile development and TDD in practice for c#.

By coding against an interface means that in your test, you can use a mock object instead of the real object. By using a good mock framework, you can do in your mock object whatever you like.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文