понедельник, 8 декабря 2014 г.

Для чего нужны интерфейсы?

В чем сила интерфейсов? Сила интерфейсов в правде слабости системы типов!

Принято считать, что интерфейсы предназначены для моделирования абстракций и обеспечения слабой связанности (loose coupling). Звучит умно, но что это значит?

Что дают интерфейсы?

Наследование моделирует семейство объектов с общим поведением. Интерфейс и абстрактный класс определяет «протокол» этого семейства, а наследники определяют конкретную реализацию. Разные виды предусловий можно спрятать за интерфейсом IPrecondition, разные виды стратегий сортировки – за ISorter, разные виды импортеров/экспортеров – за Importer/Exporter.

Наличие интерфейсов/абстрактных классов позволяет думать о задаче более абстрактно, не вдаваясь в подробности, какая конкретная разновидность используется. Абстрактность интерфейсов позволяет сосредоточиться на главном, игнорируя второстепенные детали. Отсюда вытекает распространенное убеждение, что интерфейс моделируют абстракцию.

Существуют разные уровни абстрагирования или игнорирования деталей. Домашний кондиционер является разновидностью «устройства управления климатом», но конкретный кондиционер, установленный лично в вашем доме тоже является абстракцией – он прячет от вас свои детали реализации.

Конкретный класс «абстрактен» - его поведение определяется его открытым интерфейсом, а детали реализации скрыты. Будет ли класс реализовывать интерфейс или нет, определяется не требованием «абстрактности», а моделью. Если в вашем «мире» нужно смотреть на конкретный экземпляр кондиционера через призму охладительным приборов, то интерфейс нужен, в противном случае – нет.

Когда класс должен реализовывать интерфейс?

1. Класс реализует стратегию или является частью семейства объектов: IRepository, IFormatter, IPrecondition.

2. Класс реализует ролевой интерфейс (следствие ISP): ICloneable, IComparable etc.

3. Класс реализует интерфейс, требуемый для связи с другими классами. Класс является адаптером; необходимость интерфейса обусловлено принципом DIP.

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

Когда класс должен принимать интерфейс в качестве зависимости?

Аргументы класса говорят о том, что на данном уровне нельзя принять некоторое решение самостоятельно, и класс перекладывает его на вызывающий код. Наличие аргументов – это компромисс между простотой и гибкостью. Класс без аргументов/зависимостей прост в использовании, но у разных клиентов могут быть разные потребности, которые решаются «конфигурированием» класса с помощью аргументов.

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

Причины, когда класс должен требовать интерфейсы в аргументах конструктора:

  • Класс работает с семейством типов. «Семейство типов» уже существует и определяется требованиями текущей модели, а не воображением разработчика.
  • Следствие DIP. Класс хочет общаться с объектом другого уровня. Он сам определяет интерфейс и требует его реализацию. Можно рассматривать, как особую форму паттерна Наблюдателя.
  • Для обеспечения тестируемости. Полезно, только если реализация «абстракции» завязана на внешнее окружение. Даже в этом случае можно обойтись Шаблонным Методом: выделить общение с внешним ресурсом не во внешнюю зависимость, а в виртуальный метод и переопределить его в тесте.

Выделять интерфейсы «на всякий случай» - не нужно! Если это библиотечный код, то интерфейсы там полное зло: любое их изменение – ломает всех клиентов. А если это продакшн код, то он все равно будет меняться, и наличие лишних интерфейсов лишь сделает этот процесс более сложным.

12 комментариев:

  1. С ICloneable не очень хороший пример, больно уж спорный интерфейс, не нужно его в суе поминать.

    ОтветитьУдалить
    Ответы
    1. Да, со стандартным ICloneable беда страшная. Но я тут ссылаюсь на гипотетически правильный интерфейс, который четко определяет семантику клонирования:)

      Удалить
  2. Не знаю, возможно ты во утверждениях под словом "интерфейс" предполагаешь "interface/abstract class" и тогда всё звучит очень даже ожидаемо и всё что я напишу дальше можешь не читать. Но еже ли речь именно о interface, то для меня странно, например, говорить о семействе типов, которые задаются интерфейсом - для меня семейство наследуется от абстрактного класса, а интерфейсом могут быть объединены различные семейства со схожим аспектом поведения. Т.е. я иду со стороны различий применения интерфейсов и абстактных классов и поэтому некоторые примеры вызывают протест с устоявшимися представлениями.

    ОтветитьУдалить
    Ответы
    1. В большинстве случаев различия между интерфейсами и абстрактными классами не существенны. Но в основном везде, где я говорил об интерфейсе, поняите .NET интерфейса было применимым.

      На самом деле, интерфейсы точно также моделирует семейство типов, что и базовый класс. Вот, в С++ или в Eiffel есть множественное наследование и там интерфейсов нет вообще.

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

      З.Ы. Протесты в студию:))

      Удалить
    2. Сергей, а когда лучше использовать интерфейс, а когда абстрактный класс? Если не брать в расчет множественное наследование.

      Удалить
    3. http://sergeyteplyakov.blogspot.com/2014/12/interfaces-vs-abstract-classes.html

      Удалить
    4. Сергей, привет. Вообще-то пытался ответить на твой комментарий о множественном наследовании но почему-то там кнопочки "Ответить" не было. Возможно степень вложенности комментариев ограничена? Или просто я чего-то напутал.

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

      ответ 1 (вступление) и

      ответ 2 (основная мысль)

      Удалить
    5. Игорь, спасибо.

      Кстати, получается интересная картина: с одной стороны, пост должен быть коротким, поскольку в блоге полотна по 10-15 страниц читать не удобно. С другой стороны, краткость не для всех сестра таланта, поэтому на 2-х страницах все тонкости обсудить сложно:))

      Это я к тому, что я специально хотел уменьшить объем букв, кому-то это больше по душе, кому-то меньше:) Так что неточности и неоднозначности в обсуждении будут:)

      Удалить
  3. Я бы еще добавил один кейс, когда можно использовать интерфейсы.
    Множественного наследования в .NET нету, зато можно использовать связку интерфейс + экстеншн. Это дает возможность эмулировать множественное наследование или писать миксины, что очень удобно. Реализовал классом интерфейс (пусть даже пустой) и получил все экстеншны к нему в "подарок".

    ОтветитьУдалить
    Ответы
    1. extensions в бизес логике мне кажутся плохой архитектурой. а) нарушается принцип grasp информационный эксперт б) как результат логика размазана и при моделировании новых сущностей неизвестно какие экстеншены уже реализованы чтобы повторно их использовать в) появляются жесткие и неочевидные связи (видны только в секции using). как результат появляется новый вид связей в добавок к наследованию и агрегированию - экстеншены. тестируемость тоже под вопросом. вообще есть же рекомендация избегать любых статических зависимостей. у нас на проекте сейчас используется такой подход и это ужасно..

      Удалить
    2. а) Почему нарушается информационный эксперт? Екстенш пишется к конкретному интерфейсу, наделяя его неким поведением. Естественно бизнес-логику так писать не стоит, но это удобный метод написания миксинов. Весь LINQ так написан и выглядит очень даже ничего. Главное - не злоупотреблять. В Java есть дефолтная реализация интерфейса, в Scala - trait, в С# екстеншны. Мне в этом плане больше нравится Scala, но С# позволяет удобно реализовывать подобные вещи
      б) в) наследование - более жесткий тип связи и не всегда уместно, особенно если вы любитель более функционального подхода, а статич. завимимость не всегда зло.

      Удалить
  4. По поводу отличий в C#.
    Абстрактный класс может сделать абстрактным виртуальный член своего родителя.
    http://www.sql.ru/forum/1130812/abstract-override?hl=

    С помощью же интерфейсов в c#, как было сказано выше, можно реализовать функционал примесей, яркий пример тому LINQ.
    Интерфейс может быть маркером, см. INamingContainet.
    Ну и следует упомянуть о ко/контрвариантности, которая доступна в интерфейсах и делегатах, но не в абстрактных классах.

    ОтветитьУдалить