Что имеют в виду программисты, когда говорят: «Код против интерфейса, а не объекта»? - PullRequest
78 голосов
/ 16 декабря 2010

Я начал очень длинный и трудный квест для изучения и применил TDD к своему рабочему процессу. У меня сложилось впечатление, что TDD очень хорошо вписывается в принципы IoC.

После просмотра некоторых вопросов с тегами TDD здесь, в SO, я прочитал, что это хорошая идея - программировать против интерфейсов, а не объектов.

Можете ли вы предоставить простые примеры кода того, что это такое, и как применять его в реальных случаях использования? Простые примеры являются ключевыми для меня (и других людей, желающих учиться), чтобы понять концепции.

Большое спасибо.

Ответы [ 7 ]

81 голосов
/ 16 декабря 2010

Рассмотрим:

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>(), которые берут любой интерфейс и создают вам фиктивный объект на лету.

18 голосов
/ 16 декабря 2010

Все дело в близости.Если вы кодируете реализацию (реализованный объект), вы находитесь в довольно тесных отношениях с этим «другим» кодом, как его потребителем.Это означает, что вы должны знать, как его конструировать (то есть, какие у него зависимости, возможно, в качестве параметров конструктора, возможно, в качестве установщиков), когда его утилизировать, и вы, вероятно, не сможете многое сделать без него.

Интерфейс перед реализованным объектом позволяет вам делать несколько вещей:

  1. Например, вы можете / должны использовать фабрику для создания экземпляров объекта.Контейнеры IOC делают это очень хорошо для вас, или вы можете сделать свой собственный.Поскольку обязанности по построению находятся вне вашей ответственности, ваш код может просто предполагать, что он получает то, что ему нужно.На другой стороне фабричной стены вы можете создавать реальные экземпляры или макетировать экземпляры класса.В производственной среде вы, конечно, будете использовать реальный, но для тестирования вам может потребоваться создать заглушенные или динамически смоделированные экземпляры для тестирования различных состояний системы без необходимости запуска системы.
  2. Вам не нужно знать, где находитсяобъект есть.Это полезно в распределенных системах, где объект, с которым вы хотите общаться, может быть или не быть локальным для вашего процесса или даже системы.Если вы когда-либо программировали Java RMI или старый skool EJB, вы знаете процедуру «общения с интерфейсом», которая скрывала прокси-сервер, выполнявший функции удаленной сети и сортировки, о которых ваш клиент не заботился.WCF придерживается схожей философии «общаться с интерфейсом» и позволяет системе определять способ связи с целевым объектом / службой.

** ОБНОВЛЕНИЕ ** Был запрос на примерКонтейнер МОК (Фабрика).Существует множество подходов для почти всех платформ, но по своей сути они работают следующим образом:

  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();
9 голосов
/ 16 декабря 2010

При программировании на интерфейсе вы будете писать код, который использует экземпляр интерфейса, а не конкретный тип. Например, вы можете использовать следующий шаблон, который включает внедрение конструктора. Внедрение в конструктор и другие части инверсии управления не обязательны для программирования на интерфейсах, однако, поскольку вы исходите из перспективы 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 голосов
/ 16 декабря 2010

Это означает, что мыслить общим. Не указано.

Предположим, у вас есть приложение, которое уведомляет пользователя, отправляющего ему какое-либо сообщение. Если вы работаете с использованием интерфейса IMessage, например

interface IMessage
{
    public void Send();
}

Вы можете настроить для каждого пользователя способ получения сообщения. Например, кто-то хочет получить уведомление по электронной почте, и ваш IoC создаст конкретный класс EmailMessage. Некоторые хотят SMS, и вы создаете экземпляр SMSMessage.

Во всех этих случаях код уведомления пользователя никогда не изменится. Даже если вы добавите еще один конкретный класс.

2 голосов
/ 26 декабря 2010

Протестируйте свой код, как тот, кто будет его использовать после прочтения документации. Не проверяйте ничего, основываясь на своих знаниях, потому что вы написали или прочитали код. Вы хотите убедиться, что ваш код ведет себя как ожидалось.

В лучшем случае вы сможете использовать свои тесты в качестве примеров. Док-тесты в Python являются хорошим примером для этого.

Если вы следуете этим рекомендациям, изменение реализации не должно быть проблемой.

Также, по моему опыту, рекомендуется проверять каждый «слой» вашего приложения. У вас будут атомарные юниты, которые сами по себе не имеют зависимостей, и у вас будут юниты, которые зависят от других юнитов, пока вы в конечном итоге не доберетесь до приложения, которое само по себе является юнитом.

Вам следует тестировать каждый слой, не полагайтесь на тот факт, что, тестируя блок A, вы также тестируете блок B, от которого зависит блок A (правило также распространяется и на наследование.) Это также следует рассматривать как реализацию детали, даже если вы чувствуете, что повторяетесь.

Имейте в виду, что однажды написанные тесты вряд ли изменятся, а код, который они тестируют, изменится почти наверняка.

На практике также существует проблема ввода-вывода и внешнего мира, поэтому вы хотите использовать интерфейсы, чтобы при необходимости создавать макеты.

В более динамичных языках это не такая уж большая проблема, здесь вы можете использовать утиную типизацию, множественное наследование и миксины для составления тестовых случаев. Если вы вообще не любите наследование, вы, вероятно, делаете это правильно.

2 голосов
/ 16 декабря 2010

Большим преимуществом программирования против интерфейсов при выполнении модульного тестирования является то, что он позволяет вам изолировать кусок кода от любых зависимостей, которые вы хотите тестировать отдельно или моделировать во время тестирования.

упомянутое здесь ранее где-то использование интерфейса для доступа к значениям конфигурации.Вместо того, чтобы смотреть прямо на ConfigurationManager, вы можете предоставить один или несколько интерфейсов, которые позволят вам получить доступ к значениям конфигурации.Обычно вы предоставляете реализацию, которая читает из файла конфигурации, но для тестирования вы можете использовать такую, которая просто возвращает тестовые значения или генерирует исключения или что-то в этом роде.

Рассмотрите также ваш уровень доступа к данным.Тесная связь вашей бизнес-логики с конкретной реализацией доступа к данным затрудняет тестирование, не имея под рукой всей базы данных с нужными вам данными.Если ваш доступ к данным скрыт за интерфейсами, вы можете предоставить только те данные, которые необходимы для теста.

Использование интерфейсов увеличивает «площадь поверхности», доступную для тестирования, что позволяет проводить более детальные тесты, которые действительно проверяют отдельные блоки вашегокод.

1 голос
/ 16 декабря 2010

Эта экранная передача объясняет гибкую разработку и TDD на практике для c #.

Кодирование по интерфейсу означает, что в вашем тесте вы можете использовать фиктивный объект вместо реального объекта.Используя хороший макет фреймворка, вы можете делать в своем макете объект все что угодно.

...