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

78 .net c# tdd inversion-of-control

我已经开始了很长时间的艰苦探索,学习并将TDD 应用到我的工作流程中.我认为TDD与IoC原理非常吻合.

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

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

非常感谢.

Bil*_*eal 82

考虑:

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

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

因为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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 旁注:在您使用的语言使用单词时,它并不总是*具有*成为实际界面.考虑到Java或C#对继承的限制,它也可能是一个抽象类,这并不是完全不合理的. (14认同)
  • @Joey:你甚至不必使用*任何东西*.例如,在Ruby中,您编写的*Interface*通常只用英语记录,如果有的话.但是,这并不会使它成为一个*接口*.相反,在你的代码中使用`interface`关键字,并不意味着它是针对*Interface*的编程.思考实验:采取可怕的紧密耦合的不连贯代码.对于每个类,只需复制并粘贴它,删除所有方法体,用`interface`替换`class`关键字,并将代码中的所有引用更新为该类型.代码现在更好吗? (5认同)
  • @Joey:您可以使用抽象类,是的,但是如果您这样做,则必须将该类设计为可继承的,这可能会更多一些工作.当然,在像C++这样没有语言级接口的语言中你就是这样做的 - 制作一个抽象类.请注意,在Java和C#中,使用该接口仍然更好,因为您可以从多个接口继承,但只能从一个类继承.允许多个接口继承鼓励您使接口更小,这是一件好事TM :) (2认同)

hos*_*ude 18

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

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

  1. 对于一个你可以/应该利用工厂来构造对象的实例.IOC容器非常适合您,或者您可以自己制作.由于施工职责不在你的责任范围内,你的代码可以假设它正在满足它的需求.在工厂墙的另一侧,您可以构建实例,也可以模拟类的实例.在生产中,您当然会使用real,但是对于测试,您可能希望创建存根或动态模拟的实例来测试各种系统状态,而无需运行系统.
  2. 您不必知道对象的位置.这在分布式系统中很有用,在这种分布式系统中,您要与之通信的对象可能是也可能不是您的进程甚至系统的本地对象.如果您曾经编写过Java RMI或旧的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();
Run Code Online (Sandbox Code Playgroud)


Mic*_*ins 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

存储库对象是传入的,是一种接口类型.传入接口的好处是能够在不改变用法的情况下"换出"具体实现.

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

  • +1 - 应该注意的是,为了有效地使用接口,你没有理由需要像IoC容器这样的东西. (3认同)