Почему большинство системных архитекторов настаивают на первом программировании интерфейса?

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

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

Ответ 1

Программирование на интерфейс означает соблюдение "контракта", созданного с использованием этого интерфейса. И поэтому, если ваш IPoweredByMotor интерфейс имеет метод start(), будущие классы, реализующие интерфейс, будь то MotorizedWheelChair, Automobile или SmoothieMaker, при реализации методов этого интерфейса добавьте гибкость в вашу систему, потому что одна часть кода может запускать двигатель многих разных типов вещей, потому что все, что нужно знать одному фрагменту кода, это то, что они отвечают на start(). Неважно, как они начинаются, просто чтобы они начали.

Ответ 2

Отличный вопрос. Я отсылаю вас к Джошу Блоху в Эффективной Java, который пишет (пункт 16), почему предпочитают использовать интерфейсы над абстрактными классами. Кстати, если у вас нет этой книги, я очень рекомендую ее! Вот краткое изложение того, что он говорит:

  • Существующие классы могут быть легко модифицированы для реализации нового интерфейса. Все, что вам нужно сделать, это реализовать интерфейс и добавить необходимые методы. Существующие классы не могут быть легко модифицированы, чтобы расширить новый абстрактный класс.
  • Интерфейсы идеально подходят для определения микширования. Интерфейс mix-in позволяет классам объявлять дополнительное, необязательное поведение (например, Comparable). Он позволяет смешивать дополнительные функции с основными функциями. Абстрактные классы не могут определять mix-ins - класс не может распространять более одного родителя.
  • Интерфейсы допускают неиерархические структуры. Если у вас есть класс, который имеет функциональность многих интерфейсов, он может реализовать их все. Без интерфейсов вам нужно будет создать раздутую иерархию классов с классом для каждой комбинации атрибутов, что приведет к комбинаторному взрыву.
  • Интерфейсы обеспечивают безопасные функциональные возможности.. Вы можете создавать классы-оболочки с использованием шаблона Decorator, надежного и гибкого дизайна. Класс-оболочка реализует и содержит один и тот же интерфейс, перенаправляя некоторые функции существующим методам, добавляя особое поведение к другим методам. Вы не можете сделать это с помощью абстрактных методов - вместо этого вы должны использовать наследование, что является более хрупким.

Как насчет преимущества абстрактных классов, обеспечивающих базовую реализацию? Вы можете предоставить абстрактный класс реализации скелета с каждым интерфейсом. Это сочетает достоинства обоих интерфейсов и абстрактных классов. Скелетные реализации обеспечивают помощь в реализации без наложения серьезных ограничений, которые абстрактные классы действуют, когда они служат определениями типов. Например, Collections Framework определяет тип с использованием интерфейсов и обеспечивает реализацию скелета для каждого из них.

Ответ 3

Программирование на интерфейсы дает несколько преимуществ:

  • Требуется для шаблонов типа GoF, например шаблон посетителя

  • Позволяет выполнять альтернативные реализации. Например, реализация нескольких объектов доступа к данным может существовать для одного интерфейса, который абстрагирует используемый механизм базы данных (AccountDaoMySQL и AccountDaoOracle могут реализовывать AccountDao)

  • Класс может реализовывать несколько интерфейсов. Java не допускает множественного наследования конкретных классов.

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

  • В основном используются современные рамки для инъекций зависимостей, такие как http://www.springframework.org/.

  • В Java интерфейсы могут использоваться для создания динамических прокси - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html. Это можно использовать очень эффективно с такими фреймворками, как Spring для выполнения аспектно-ориентированного программирования. Аспекты могут добавлять очень полезные функции в классы без непосредственного добавления Java-кода к этим классам. Примеры этой функции включают ведение журнала, аудит, мониторинг производительности, демаркацию транзакций и т.д. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

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

Ответ 4

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

Когда Gang of Four написал:

Программа для интерфейса не является реализацией.

не было такой вещи, как интерфейс Java или С#. Они говорили об объектно-ориентированной концепции интерфейса, которую каждый класс имеет. Эрих Гамма упоминает об этом в этом интервью.

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

Ответ 5

Как получилось?

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

Откуда вы знаете все отношения между объектами, которые произойдут в этом интерфейсе?

У вас нет, и это проблема.

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

Причины нерасширения абстрактного класса:

  • У вас радикально разные реализации и создание достойного базового класса слишком сложно.
  • Вам нужно сжечь свой единственный базовый класс для чего-то еще.

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

Вопросы, которые вы не задали:

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

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

Нужен ли мне тоже?

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

Рассмотрим классический пример, который LameDuck наследует от Duck. Звучит просто, не так ли?

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

Ответ 6

Программирование на интерфейс означает соблюдение "контракта", созданного используя этот интерфейс

Это самая непонятная вещь о интерфейсах.

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

Эта ошибочная вера настолько широко распространена, что многие люди считают ее общепринятой. Это, однако, неправильно.

Итак, это утверждение в OP

Почти каждая прочитанная Java-книга рассказывает об использовании интерфейса в качестве способа для совместного использования состояния и поведения между объектами

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

Вы можете сделать предположение, что люди будут реализовывать интерфейс, чтобы обеспечить поведение, подразумеваемое именем его методов, но это не похоже на одно и то же. И он не устанавливает никаких ограничений вообще при вызове таких методов (например, что Start следует вызывать перед остановкой).

Это утверждение

Требуется для шаблонов типа GoF, например шаблон посетителя

также неверен. В книге GoF используются ровно нулевые интерфейсы, поскольку они не были характерными для языков, используемых в то время. Ни один из шаблонов не требует интерфейсов, хотя некоторые из них могут их использовать. IMO, шаблон Observer - это тот, в котором интерфейсы могут играть более элегантную роль (хотя шаблон обычно реализуется с использованием событий в настоящее время). В шаблоне Visitor почти всегда используется базовый класс Visitor, реализующий поведение по умолчанию для каждого типа посещенных node, IME.

Лично я думаю, что ответ на вопрос трижды:

  • Интерфейсы воспринимаются многими как серебряная пуля (эти люди обычно работают под "недоразумением" контракта или считают, что интерфейсы магически развязывают их код)

  • Люди Java очень сосредоточены на использовании фреймворков, многие из которых (по праву) требуют, чтобы классы реализовали свои интерфейсы.

  • Интерфейсы были лучшим способом сделать некоторые вещи до того, как были представлены дженерики и аннотации (атрибуты на С#).

Интерфейсы - очень полезная языковая функция, но они сильно злоупотребляют. Симптомы включают:

  • Интерфейс реализуется только одним классом

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

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

Все эти вещи являются запахами кода, IMO.

Ответ 7

Это один из способов повысить свободу coupling.

При низком соединении изменение одного модуля не потребует изменения в реализации другого модуля.

Хорошим использованием этой концепции является Абстрактный шаблон Factory. В примере в Википедии интерфейс GUIFactory создает интерфейс Button. Конкретным Factory может быть WinFactory (создание WinButton) или OSXFactory (производство OSXButton). Представьте, что вы пишете приложение графического интерфейса, и вам нужно просмотреть все экземпляры класса OldButton и изменить их на WinButton. Затем в следующем году вам нужно добавить версию OSXButton.

Ответ 8

По-моему, вы так часто видите это, потому что это очень хорошая практика, которая часто применяется в неправильных ситуациях.

Есть много преимуществ для интерфейсов относительно абстрактных классов:

  • Вы можете переключать реализации без перестройки кода, который зависит от интерфейса. Это полезно для: классов прокси, инъекции зависимостей, АОП и т.д.
  • Вы можете отделить API от реализации в своем коде. Это может быть приятно, потому что это становится очевидным, когда вы меняете код, который будет влиять на другие модули.
  • Это позволяет разработчикам писать код, который зависит от вашего кода, чтобы легко высмеять ваш API для целей тестирования.

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

Ответ 9

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

Ответ 10

Почему расширение является злом. Эта статья в значительной степени является прямым ответом на заданный вопрос. Я не думаю, что практически нет случая, когда вам действительно нужен абстрактный класс, и множество ситуаций, когда это плохая идея. Это не означает, что реализация с использованием абстрактных классов плохая, но вам придется позаботиться о том, чтобы вы не делали контракт на интерфейс зависимым от артефактов некоторой конкретной реализации (пример: класс Stack в Java).

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

Ответ 11

Здесь есть несколько отличных ответов, но если вы ищете конкретную причину, смотрите не дальше, чем Unit Testing.

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

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

В течение всего кода используйте тип IRepository вместо TaxRateRepository.

В репозитории есть непубличный конструктор, который побуждает пользователей (разработчиков) использовать factory для создания репозитория:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

factory - это единственное место, где напрямую ссылается класс TaxRateRepository.

Итак, для этого примера вам нужны вспомогательные классы:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

И есть еще одна другая реализация IRepository - макет:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

Поскольку в реальном времени код (бизнес-класс) использует factory для получения репозитория, в unit test вы подключаете MockRepository для TaxRateRepository. После того, как подстановка сделана, вы можете жестко скопировать возвращаемое значение и сделать базу данных ненужной.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

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

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

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

Ответ 12

В каком-то смысле, я думаю, ваш вопрос сводится к простому: "зачем использовать интерфейсы, а не абстрактные классы?" Технически вы можете добиться ослабленной связи с обоими - базовая реализация все еще не подвергается вызывающему коду, и вы можете использовать шаблон Abstract Factory, чтобы вернуть базовую реализацию (реализацию интерфейса или расширение абстрактного класса), чтобы повысить гибкость вашего дизайна. На самом деле вы можете утверждать, что абстрактные классы дают вам немного больше, поскольку они позволяют вам требовать реализации для удовлетворения вашего кода ( "вы ДОЛЖНЫ реализовать start()" ) и предоставлять стандартные реализации ( "У меня есть стандартная краска() может переопределить, если вы хотите" ) - с интерфейсами, должны быть реализованы реализации, которые со временем могут привести к хрупким проблемам наследования с помощью изменений интерфейса.

В основном, однако, я использую интерфейсы в основном из-за ограничения наследования одиночного Java. Если моя реализация ДОЛЖНА наследовать от абстрактного класса, который будет использоваться при вызове кода, это означает, что я теряю гибкость, чтобы наследовать что-то еще, хотя это может иметь больше смысла (например, для повторного использования кода или иерархии объектов).

Ответ 13

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

общественный свободный напиток (кофе someDrink) {

}

Теперь скажем, что вы хотите использовать тот же самый метод, но передаете объект hotTea. Ну, ты не можешь. Вы просто жестко закодировали этот метод, чтобы использовать только кофейные объекты. Может быть, это хорошо, может быть, так плохо. Недостатком вышесказанного является то, что он строго блокирует вас одним типом объекта, когда вы хотите передать всевозможные связанные объекты.

Используя интерфейс, скажем, IHotDrink,

интерфейс IHotDrink {}

и переработав описанный выше метод, чтобы использовать интерфейс вместо объекта,

публичный пустотный напиток (IHotDrink someDrink) {

}

Теперь вы можете передать все объекты, реализующие интерфейс IHotDrink. Конечно, вы можете написать тот же самый метод, который делает то же самое с другим параметром объекта, но почему? Вы внезапно поддерживаете раздутый код.

Ответ 14

Все о дизайне перед кодированием.

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

Если бы вы нырнули прямо в кодировку и поняли, что на полпути вам не хватает чего-то, что намного сложнее исправить.

Ответ 15

Это можно увидеть с точки зрения perl/python/ruby:

  • когда вы передаете объект в качестве параметра методу, который вы не передаете ему, вы просто знаете, что он должен отвечать на некоторые методы.

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

Ответ 16

Я думаю, что основной причиной использования интерфейсов в Java является ограничение на единичное наследование. Во многих случаях это приводит к ненужным осложнениям и дублированию кода. Взгляните на Черты в Scala: http://www.scala-lang.org/node/126 Черты - это особый вид абстрактных классов, но класс может распространять многие из них.