程序员说“针对接口而不是对象的代码”是什么意思?


78

我已经开始了漫长而艰巨的学习和追求 应用TDD到我的工作流程。我觉得TDD非常符合IoC原则。

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

您能否提供简单的代码示例,以及如何在实际用例中应用它?简单的示例对我(以及其他想学习的人)来说是理解概念的关键。


7
这是一种OOP,而不是C#特定的东西……
Billy ONeal,2010年

2
@Billy ONeal:可以,但是由于接口在C#/ Java中的作用不同,所以我想先学习我最熟悉的语言。

11
@Sergio Boombastic:对接口进行编程的概念与interfaceJava或C#无关。实际上,当编写引用该书的书时,甚至Java和C#都不存在。
约尔格W¯¯米塔格

1
@约尔格:好,它有什么用它做。当然,要使用最新的OOP语言中的接口,如引用中所述。
Michael Petrotta 2010年

1
@迈克尔·彼得罗塔:虽然他们不是很擅长。例如,接口List说,后add荷兰国际集团的元素列表,所述元素是在列表中,由清单的长度增加1。它在[ interface List](Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add)中实际在哪里说呢?
约尔格W¯¯米塔格

Answers:


82

考虑:

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>(),它们都可以采用任何接口,并为您动态构建一个模拟对象。


14
旁注:在您使用的语言使用该单词的意义上,它不一定总是必须是实际的界面。考虑到Java或C#对继承的限制,它也可能不是完全不合理的抽象类。
乔伊(Joey)2010年

2
@Joey:可以使用抽象类,是的,但是如果这样做,则必须将类设计为可继承的,这可能需要更多工作。当然,在没有语言级接口的C ++之类的语言中,您正是这样做的-创建一个抽象类。请注意,尽管在Java和C#中,最好还是使用该接口,因为您可以从多个接口继承,但只能继承一个类。允许多个接口继承会鼓励您减小接口,这是一件好事TM :)
Billy ONeal 2010年

5
@Joey:您甚至不必使用任何东西。例如,在Ruby中,您所编程的接口通常仅以英文记录(如果有的话)。但是,这并没有减少它的接口。相反,interface在整个代码中使用关键字并不意味着它是针对Interface进行编程的。思想实验:采用可怕的紧密耦合的内聚代码。对于每个类,只需将其复制并粘贴,删除所有方法主体,将替换为class关键字interface并将代码中的所有引用更新为该类型。现在的代码更好了吗?
约尔格W¯¯米塔格

约尔格(Jörg),这确实是一种很好的观察方法,:-)
Joey 2010年

除了模拟之外,隐藏实现细节还允许您更改下面的代码以提高性能等,而无需更改调用代码。例如,Java使用List来做到这一点。实施下方可以是数组,栈,等等
布赖恩泛

18

这全都是亲密关系。如果您对实现(已实现的对象)进行编码,那么您将与该“其他”代码有密切的关系。这意味着您必须知道如何构造它(即,它具有什么依赖关系,可能作为构造函数参数,可能作为setter),何时处置它,没有它,您可能无法做很多事情。

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

  1. 对于一个您可以/应该利用工厂来构造对象的实例。IOC容器可以为您做得很好,或者您可以自己制造。承担超出您职责范围的施工职责后,您的代码可以仅假定它正在获取所需的内容。在工厂墙壁的另一侧,您可以构造类的真实实例或模拟实例。在生产中,您当然会使用real,但是对于测试,您可能需要创建存根或动态模拟的实例来测试各种系统状态,而不必运行系统。
  2. 您不必知道对象在哪里。这在分布式系统中非常有用,在分布式系统中,您要与之交谈的对象可能位于流程甚至系统本地,也可能不在本地。如果您曾经编程过Java RMI或旧的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使头晕目眩。:P

1
@Sergio:hoserdude的意思是,在很多情况下,您的代码永远都不知道对象的实际实现者是什么,因为它是由Framework或由您代表的其他某些库自动实现的。
Billy ONeal 2010年

您能举一个很好的,基本的IoC容器工厂示例吗?
令人鼓舞的2011年

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

存储库对象被传入,并且是接口类型。传递接口的好处是能够“交换”具体实现而无需更改用法。

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


3
+1-应该注意的是,没有任何理由真正需要IoC容器之类的东西来有效地使用接口。
Billy ONeal 2010年

因此,基本上,我获得的唯一好处就是能够使用Mocking框架?

和可扩展性。可测试性是低耦合,高凝聚力系统的一个不错的副作用。通过传递接口,您无需担心实际类的作用。您不再关心它是如何做到的,而只是担心它声称它确实做到了。这样可以将关注点分离开,并使您可以专注于当前工作的特定要求。
Michael Shimmins 2010年

@Sergio:您需要这样的东西才能进行任何模拟。不只是框架。@Michael:我以为“ DDD”是“开发人员驱动的开发”……应该是TDD吗?
Billy ONeal 2010年

1
@Billy-我正在假设域驱动开发
Michael Shimmins 2010年

3

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

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

interface IMessage
{
    public void Send();
}

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

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


@Billy:[SOLID](en.wikipedia.org/wiki/Solid_ (object-directional_design)的“ O”部分,谢谢!
Lorenzo 2010年


@Billy:糟糕,它吃了最后一个括号。这应该是工作
洛伦佐(Lorenzo)2010年


2

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

我之前在这里提到的一个示例是使用接口访问配置值。您可以提供一个或多个接口来访问配置值,而不是直接查看ConfigurationManager。通常,您会提供一种从配置文件中读取的实现,但是对于测试,您可以使用仅返回测试值或引发异常或其他任何行为的实现。

还请考虑您的数据访问层。将您的业务逻辑紧密耦合到特定的数据访问实现上,很难在没有整个数据库方便地处理所需数据的情况下进行测试。如果您的数据访问隐藏在接口后面,则可以仅提供测试所需的数据。

使用界面会增加可用于测试的“表面积”,从而可以进行更细粒度的测试,这些测试确实可以测试代码的各个单元。


2

阅读文档后,像测试使用它的人一样测试您的代码。请勿根据已掌握的知识来进行任何测试,因为您已经编写或阅读了代码。您要确保代码行为正常符合预期。

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

如果您遵循这些准则,则更改实现不成问题。

同样,根据我的经验,最好是测试应用程序的每个“层”。您将拥有原子单位,而原子单位本身没有依赖关系,并且您将拥有依赖于其他单位的单位,直到最终到达本身就是单位的应用程序为止。

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

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

实际上,还有IO和外界的问题,因此您需要使用接口,以便在必要时可以创建模拟。

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


1

该截屏视频解释了c#在实践中的敏捷开发和TDD。

通过针对接口进行编码意味着在测试中,您可以使用模拟对象而不是真实对象。通过使用良好的模拟框架,您可以在模拟对象中完成任何操作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.