Какова цель интерфейса маркера?
Какова цель интерфейса маркера?
Ответы:
Это немного касательная, основанная на ответе "Митча Уита".
Как правило, всякий раз, когда я вижу, что люди цитируют рекомендации по разработке фреймворка, я всегда хотел бы упомянуть следующее:
Обычно вам следует игнорировать рекомендации по разработке фреймворка.
Это не из-за каких-либо проблем с рекомендациями по проектированию фреймворка. Я считаю, что .NET framework - фантастическая библиотека классов. Большая часть этой фантастики проистекает из руководящих принципов разработки фреймворка.
Однако рекомендации по дизайну не применимы к большей части кода, написанного большинством программистов. Их цель - дать возможность создать большую структуру, которая используется миллионами разработчиков, а не сделать написание библиотеки более эффективным.
Многие из содержащихся в нем предложений могут помочь вам в следующих действиях:
Фреймворк .net большой, очень большой. Он настолько велик, что было бы совершенно неразумно предполагать, что кто-то имеет подробные знания обо всех его аспектах. Фактически, гораздо безопаснее предположить, что большинство программистов часто сталкиваются с частями фреймворка, которые они никогда раньше не использовали.
В этом случае основными задачами разработчика API являются:
Руководящие принципы проектирования фреймворка подталкивают разработчиков к созданию кода, который выполняет эти цели.
Это означает, что нужно делать такие вещи, как избегать уровней наследования, даже если это означает дублирование кода, или выталкивать весь код, генерирующий исключение, в «точки входа» вместо использования общих помощников (чтобы трассировки стека имели больше смысла в отладчике), и многое другое. других подобных вещей.
Основная причина, по которой в этих рекомендациях предлагается использовать атрибуты вместо интерфейсов маркеров, заключается в том, что удаление интерфейсов маркеров делает структуру наследования библиотеки классов гораздо более доступной. Диаграмма классов с 30 типами и 6 уровнями иерархии наследования очень сложна по сравнению с диаграммой с 15 типами и 2 уровнями иерархии.
Если действительно миллионы разработчиков используют ваши API или ваша база кода действительно велика (скажем, более 100 000 LOC), то следование этим рекомендациям может очень помочь.
Если 5 миллионов разработчиков потратят 15 минут на изучение API, а не 60 минут на его изучение, в результате чистая экономия составит 428 человеко-лет. Это много времени.
Однако в большинстве проектов не задействованы миллионы разработчиков или более 100 тысяч разработчиков. В типичном проекте, состоящем, скажем, из 4 разработчиков и около 50 тыс. Локальных сетей, набор допущений сильно отличается. Разработчики в команде будут гораздо лучше понимать, как работает код. Это означает, что гораздо больше смысла оптимизировать для быстрого создания высококачественного кода, а также для уменьшения количества ошибок и усилий, необходимых для внесения изменений.
Если вы потратите 1 неделю на разработку кода, совместимого с платформой .net, против 8 часов на написание кода, который легко изменить и в котором меньше ошибок, это может привести к:
Без 4 999 999 других разработчиков, которые несут затраты, это обычно не стоит.
Например, тестирование интерфейсов маркеров сводится к одному выражению «is» и приводит к меньшему количеству кода, который ищет атрибуты.
Итак, мой совет:
virtual protected
шаблонному методу DoSomethingCore
вместо DoSomething
- не такая уж большая дополнительная работа, и вы четко даете понять, что это шаблонный метод ... IMNSHO, люди, которые пишут приложения без учета API ( But.. I'm not a framework developer, I don't care about my API!
), - это именно те люди, которые пишут много дублированных ( а также недокументированный и обычно нечитаемый) код, а не наоборот.
Маркер Интерфейсы используются для обозначения возможности класса как реализации определенного интерфейса во время выполнения.
Руководства по дизайну интерфейсов и .NET Type Design - Interface Design не поощряют использование интерфейсов-маркеров в пользу использования атрибутов в C #, но, как указывает @Jay Bazuzi, легче проверить интерфейсы-маркеры, чем атрибуты:o is I
Итак, вместо этого:
public interface IFooAssignable {}
public class FooAssignableAttribute : IFooAssignable
{
...
}
В рекомендациях .NET рекомендуется сделать следующее:
public class FooAssignableAttribute : Attribute
{
...
}
[FooAssignable]
public class Foo
{
...
}
Поскольку в каждом другом ответе говорится, что «их следует избегать», было бы полезно получить объяснение, почему.
Во-первых, почему используются интерфейсы-маркеры: они существуют для того, чтобы позволить коду, использующему объект, реализующий его, проверить, реализуют ли они указанный интерфейс, и по-разному относиться к объекту, если он это делает.
Проблема с этим подходом в том, что он нарушает инкапсуляцию. Сам объект теперь имеет косвенный контроль над тем, как он будет использоваться извне. Более того, он знает систему, в которой он будет использоваться. Применяя интерфейс маркера, определение класса предполагает, что он ожидает использования где-то, где проверяется наличие маркера. Он неявно знает среду, в которой используется, и пытается определить, как ее следует использовать. Это идет вразрез с идеей инкапсуляции, потому что она знает о реализации части системы, которая существует полностью за пределами ее собственной области.
На практическом уровне это снижает переносимость и возможность повторного использования. Если класс повторно используется в другом приложении, интерфейс также необходимо скопировать, и он может не иметь никакого значения в новой среде, что делает его полностью избыточным.
Таким образом, «маркер» - это метаданные о классе. Эти метаданные не используются самим классом и имеют значение только для (некоторого!) Внешнего клиентского кода, так что он может обрабатывать объект определенным образом. Поскольку они имеют значение только для клиентского кода, метаданные должны быть в клиентском коде, а не в API класса.
Разница между «интерфейсом маркера» и обычным интерфейсом состоит в том, что интерфейс с методами сообщает внешнему миру, как его можно использовать, тогда как пустой интерфейс подразумевает, что он сообщает внешнему миру, как его следует использовать.
IConstructableFromString<T>
указано, что класс T
может быть реализован только в том IConstructableFromString<T>
случае, если у него есть статический член ...
public static T ProduceFromString(String params);
, класс-компаньон для интерфейса может предлагать метод public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>
; если бы в клиентском коде был такой метод T[] MakeManyThings<T>() where T:IConstructableFromString<T>
, можно было бы определять новые типы, которые могли бы работать с клиентским кодом, без необходимости изменять клиентский код для работы с ними. Если бы метаданные были в клиентском коде, было бы невозможно создать новые типы для использования существующим клиентом.
T
и классом, который его использует, у IConstructableFromString<T>
вас есть метод в интерфейсе, который описывает некоторое поведение, поэтому это не интерфейс маркера.
ProduceFromString
в приведенном выше примере не будет включать интерфейс любым способом, за исключением того, что интерфейс будет использоваться в качестве маркера, чтобы указать, какие классы следует ожидать для реализации необходимой функции.
Интерфейсы-маркеры иногда могут быть неизбежным злом, если язык не поддерживает размеченные типы объединения .
Предположим, вы хотите определить метод, который ожидает аргумент, тип которого должен быть в точности одним из A, B или C. Во многих функционально-ориентированных языках (например, F # ) такой тип может быть четко определен как:
type Arg =
| AArg of A
| BArg of B
| CArg of C
Однако в языках OO-first, таких как C #, это невозможно. Единственный способ добиться здесь чего-то подобного - определить интерфейс IArg и «пометить» им A, B и C.
Конечно, вы можете избежать использования интерфейса маркера, просто приняв тип «объект» в качестве аргумента, но тогда вы потеряете выразительность и некоторую степень безопасности типов.
Размеченные типы объединения чрезвычайно полезны и существуют в функциональных языках не менее 30 лет. Как ни странно, до сих пор все основные объектно-ориентированные языки игнорируют эту возможность - хотя на самом деле она не имеет ничего общего с функциональным программированием как таковым, а принадлежит системе типов.
Foo<T>
будет отдельный набор статических полей для каждого типа T
, нетрудно иметь общий класс, содержащий статические поля, содержащие делегатов для обработки a T
, и предварительно заполнить эти поля функциями для обработки каждого типа, которым класс является предполагается работать с. Использование общего ограничения интерфейса для типа T
будет проверять во время компиляции, что предоставленный тип, по крайней мере, утвержден как действительный, даже если он не сможет гарантировать, что это действительно так.
Интерфейс маркера - это просто пустой интерфейс. Класс будет реализовывать этот интерфейс как метаданные, которые по какой-то причине будут использоваться. В C # вы бы чаще использовали атрибуты для разметки класса по тем же причинам, по которым вы использовали бы интерфейс маркера на других языках.
Интерфейс маркера позволяет пометить класс таким образом, чтобы он был применен ко всем классам-потомкам. «Чистый» маркерный интерфейс ничего не определяет и не наследует; более полезным типом маркерных интерфейсов может быть тот, который «наследует» другой интерфейс, но не определяет новых членов. Например, если существует интерфейс «IReadableFoo», можно также определить интерфейс «IImmutableFoo», который будет вести себя как «Foo», но обещает любому, кто его использует, ничего не изменит его значение. Подпрограмма, которая принимает IImmutableFoo, сможет использовать его, как IReadableFoo, но подпрограмма будет принимать только классы, которые были объявлены как реализующие IImmutableFoo.
Я не могу придумать много вариантов использования «чистых» интерфейсов маркеров. Единственное, о чем я могу думать, - это если EqualityComparer (of T) .Default вернет Object.Equals для любого типа, который реализует IDoNotUseEqualityComparer, даже если этот тип также реализовал IEqualityComparer. Это позволило бы иметь незапечатанный неизменяемый тип без нарушения принципа подстановки Лискова: если тип запечатывает все методы, связанные с проверкой равенства, производный тип может добавлять дополнительные поля и делать их изменяемыми, но мутация таких полей не будет t быть видимым при использовании любых методов базового типа. Было бы неплохо иметь незапечатанный неизменяемый класс и либо избегать любого использования EqualityComparer.Default, либо доверять производным классам, чтобы не реализовывать IEqualityComparer,
Эти два метода расширения решат большинство проблем, которые, как утверждает Скотт, отдают предпочтение интерфейсам маркеров перед атрибутами:
public static bool HasAttribute<T>(this ICustomAttributeProvider self)
where T : Attribute
{
return self.GetCustomAttributes(true).Any(o => o is T);
}
public static bool HasAttribute<T>(this object self)
where T : Attribute
{
return self != null && self.GetType().HasAttribute<T>()
}
Теперь у вас есть:
if (o.HasAttribute<FooAssignableAttribute>())
{
//...
}
против:
if (o is IFooAssignable)
{
//...
}
Я не вижу, как создание API займет в 5 раз больше времени с первым шаблоном по сравнению со вторым, как утверждает Скотт.
Интерфейс маркера - это полностью пустой интерфейс, не имеющий тела / данных-членов / реализации.
При необходимости класс реализует интерфейс маркера, он просто « маркирует »; означает, что он сообщает JVM, что конкретный класс предназначен для клонирования, поэтому разрешите его клонирование. Этот конкретный класс предназначен для сериализации своих объектов, поэтому разрешите сериализацию его объектов.
Интерфейс маркера - это просто процедурное программирование на языке объектно-ориентированного программирования. Интерфейс определяет контракт между разработчиками и потребителями, за исключением интерфейса маркера, поскольку интерфейс маркера не определяет ничего, кроме самого себя. Итак, сразу после выхода маркерный интерфейс не справляется с основной целью - быть интерфейсом.