Интерфейсы - это механизм для уменьшения связи между различными, возможно, несопоставимыми частями системы.
С точки зрения .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; }
}
}
Вы могли бы подумать Круто, но разве наличие нескольких свойств не сбивает с толку? или Что произойдет, если эти свойства уже есть, но они означают что-то другое? .NET дает вам возможность явно реализовать интерфейс.
Это означает, что свойства IUpdateTracked будут видны только тогда, когда мы используем ссылку на IUpdateTracked. Обратите внимание, что в объявлении нет модификатора public, а объявление включает имя интерфейса.
// 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 выглядит так! И после того, как вы отправили код и опубликовали интерфейс, вы никогда не должны его изменять. Как только код потребителя начинает полагаться на этот интерфейс, вы не хотите нарушать его код в полевых условиях.
См. эту публикацию Haacked за хорошее обсуждение.
Интерфейсы против абстрактных (базовых) классов
Абстрактные классы могут обеспечивать реализацию, тогда как интерфейсы - нет. Абстрактные классы в некотором смысле более гибкие в аспекте управления версиями, если вы следуете некоторым рекомендациям, таким как шаблон NVPI (невиртуальный открытый интерфейс).
Стоит повторить, что в .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;
}
}
Вкратце, преимущество, показанное в приведенном выше фрагменте, заключается в том, что определение уровня заработной платы просто вводится в корректор ежегодного повышения. То, как определяется класс заработной платы, на самом деле не имеет значения для этого класса. При тестировании разработчик может имитировать результаты определения уровня оплаты, чтобы корректор заработной платы работал должным образом. Тесты также быстрые, потому что тест проверяет только класс, а не все остальное.
Это не учебник для начинающих, так как этому предмету посвящены целые книги; приведенный выше пример очень упрощен.
person
Community
schedule
23.09.2008