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

StackOverflow https://stackoverflow.com/questions/48605

Вопрос

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

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

Это было полезно?

Решение

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

Другие советы

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

  1. Существующие классы можно легко модифицировать для реализации нового интерфейса. Все, что вам нужно сделать, это реализовать интерфейс и добавить необходимые методы.Существующие классы невозможно легко модифицировать для расширения нового абстрактного класса.
  2. Интерфейсы идеально подходят для определения примесей. Интерфейс примеси позволяет классам объявлять дополнительное, необязательное поведение (например, Comparable).Это позволяет совмещать дополнительные функции с основными.Абстрактные классы не могут определять примеси — класс не может наследовать более одного родительского элемента.
  3. Интерфейсы допускают неиерархические структуры. Если у вас есть класс, обладающий функциональностью многих интерфейсов, он может реализовать их все.Без интерфейсов вам пришлось бы создавать раздутую иерархию классов с классом для каждой комбинации атрибутов, что привело бы к комбинаторному взрыву.
  4. Интерфейсы позволяют безопасно расширять функциональные возможности. Вы можете создавать классы-оболочки, используя шаблон «Декоратор» — надежный и гибкий дизайн.Класс-оболочка реализует и содержит тот же интерфейс, перенаправляя некоторые функциональные возможности существующим методам и добавляя специализированное поведение другим методам.Вы не можете сделать это с помощью абстрактных методов — вместо этого вы должны использовать наследование, которое более хрупко.

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

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

  1. Требуется для шаблонов типа GoF, таких как шаблон посетителя.

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

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

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

  5. Широко используется современными фреймворками внедрения зависимостей, такими как http://www.springframework.org/.

  6. В 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.

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

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

Когда Банда из четырех написал:

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

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

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

Почему?

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

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

Вы этого не делаете, и это проблема.

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

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

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

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

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

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

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

Мне действительно нужно то и другое?

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

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

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

Программирование в интерфейс означает уважение «контракта», созданного с помощью этого интерфейса

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

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

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

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

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

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

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

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

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

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

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

  1. Интерфейсы многими рассматриваются как серебряная пуля (эти люди обычно действуют под влиянием «контрактного» заблуждения или думают, что интерфейсы волшебным образом отделяют их код)

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

  3. До появления дженериков и аннотаций (атрибутов в C#) интерфейсы были лучшим способом выполнения некоторых задач.

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

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

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

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

Все эти вещи — запахи кода, ИМХО.

Это один из способов продвижения свободного связь.

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

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

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

Интерфейсы имеют множество преимуществ по сравнению с абстрактными классами:

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

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

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

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

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

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

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

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.

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

public static class RepositoryFactory {

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

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

Фабрика — единственное место, где на класс 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 для получения репозитория, в модульном тесте вы подключаете 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); }
}

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

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

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

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

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

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

public void Drink (кофе, когда -нибудь) {

}

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

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

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

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

public void Drink (ihotdrink onemedrink) {

}

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

Все дело в проектировании перед кодированием.

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

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

Вы можете увидеть это с точки зрения Perl/Python/Ruby:

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top