Ответ 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();
}