Что означают программисты, когда говорят: "Код против интерфейса, а не объект"?

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

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

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

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

Ответ 1

Рассмотрим:

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

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

Поскольку MyMethod принимает только MyClass, если вы хотите заменить MyClass макетным объектом на unit test, вы не сможете. Лучше использовать интерфейс:

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

Ответ 2

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

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

  • Для этого вы можете/должны использовать factory для создания экземпляров объекта. Контейнеры IOC делают это очень хорошо для вас, или вы можете сделать свой собственный. Со строительными обязанностями за пределами вашей ответственности ваш код может просто предположить, что он получает то, что ему нужно. С другой стороны стены factory вы можете либо построить реальные экземпляры, либо макет экземпляров класса. В производстве вы, конечно, будете использовать реальное, но для тестирования вы можете захотеть создать закодированные или динамически высмеиваемые экземпляры для проверки различных состояний системы без необходимости запуска системы.
  • Вам не обязательно знать, где находится объект. Это полезно в распределенных системах, где объект, с которым вы хотите поговорить, может быть или не быть локальным для вашего процесса или даже системы. Если вы когда-либо программировали Java RMI или старый EJB, вы знаете рутину "разговаривать с интерфейсом", которая скрывала прокси-сервер, который выполнял удаленные сетевые и сортировочные обязанности, которые не нужно заботиться о вашем клиенте. WCF имеет аналогичную философию "говорить с интерфейсом" и позволяет системе определять, как связаться с целевым объектом/службой.

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

  • Вы инициализируете контейнер в своей программе запуска приложений. Некоторые структуры делают это через конфигурационные файлы или код или оба.

  • Вы "Зарегистрируете" реализации, которые вы хотите создать для вас в качестве factory для интерфейсов, которые они реализуют (например: зарегистрируйте MyServiceImpl для интерфейса службы). Во время этого процесса регистрации обычно существует некоторая политика поведения, которую вы можете предоставить, например, если каждый экземпляр создается каждый раз или используется один (тон) экземпляр

  • Когда контейнер создает для вас объекты, он вводит любые зависимости в эти объекты как часть процесса создания (т.е. если ваш объект зависит от другого интерфейса, реализация этого интерфейса в свою очередь предоставляется и так далее).

Pseudo-codishly может выглядеть так:

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

Ответ 3

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

Ответ 4

Это значит думать родовым. Не определен.

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

interface IMessage
{
    public void Send();
}

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

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

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

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

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

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

Ответ 7

Этот скринкаст объясняет гибкое развитие и TDD на практике для С#.

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