Почему большинство системных архитекторов настаивают на первом программировании интерфейса? - PullRequest
25 голосов
/ 07 сентября 2008

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

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

Ответы [ 16 ]

29 голосов
/ 07 сентября 2008

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

23 голосов
/ 08 сентября 2008

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

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

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

20 голосов
/ 07 сентября 2008

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

  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. Макетные реализации, модульное тестирование. Когда зависимые классы являются реализациями интерфейсов, могут быть написаны фиктивные классы, которые также реализуют эти интерфейсы. Ложные классы могут использоваться для облегчения юнит-тестирования.

19 голосов
/ 07 сентября 2008

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

Когда Банда Четырех пишет:

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

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

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

7 голосов
/ 09 сентября 2008

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

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

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

Нет, и это проблема.

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

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

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

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

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

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

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

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

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

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

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

4 голосов
/ 15 декабря 2008

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3 голосов
/ 09 сентября 2008

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

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

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

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

3 голосов
/ 07 сентября 2008

Это один из способов продвижения слабой связи .

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

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

2 голосов
/ 07 сентября 2008

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

1 голос
/ 22 января 2009

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

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

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 и кодирования старой школы.

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