Хороший чехол для интерфейсов

Я работаю в компании, где некоторым требуется обоснование использования интерфейса в нашем коде (Visual Studio C# 3.5).

Я хотел бы попросить обоснование Iron Clad, для которого необходимы интерфейсы. (Моя цель - ДОКАЗАТЬ, что интерфейсы являются нормальной частью программирования.)

Мне не нужно убеждать, мне просто нужен хороший аргумент, чтобы убедить других.

Тип аргумента, который я ищу, основан на фактах, а не на сравнении (т. Е. "Потому что библиотека.NET использует их" основан на сравнении).

Аргумент против них таков: если класс правильно настроен (с его открытыми и закрытыми членами), то интерфейс - это просто дополнительные издержки, потому что те, которые используют класс, ограничены открытыми членами. Если вам нужен интерфейс, который реализуется более чем одним классом, просто настройте наследование / полиморфизм.

17 ответов

Решение

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

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

Определение поведения, которое может применяться ко многим различным классам, что позволяет использовать их взаимозаменяемо, даже если между классами нет никакой связи (кроме определенного поведения). Например, класс Horse и Bicycle могут иметь метод Ride. Вы можете определить интерфейс IRideable, который определяет поведение Ride, и любой класс, который использует это поведение, может использовать объект Horse или Bicycle, не вызывая неестественное наследование между ними.

Аргумент против них таков: если класс правильно настроен (с его открытыми и закрытыми членами), то интерфейс - это просто дополнительные издержки, потому что те, которые используют класс, ограничены открытыми членами. Если вам нужен интерфейс, который реализуется более чем одним классом, просто настройте наследование / полиморфизм.

Рассмотрим следующий код:

interface ICrushable
{
  void Crush();
}

public class Vehicle
{
}

public class Animal
{
}

public class Car : Vehicle, ICrushable
{
  public void Crush()
  {
     Console.WriteLine( "Crrrrrassssh" );
  }
}

public class Gorilla : Animal, ICrushable
{
  public void Crush()
  {
     Console.WriteLine( "Sqqqquuuuish" );
  }
}

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

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

Увы как кто то сказал

В технологиях доминируют два типа людей: те, кто понимает, что им не удается, и те, кто управляет тем, что они не понимают.

Чтобы включить модульное тестирование класса.

Для эффективного отслеживания зависимостей (если интерфейс не извлечен и не затронут, возможно, изменилась только семантика класса).

Потому что нет времени выполнения.

Чтобы включить внедрение зависимости.

... и, возможно, потому что это чертовски 2009 год, а не 70-е, и современные дизайнеры языка на самом деле имеют представление о том, что они делают?

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

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

Подумайте о чем-то, что Serializable, на самом деле не имеет смысла (с точки зрения дизайна / моделирования) иметь базовый класс под названием Serializable, так как не имеет смысла говорить что-то как Serializable. Наличие чего-либо реализующего интерфейс Serializable имеет больше смысла в том, чтобы сказать "это то, что может сделать класс, а не то, что класс"

Интерфейсы вообще не "нужны", это дизайнерское решение. Я думаю, что вам нужно убедить себя, почему в каждом конкретном случае полезно использовать интерфейс, потому что при добавлении интерфейса возникают накладные расходы. С другой стороны, чтобы противостоять аргументу против интерфейсов, потому что вы можете "просто" использовать наследование: наследование имеет свои недостатки, одним из которых является то, что - по крайней мере в C# и Java - вы можете использовать наследование только один раз (одиночное наследование); но второе - и, возможно, более важное - это то, что наследование требует от вас понимания работы не только родительского класса, но и всех классов-предков, что делает расширение сложнее, но и более хрупким, потому что изменение в родительском классе ' Реализация может легко сломать подклассы. В этом суть аргумента "композиция поверх наследования", которому научила нас книга GOF.

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

Как вы доказываете, что вам нужны интерфейсы в коде, который вы пишете на своем рабочем месте? Находя место в вашей реальной кодовой базе (не в каком-то коде из чужого продукта и, конечно, не в каком-нибудь игрушечном примере о том, как Duck реализует метод makeNoise в IAnimal), где решение на основе интерфейса лучше, чем решение на основе наследования. Покажите своим боссам проблему, с которой вы сталкиваетесь, и спросите, имеет ли смысл модифицировать рекомендации, чтобы приспособиться к таким ситуациям. Это обучающий момент, когда все смотрят на одни и те же факты, а не бьют друг друга по голове общностями и спекуляциями.

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

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

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

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

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

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

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

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

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

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

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

На ум приходит реализация IDisposable. Ваша команда поместит метод Dispose в класс Object и позволит ему распространяться по системе независимо от того, имеет ли он смысл для объекта или нет.

Ну, моя первая реакция заключается в том, что если вам нужно объяснить, почему вам нужны интерфейсы, это все равно тяжелая битва:)

При этом, помимо всех упомянутых выше причин, интерфейсы являются единственным способом для слабосвязанного программирования, n-уровневых архитектур, где необходимо обновлять / заменять компоненты на лету и т. д. - в личном опыте, однако это была слишком эзотерическая концепция для руководителя архитектурной команды, в результате чего мы жили в аду dll - в мире.net не меньше!

Пожалуйста, простите меня за псевдокод заранее!

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

Где вы могли бы привыкнуть

Widget widget1 = new Widget;

Это конкретно говорит о том, что вы хотите создать новый экземпляр Widget. Однако, если вы делаете это внутри метода другого объекта, вы теперь говорите, что другой объект напрямую зависит от использования Widget. Таким образом, мы могли бы сказать что-то вроде

public class AnotherObject
{
    public void SomeMethod(Widget widget1)
    {
        //..do something with widget1
    }
}

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

public class AnotherObject
{
    public void SomeMethod(IWidget widget1)
    {
        //..do something with widget1
    }
}

Обратите внимание, что теперь нам не требуется конкретная реализация Widget, а вместо этого мы запрашиваем все, что соответствует интерфейсу IWidget. Это означает, что все может быть внедрено, что означает, что при повседневном использовании кода мы можем внедрить фактическую реализацию Widget. Но это также означает, что когда мы хотим протестировать этот код, мы можем внедрить поддельный /mock/stub (в зависимости от вашего понимания этих терминов) и протестировать наш код.

Но как мы можем пойти дальше? С помощью StructureMap мы можем еще больше отделить этот код. В последнем примере кода наш вызывающий код может выглядеть примерно так

public class AnotherObject
{
    public void SomeMethod(IWidget widget1)
    {
        //..do something with widget1
    }
}

public class CallingObject
{
    public void AnotherMethod()
    {
        IWidget widget1 = new Widget();
        new AnotherObject().SomeMethod(widget1);
    }
}

Как вы можете видеть в приведенном выше коде, мы удалили зависимость в SomeMethod, передав объект, соответствующий IWidget. Но в CallingObject().AnotherMethod у нас все еще есть зависимость. Мы также можем использовать StructureMap для удаления этой зависимости!

[PluginFamily("Default")]
public interface IAnotherObject
{
    ...
}

[PluginFamily("Default")]
public interface ICallingObject
{
    ...
}

[Pluggable("Default")]
public class AnotherObject : IAnotherObject
{
    private IWidget _widget;
    public AnotherObject(IWidget widget)
    {
        _widget = widget;
    }

    public void SomeMethod()
    {
        //..do something with _widget
    }
}

[Pluggable("Default")]
public class CallingObject : ICallingObject
{
    public void AnotherMethod()
    {
        ObjectFactory.GetInstance<IAnotherObject>().SomeMethod();
    }
}

Обратите внимание, что нигде в приведенном выше коде мы не создаем фактическую реализацию AnotherObject. Поскольку все связано с StructurMap, мы можем разрешить StructureMap передавать соответствующие реализации в зависимости от того, когда и где выполняется код. Теперь код по-настоящему гибок в том, что мы можем указать через конфигурацию или программно в тесте, какую реализацию мы хотим использовать. Эта конфигурация может быть сделана на лету или как часть процесса сборки и т. Д. Но она не должна быть жестко подключена.

Извинения, поскольку это не отвечает на ваш вопрос относительно случая для Интерфейсов.

Тем не менее, я предлагаю, чтобы человек, о котором идет речь, читал..

Head First Design Patterns

- Ли

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

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

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

Я не понимаю, как это лишние накладные расходы.

Интерфейсы обеспечивают гибкость, управляемость кода и возможность повторного использования. При кодировании интерфейса вам не нужно беспокоиться о конкретизированном коде реализации или логике определенного класса, который вы используете. Вы просто ожидаете результата. Многие классы имеют разные реализации для одной и той же функции (StreamWriter,StringWriter,XmlWriter).. вам не нужно беспокоиться о том, как они реализуют запись, вам просто нужно вызвать ее.

Другие вопросы по тегам