2012-04-25 2 views
24

Я был виновен в наличии отношения 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) { } 
} 

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

+1

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

+0

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

+0

Это не вопрос, но наследование переоценено. «SqlLogger» - это всего лишь конкретный «Logger» с 'SqlLogPersistenceStrategy'. В большинстве случаев композиция намного лучше, чем наследование. Также для вашей проблемы, как насчет провайдера? 'ILogInfo',' ILogError' и т. Д. – plalx

ответ

27

"Quick" Ответ

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

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

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

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

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

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

Осуществление против потребления

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

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

Мой опыт

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

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

Базовый класс Против

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

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

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

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

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

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

  1. Полностью отдельные интерфейсы для любого нового материала: MyInterfaceV1, MyInterfaceV2.
  2. Наследование интерфейса: 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(); 
} 
+0

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

+0

Если вы не можете изменить интерфейс - вы можете создать еще один: 'inteface IDebuger {void Debug (строковое сообщение);}' и реализовать его в FileLogger. Поэтому, если другим командам это не понадобится, они не будут использовать и реализовывать его. –

+0

@nivlam Альтернативой этому является создание интерфейса, он заблокирован для изменения. Создайте новые интерфейсы, которые наследуют старые, а затем реализации могут дополнительно реализовать новые интерфейсы ... что означает только вашу внутреннюю реализацию. Я обновил свой ответ на свой счет. –

2

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

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

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

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

+0

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

+4

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

0

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

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

9

Ваш интерфейс ILogger ломает interface segregation principle, когда вы начинаете добавлять Debug, Error и Critical методы, кроме Info. Взгляните на horrible Log4Net ILog interface, и вы поймете, о чем я говорю.

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

void Log(LogEntry entry); 

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

  1. LogEntry будет простой DTO, и вы можете добавить к нему новые свойства, не нарушая работу какого-либо клиента.
  2. Вы можете создать набор методов расширения для вашего 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, 
     }); 
    } 
} 

Для более детального обсуждения этого проекта, пожалуйста, прочитайте this.

+0

Проблема заключается в том, что контракт не определяет уровень серьезности или не поддерживается. С ISP в виду, что относительно 'ILogInfo',' ILogError', 'ILogDebug' и т. Д.? – plalx

Смежные вопросы