Интерфейсы - это механизм, уменьшающий связь между различными, возможно разрозненными частями системы.
С .NET перспектива
- Определение интерфейса представляет собой список операций и / или свойств.
- Методы интерфейса всегда общедоступны.
- Сам интерфейс не должен быть публичным.
Когда вы создаете класс, который реализует интерфейс, вы должны предоставить явную или неявную реализацию всех методов и свойств, определенных интерфейсом.
Кроме того, .NET имеет только одно наследование, и интерфейсы необходимы объекту для предоставления методов другим объектам, которые не знают или находятся вне своей иерархии классов. Это также называется разоблачением поведения.
Пример, который немного конкретнее:
Представьте себе, что у нас есть много DTO (объектов передачи данных), у которых есть свойства, которые обновляли в последний раз и когда это было. Проблема в том, что не все DTO имеют это свойство, потому что оно не всегда уместно.
В то же время мы хотим, чтобы универсальный механизм гарантировал, что эти свойства будут установлены, если они доступны при передаче в рабочий процесс, но объект рабочего процесса должен быть свободно связан с представленными объектами. то есть метод отправки рабочего процесса не должен знать обо всех тонкостях каждого объекта, и все объекты в рабочем процессе не обязательно являются объектами DTO.
// First pass - not maintainable
void SubmitToWorkflow(object o, User u)
{
if (o is StreetMap)
{
var map = (StreetMap)o;
map.LastUpdated = DateTime.UtcNow;
map.UpdatedByUser = u.UserID;
}
else if (o is Person)
{
var person = (Person)o;
person.LastUpdated = DateTime.Now; // Whoops .. should be UtcNow
person.UpdatedByUser = u.UserID;
}
// Whoa - very unmaintainable.
В приведенном выше коде SubmitToWorkflow()
должен знать о каждом объекте. Кроме того, код представляет собой беспорядок с одним массовым переключателем if / else /, нарушающим принцип не повторять себя (DRY), и требует от разработчиков запоминать изменения копирования / вставки при каждом добавлении нового объекта в систему.
// Second pass - brittle
void SubmitToWorkflow(object o, User u)
{
if (o is DTOBase)
{
DTOBase dto = (DTOBase)o;
dto.LastUpdated = DateTime.UtcNow;
dto.UpdatedByUser = u.UserID;
}
Это немного лучше, но все еще хрупкое. Если мы хотим передать другие типы объектов, нам все еще нужно больше операторов case. и т.д.
// Third pass pass - also brittle
void SubmitToWorkflow(DTOBase dto, User u)
{
dto.LastUpdated = DateTime.UtcNow;
dto.UpdatedByUser = u.UserID;
Это все еще хрупко, и оба метода накладывают ограничение, что все DTO должны реализовать это свойство, которое мы указали, не было универсально применимым. Некоторые разработчики могут испытывать желание написать бесполезные методы, но это плохо пахнет. Мы не хотим, чтобы классы делали вид, что они поддерживают отслеживание обновлений, но не делаем.
Интерфейсы, как они могут помочь?
Если мы определим очень простой интерфейс:
public interface IUpdateTracked
{
DateTime LastUpdated { get; set; }
int UpdatedByUser { get; set; }
}
Интерфейс может реализовать любой класс, которому требуется это автоматическое отслеживание обновлений.
public class SomeDTO : IUpdateTracked
{
// IUpdateTracked implementation as well as other methods for SomeDTO
}
Можно сделать метод рабочего процесса намного более общим, меньшим и более обслуживаемым, и он будет продолжать работать независимо от того, сколько классов реализует интерфейс (DTO или другие), потому что он имеет дело только с интерфейсом.
void SubmitToWorkflow(object o, User u)
{
IUpdateTracked updateTracked = o as IUpdateTracked;
if (updateTracked != null)
{
updateTracked.LastUpdated = DateTime.UtcNow;
updateTracked.UpdatedByUser = u.UserID;
}
// ...
- Мы можем отметить, что изменение
void SubmitToWorkflow(IUpdateTracked updateTracked, User u)
будет гарантировать безопасность типов, однако в этих обстоятельствах оно не выглядит уместным.
В некотором производственном коде, который мы используем, у нас есть генерация кода для создания этих классов DTO из определения базы данных. Единственное, что делает разработчик, - это правильно создает имя поля и украшает класс интерфейсом. Пока свойства называются LastUpdated и UpdatedByUser, они просто работают.
Может быть, вы спрашиваете Что произойдет, если моя база данных унаследована и это невозможно? Вам просто нужно немного больше набрать текст; Еще одна замечательная особенность интерфейсов - они позволяют создавать мосты между классами.
В приведенном ниже коде у нас есть фиктивный LegacyDTO
, ранее существовавший объект с полями с одинаковыми именами. Он реализует интерфейс IUpdateTracked, чтобы связать существующие свойства с разными именами.
// Using an interface to bridge properties
public class LegacyDTO : IUpdateTracked
{
public int LegacyUserID { get; set; }
public DateTime LastSaved { get; set; }
public int UpdatedByUser
{
get { return LegacyUserID; }
set { LegacyUserID = value; }
}
public DateTime LastUpdated
{
get { return LastSaved; }
set { LastSaved = value; }
}
}
Вы могли бы что-то сделать Круто, но не смущает ли наличие нескольких свойств? или Что произойдет, если эти свойства уже есть, но они означают что-то другое? возможность явной реализации интерфейса.
Это означает, что свойства IUpdateTracked будут видны только тогда, когда мы используем ссылку на IUpdateTracked. Обратите внимание, что в объявлении нет открытого модификатора, и объявление содержит имя интерфейса.
// Explicit implementation of an interface
public class YetAnotherObject : IUpdatable
{
int IUpdatable.UpdatedByUser
{ ... }
DateTime IUpdatable.LastUpdated
{ ... }
Такая гибкость в определении того, как класс реализует интерфейс, дает разработчику большую свободу для отделения объекта от методов, которые его потребляют. Интерфейсы - отличный способ разорвать связь.
Интерфейсам гораздо больше, чем это. Это просто упрощенный пример из реальной жизни, в котором используется один аспект программирования на основе интерфейса.
Как я упоминал ранее и другими респондентами, вы можете создавать методы, которые принимают и / или возвращают ссылки на интерфейсы, а не на конкретную ссылку на класс. Если бы мне нужно было найти дубликаты в списке, я мог бы написать метод, который принимает и возвращает IList
(интерфейс, определяющий операции, работающие со списками), и я не ограничен конкретным классом коллекции.
// Decouples the caller and the code as both
// operate only on IList, and are free to swap
// out the concrete collection.
public IList<T> FindDuplicates( IList<T> list )
{
var duplicates = new List<T>()
// TODO - write some code to detect duplicate items
return duplicates;
}
Предупреждение о версии
Если это общедоступный интерфейс, вы объявляете Я гарантирую, что интерфейс x выглядит следующим образом! И после того, как вы отправили код и опубликовали интерфейс, вы никогда не должны его менять. Как только потребительский код начинает полагаться на этот интерфейс, вы не хотите разбивать его код в поле.
См. этот взломанный пост для хорошего обсуждения.
Интерфейсы против абстрактных (базовых) классов
Абстрактные классы могут обеспечивать реализацию, а интерфейсы - нет. Абстрактные классы в некоторой степени более гибки в аспекте управления версиями, если вы следуете некоторым рекомендациям, таким как шаблон NVPI (Non-Virtual Public Interface).
Стоит повторить, что в .NET класс может наследовать только от одного класса, но класс может реализовывать столько интерфейсов, сколько ему захочется.
Внедрение зависимостей
Краткий обзор интерфейсов и внедрения зависимостей (DI) заключается в том, что использование интерфейсов позволяет разработчикам писать код, который запрограммирован для интерфейса для предоставления услуг. На практике вы можете получить множество небольших интерфейсов и небольших классов, и одна идея состоит в том, что небольшие классы, которые выполняют одну и только одну вещь, намного легче кодировать и поддерживать.
class AnnualRaiseAdjuster
: ISalaryAdjuster
{
AnnualRaiseAdjuster(IPayGradeDetermination payGradeDetermination) { ... }
void AdjustSalary(Staff s)
{
var payGrade = payGradeDetermination.Determine(s);
s.Salary = s.Salary * 1.01 + payGrade.Bonus;
}
}
Вкратце, преимущество, показанное в приведенном выше фрагменте, заключается в том, что определение уровня оплаты труда просто вводится в регулятор годового повышения. Как определяется уровень оплаты, на самом деле не имеет значения для этого класса. При тестировании разработчик может смоделировать результаты определения уровня оплаты, чтобы убедиться, что корректор заработной платы функционирует по своему усмотрению. Тесты также бывают быстрыми, потому что он тестирует только класс, а не все остальное.
Это не учебник для начинающих, хотя есть целые книги, посвященные этой теме; приведенный выше пример очень упрощен.