Що мають на увазі програмісти, коли кажуть: "Код проти інтерфейсу, а не об'єкта"?


79

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

Переглянувши деякі запитання з тегами TDD у SO, я прочитав, що непогано програмувати проти інтерфейсів, а не об’єктів.

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


7
Це щось на кшталт ООП, а не специфічне для C # ...
Billy ONeal

2
@Billy ONeal: Може бути, але оскільки інтерфейси діють по-різному в скажімо C # / Java, я хотів би навчитися на мові, яку я знайомий спочатку.

11
@Sergio Boombastic: концепція програмування інтерфейсу не має нічого спільного з interfaceані на Java, ані на C #. Насправді, коли була написана книга, з якої взята ця цитата, ні Java, ні C # навіть не існувало.
Jörg W Mittag

1
@ Йорг: ну, це має щось спільне. Інтерфейси в останніх мовах ООП, безумовно, призначені для використання, як описано в цитаті.
Michael Petrotta

1
@ Майкл Петротта: Однак вони не дуже добре це роблять. Наприклад, Interface of Listговорить, що після внесення addелемента до списку елемент знаходиться у списку, а довжина списку збільшується на 1. Де насправді сказано, що в [ interface List] ( Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add )?
Jörg W Mittag

Відповіді:


83

Розглянемо:

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

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

Оскільки MyMethodприймає лише a 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

2
@Joey: Ви можете використовувати абстрактний клас, так, але якщо ви це зробите, вам доведеться розробити клас, який буде призначений для успадкування, що може трохи більше попрацювати. Звичайно, в таких мовах, як С ++, які не мають мовного інтерфейсу, ви робите саме це - створюйте абстрактний клас. Зверніть увагу, що в Java та C # все-таки краще використовувати інтерфейс, оскільки ви можете успадковувати з декількох інтерфейсів, але лише одного класу. Дозвіл успадкування декількох інтерфейсів спонукає вас зменшити свої інтерфейси, що добре, що TM :)
Billy ONeal

5
@Joey: вам навіть не потрібно нічого використовувати . Наприклад, у Ruby інтерфейс, щодо якого ви програмуєте, зазвичай документально оформлений лише англійською мовою, якщо взагалі. Однак це не робить його менш інтерфейсом . І навпаки, ляпас interfaceключових слів у всьому коді не означає, що він програмується проти Interface s. Експеримент з думкою: Візьміть жахливий тісно зв'язаний незв’язний код. Для кожного класу просто скопіюйте та вставте його, видаліть усі тіла методів, замініть classключове слово interfaceта оновіть усі посилання в коді до цього типу. Чи є код зараз кращим?
Jörg W Mittag

Йорг, це справді хороший спосіб на це подивитися :-)
Джої,

Крім глузування, приховування деталей реалізації дозволяє вам змінити код знизу, щоб покращити продуктивність тощо, не змінюючи телефонний код. Java робить це з List, як приклад. Реалізація внизу може бути масивом, стеком тощо
Брайан Пан

18

Все це питання близькості. Якщо ви кодуєте реалізацію (реалізований об'єкт), ви знаходитесь у досить близьких стосунках із цим "іншим" кодом, як його споживач. Це означає, що ви повинні знати, як його сконструювати (тобто які залежності він має, можливо, як параметри конструктора, можливо, як сеттери), коли ним розпоряджатися, і ви, мабуть, не можете багато чого зробити без нього.

Інтерфейс перед реалізованим об’єктом дозволяє зробити кілька речей -

  1. Для одного ви можете / повинні використовувати фабрику для побудови екземплярів об'єкта. Контейнери МОК дуже добре роблять це для вас, або ви можете зробити власні. Оскільки будівельні обов'язки поза вашою відповідальністю, ваш код може просто припустити, що він отримує те, що йому потрібно. З іншого боку фабричної стіни ви можете або створити реальні екземпляри, або знущатись над екземплярами класу. У виробництві ви, звичайно, використовували б реальне, але для тестування вам може знадобитися створити приховані або динамічно висміювані екземпляри для тестування різних системних станів без необхідності запускати систему.
  2. Вам не потрібно знати, де знаходиться об’єкт. Це корисно в розподілених системах, де об’єкт, з яким ви хочете поговорити, може бути локальним для вашого процесу чи навіть системи. Якщо ви коли-небудь програмували Java RMI або old 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();

+1 за бал 1. Це було дуже чітко і зрозуміло. Точка 2 промайнула мені над головою. : P

1
@Sergio: Що означає hoserdude, це те, що існує безліч випадків, коли ваш код ніколи не знає, що насправді є реалізатором об'єкта, оскільки він реалізований Framework або якоюсь іншою бібліотекою від вашого імені автоматично.
Billy ONeal

Чи можете ви навести хороший, базовий приклад фабрики контейнерів IoC?
привабливий

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

Отже, в основному, єдиною перевагою, яку я отримую, є можливість використовувати фреймворк знущань?

І розширюваність. Тестованість - це приємний побічний ефект низькозв’язаної, сильно згуртованої системи. Обмінюючи інтерфейси, ви знімаєте занепокоєння тим, що робить фактичний клас. Вас більше не турбує те, як він це робить, а лише те, що він стверджує, що це робить . Це забезпечує розділення проблем та дозволяє зосередитись на конкретних вимогах поточної роботи.
Michael Shimmins

@Sergio: Вам потрібно щось подібне, щоб зробити будь-який глум. Не просто рамки. @Michael: Я думав, що "DDD" - це "Розробка, керована розробником" ... це має бути TDD?
Billy ONeal

1
@Billy - Я припускав розробку доменів
Майкл Шиммінс

3

Це означає думати загальне. Не конкретні.

Припустимо, у вас є програма, яка повідомляє користувача, що надсилає йому якесь повідомлення. Наприклад, якщо ви працюєте з використанням інтерфейсу IMessage

interface IMessage
{
    public void Send();
}

Ви можете налаштувати для кожного користувача спосіб отримання повідомлення. Наприклад, хтось хоче отримати повідомлення за допомогою електронної пошти, і тому ваш IoC створить конкретний клас EmailMessage. Деякі інші хочуть SMS, а ви створюєте екземпляр SMSMessage.

У всіх цих випадках код сповіщення користувача ніколи не буде змінено. Навіть якщо ви додасте ще один конкретний клас.


@Billy: "O" частина [SOLID] ( en.wikipedia.org/wiki/Solid_(object-oriented_design) , дякую!
Лоренцо,

StackExchange зламав ваше посилання :( en.wikipedia.org/wiki/Solid_%28object-oriented_design%29
Billy ONeal

@Billy: ой ... він з’їв останню дужку. Цей повинен бути робочим
Лоренцо


2

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

Прикладом, про який я вже згадував тут десь, є використання інтерфейсу для доступу до значень конфігурації. Замість того, щоб дивитись безпосередньо на ConfigurationManager, ви можете надати один або кілька інтерфейсів, що дозволяють отримати доступ до значень конфігурації. Зазвичай ви пропонуєте реалізацію, яка читається з конфігураційного файлу, але для тестування ви можете використовувати таку, яка просто повертає тестові значення або видає винятки або щось інше.

Враховуйте також рівень доступу до даних. Завдяки тісній взаємодії вашої бізнес-логіки з конкретною реалізацією доступу до даних важко тестувати, не маючи цілої бази даних під рукою з необхідними даними. Якщо ваш доступ до даних прихований за інтерфейсами, ви можете надати лише ті дані, які потрібні для тесту.

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


2

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

У найкращому випадку ви повинні мати змогу використовувати свої тести як приклади, а тести на Python - хороший приклад для цього.

Якщо ви дотримуєтесь цих вказівок, зміна впровадження не повинна бути проблемою.

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

Вам слід протестувати кожен шар, не покладайтесь на той факт, що тестуючи блок A, ви також тестуєте блок B, від якого залежить блок A (правило стосується також успадкування.) Це теж слід трактувати як деталь реалізації, навіть хоча ви можете почуватися так, ніби повторюєтеся.

Майте на увазі, що колись письмові тести навряд чи зміняться, тоді як код, який вони тестують, зміниться майже напевно.

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

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


1

Цей скринкаст пояснює гнучку розробку та TDD на практиці для c #.

Кодування проти інтерфейсу означає, що у вашому тесті ви можете використовувати макетний об'єкт замість реального об'єкта. Використовуючи хороший макетний фреймворк, ви можете робити у своєму макетному об’єкті все, що вам подобається.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.