Подтвердить что ты не робот

Инъекция зависимостей с интерфейсами или классами

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

Это простой пример, но предположим, что мне нужно ввести ILogger в один из моих классов.

public interface ILogger
{
    void Info(string message);
}

public class Logger : ILogger
{
    public void Info(string message) { }
}

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

public class Logger
{
    public virtual void Info(string message)
    {
        // Log to file
    }
}

Если мне нужна другая реализация, я могу переопределить метод Info:

public class SqlLogger : Logger
{
    public override void Info(string message)
    {
        // Log to SQL
    }
}

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

public class Logger
{
    public virtual void Info(string message)
    {
        throw new NotImplementedException();
    }
}

public class SqlLogger : Logger
{
    public override void Info(string message) { }
}

public class FileLogger : Logger
{
    public override void Info(string message) { }
}

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

public class Logger
{
    public virtual void Info(string message)
    {
        throw new NotImplementedException();
    }

    public virtual void Debug(string message)
    {
        throw new NotImplementedException();
    }
}

public class SqlLogger : Logger
{
    public override void Info(string message) { }
}

public class FileLogger : Logger
{
    public override void Info(string message) { }
    public override void Debug(string message) { }
}

Опять же, это простой пример, но когда я должен предпочесть интерфейс?

4b9b3361

Ответ 1

"Быстрый ответ"

Я бы придерживался интерфейсов. Они предназначены для контрактов на потребление для внешних объектов.

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

Обновленный ответ "Быстрый"

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

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

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

Реализация против потребления

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

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

Мой опыт

Мы часто имеем связь 1-к-1 с интерфейсами. Это в значительной степени формальность, но иногда вы получаете хорошие экземпляры, где интерфейсы полезны, потому что мы забиваем/имитируем тестовые реализации или фактически предоставляем реализации, специфичные для клиента. Тот факт, что это часто нарушает эту реализацию, если мы случайно меняем интерфейс, не является запахом кода, на мой взгляд, это просто то, как вы работаете с интерфейсами.

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

Недостатки базового класса

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

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

Ломающиеся/поддерживающие реализации

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

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

Я стал свидетелем последнего, я вижу, что это выглядит в двух обличьях:

  • Полностью отдельные интерфейсы для любых новых вещей: MyInterfaceV1, MyInterfaceV2.
  • Наследование интерфейсов: MyInterfaceV2 : MyInterfaceV1.

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

Некоторый код

public interface IGetNames
{
    List<string> GetNames();
}

// One option is to redefine the entire interface and use 
// explicit interface implementations in your concrete classes.
public interface IGetMoreNames
{
    List<string> GetNames();
    List<string> GetMoreNames();
}

// Another option is to inherit.
public interface IGetMoreNames : IGetNames
{
    List<string> GetMoreNames();
}

// A final option is to only define new stuff.
public interface IGetMoreNames 
{
    List<string> GetMoreNames();
}

Ответ 2

Ваш интерфейс ILogger разбивает принцип сегрегации интерфейса, когда вы начинаете добавлять Debug, Error и Critical методы, кроме Info. Посмотрите на ужасный интерфейс LogogNet ILog, и вы узнаете, о чем я говорю.

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

void Log(LogEntry entry);

Это полностью решает все ваши проблемы, потому что:

  • LogEntry будет простым DTO, и вы можете добавить к нему новые свойства, не нарушая работу какого-либо клиента.
  • Вы можете создать набор методов расширения для вашего интерфейса ILogger, который сопоставляется с этим единственным методом Log.

Вот пример такого метода расширения:

public static class LoggerExtensions
{
    public static void Debug(this ILogger logger, string message)
    {
        logger.Log(new LogEntry(message)
        {
            Severity = LoggingSeverity.Debug,
        });
    }

    public static void Info(this ILogger logger, string message)
    {
        logger.Log(new LogEntry(message)
        {
            Severity = LoggingSeverity.Information,
        });
    }
}

Подробнее об этом проекте читайте .

Ответ 3

Вы всегда должны использовать интерфейс.

Да, в некоторых случаях у вас будут одинаковые методы для класса и интерфейса, но в более сложных сценариях вы не будете. Также помните, что в .NET нет множественного наследования.

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

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

Ответ 4

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

Кроме того, контрактный угол, который упоминает Адам Хилдсворт, очень конструктивен. IMHO делает код чище, чем 1-1 реализации интерфейсов, делает его вонючим.