Что имеют в виду программисты, когда говорят: "код против интерфейса, а не объекта."?
Я начал очень долгий и трудный поиск, чтобы узнать и применить TDD для моего рабочего процесса. У меня сложилось впечатление, что TDD очень хорошо вписывается в принципы МОК.
после просмотра некоторых вопросов с тегами TDD здесь, в SO, я прочитал, что это хорошая идея для программирования против интерфейсов, а не объектов.
можете ли вы предоставить простые примеры кода, что это такое, и как применять его в случаях реального использования? Простые примеры являются ключевыми для меня (и других людей, желающих узнайте), чтобы понять концепции.
спасибо.
7 ответов:
считаем:
class MyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(MyClass c) { //Code you want to test c.Foo(); } }
, потому что
MyMethod
принимает только aMyClass
, Если вы хотите заменитьMyClass
С mock-объект для модульного тестирования, вы не можете. Лучше пользоваться интерфейсом: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>()
, которые берут любой интерфейс и строят вам макет объекта на лету.
это все вопрос интимности. Если вы кодируете реализацию (реализованный объект), вы находитесь в довольно близких отношениях с этим "другим" кодом, как его потребитель. Это означает, что вы должны знать, как его построить (т. е. какие зависимости он имеет, возможно, как параметры конструктора, возможно, как сеттеры), когда его утилизировать, и вы, вероятно, не можете многое сделать без него.
интерфейс перед реализованным объектом позволяет сделать несколько вещей -
- для одного вы можете/должны использовать фабрику для создания экземпляров объекта. Контейнеры МОК делают это очень хорошо для вас, или вы можете сделать свой собственный. С обязанностями по строительству вне вашей ответственности, ваш код может просто предположить, что он получает то, что ему нужно. По другую сторону заводской стены можно создать реальные экземпляры или макетные экземпляры класса. В производстве вы, конечно, будете использовать real, но для тестирования вы можете создать stubbed или динамически издевались экземпляры для тестирования различных состояний системы без необходимости запуска системы.
- вы не должны знать, где находится объект. Это полезно в распределенных системах, где объект, с которым вы хотите поговорить, может быть или не быть локальным для вашего процесса или даже системы. Если вы когда-либо программировали Java RMI или old skool EJB, вы знаете процедуру "разговора с интерфейсом", которая скрывала прокси-сервер, который выполнял удаленные сетевые и сортировочные функции, которые ваш клиент не должен был беспокоиться. WCF имеет аналогичную философию "разговора с интерфейсом" и позволяет системе определять, как взаимодействовать с целевым объектом/службой.
** обновление ** Был запрос на пример контейнера МОК (завод). Есть много там для почти всех платформ, но по своей сути они работают так:
вы инициализируете контейнер в подпрограмме запуска приложений. Некоторые фреймворки делают это с помощью конфигурационных файлов или код или обоих.
вы "регистрируете" реализации, которые вы хотите, чтобы контейнер создавался для вас как фабрика для интерфейсов, которые они реализуют (например: register MyServiceImpl для интерфейса службы). Во время этого процесса регистрации обычно существует некоторая поведенческая политика, которую вы можете предоставить, например, если каждый раз создается новый экземпляр или используется один экземпляр(ton)
когда контейнер создает объекты для вас, он вводит любые зависимости в этих объектах как часть процесса создания (т. е., если ваш объект зависит от другого интерфейса, реализация этого интерфейса, в свою очередь, предоставляется и так далее).
псевдо-кодово это может выглядеть так:
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();
при программировании интерфейса, вы будете писать код, который использует экземпляр интерфейса, а не конкретного типа. Например, можно использовать следующий шаблон, который включает в себя инъекцию конструктора. Инъекция конструктора и другие части инверсии управления не требуется, чтобы иметь возможность программировать против интерфейсов, однако, поскольку вы исходите из перспективы 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(); } } }
объект репозитория передается и является интерфейсным типом. Преимуществом передачи в интерфейсе является возможность "подкачки" конкретной реализации без изменения использования.
PeopleOverEighteen метод.
это означает, что думать общий. Не совсем так.
Предположим, у вас есть приложение, которое уведомляет пользователя, отправив ему сообщение. Если вы работаете с помощью интерфейса IMessage например
interface IMessage { public void Send(); }
вы можете настроить для каждого пользователя способ получения сообщения. Например, кто-то хочет получить уведомление по электронной почте, и поэтому ваш МОК создаст конкретный класс EmailMessage. Некоторые другие хотят SMS, и вы создаете экземпляр SMSMessage.
во всех этих случая код для уведомления Пользователя никогда не будет изменен. Даже если вы добавите еще один конкретный класс.
большое преимущество программирования против интерфейсов при выполнении модульного тестирования заключается в том, что оно позволяет изолировать фрагмент кода от любых зависимостей, которые вы хотите протестировать отдельно или имитировать во время тестирования.
пример, который я упоминал здесь раньше, - это использование интерфейса для доступа к значениям конфигурации. Вместо того, чтобы смотреть непосредственно на ConfigurationManager вы можете предоставить один или несколько интерфейсов, которые позволяют получить доступ к значениям конфигурации. Обычно поставьте реализацию, которая читает из файла конфигурации, но для тестирования вы можете использовать тот, который просто возвращает тестовые значения или выдает исключения или что-то еще.
рассмотрим также ваш уровень доступа к данным. Наличие вашей бизнес-логики, тесно связанной с конкретной реализацией доступа к данным, затрудняет тестирование без использования всей базы данных с необходимыми данными. Если ваш доступ к данным скрыт за интерфейсами, вы можете предоставить только те данные, которые вам нужны для тест.
использование интерфейсов увеличивает" площадь поверхности", доступную для тестирования, что позволяет проводить более мелкие тесты, которые действительно проверяют отдельные единицы вашего кода.
этот скринкаст объясняет гибкую разработку и TDD на практике для c#.
кодирование по интерфейсу означает, что в вашем тесте вы можете использовать макет объекта вместо реального объекта. Используя хороший макет фреймворка, вы можете делать в своем макете объекта все, что вам нравится.