Какова цель интерфейса маркера?


Ответы:


77

Это немного касательная, основанная на ответе "Митча Уита".

Как правило, всякий раз, когда я вижу, что люди цитируют рекомендации по разработке фреймворка, я всегда хотел бы упомянуть следующее:

Обычно вам следует игнорировать рекомендации по разработке фреймворка.

Это не из-за каких-либо проблем с рекомендациями по проектированию фреймворка. Я считаю, что .NET framework - фантастическая библиотека классов. Большая часть этой фантастики проистекает из руководящих принципов разработки фреймворка.

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

Многие из содержащихся в нем предложений могут помочь вам в следующих действиях:

  1. Возможно, это не самый простой способ реализовать что-то
  2. Может привести к дополнительному дублированию кода
  3. Могут быть дополнительные накладные расходы времени выполнения

Фреймворк .net большой, очень большой. Он настолько велик, что было бы совершенно неразумно предполагать, что кто-то имеет подробные знания обо всех его аспектах. Фактически, гораздо безопаснее предположить, что большинство программистов часто сталкиваются с частями фреймворка, которые они никогда раньше не использовали.

В этом случае основными задачами разработчика API являются:

  1. Сохраняйте согласованность с остальной частью структуры
  2. Устранение ненужной сложности в области API

Руководящие принципы проектирования фреймворка подталкивают разработчиков к созданию кода, который выполняет эти цели.

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

Основная причина, по которой в этих рекомендациях предлагается использовать атрибуты вместо интерфейсов маркеров, заключается в том, что удаление интерфейсов маркеров делает структуру наследования библиотеки классов гораздо более доступной. Диаграмма классов с 30 типами и 6 уровнями иерархии наследования очень сложна по сравнению с диаграммой с 15 типами и 2 уровнями иерархии.

Если действительно миллионы разработчиков используют ваши API или ваша база кода действительно велика (скажем, более 100 000 LOC), то следование этим рекомендациям может очень помочь.

Если 5 миллионов разработчиков потратят 15 минут на изучение API, а не 60 минут на его изучение, в результате чистая экономия составит 428 человеко-лет. Это много времени.

Однако в большинстве проектов не задействованы миллионы разработчиков или более 100 тысяч разработчиков. В типичном проекте, состоящем, скажем, из 4 разработчиков и около 50 тыс. Локальных сетей, набор допущений сильно отличается. Разработчики в команде будут гораздо лучше понимать, как работает код. Это означает, что гораздо больше смысла оптимизировать для быстрого создания высококачественного кода, а также для уменьшения количества ошибок и усилий, необходимых для внесения изменений.

Если вы потратите 1 неделю на разработку кода, совместимого с платформой .net, против 8 часов на написание кода, который легко изменить и в котором меньше ошибок, это может привести к:

  1. Поздние проекты
  2. Меньшие бонусы
  3. Увеличение количества ошибок
  4. Больше времени проводить в офисе и меньше - на пляже, пьющем маргариту.

Без 4 999 999 других разработчиков, которые несут затраты, это обычно не стоит.

Например, тестирование интерфейсов маркеров сводится к одному выражению «is» и приводит к меньшему количеству кода, который ищет атрибуты.

Итак, мой совет:

  1. Неукоснительно следуйте рекомендациям по фреймворку, если вы разрабатываете библиотеки классов (или виджеты пользовательского интерфейса), предназначенные для широкого использования.
  2. Рассмотрите возможность принятия некоторых из них, если в вашем проекте более 100 000 LOC.
  3. В противном случае полностью игнорируйте их.

12
Лично я вижу любой код, который я пишу, как библиотеку, которая мне понадобится позже. Меня действительно не волнует, широко распространено потребление или нет - следование рекомендациям увеличивает последовательность и уменьшает удивление, когда мне нужно взглянуть на мой код и понять его годы спустя ...
Рид Копси

16
Я не говорю, что правила - это плохо. Я говорю, что они должны быть разными, в зависимости от размера вашей кодовой базы и количества ваших пользователей. Многие рекомендации по проектированию основаны на таких вещах, как поддержание бинарной сопоставимости, что не так важно для «внутренних» библиотек, используемых в нескольких проектах, как для чего-то вроде BCL. Другие рекомендации, например, касающиеся удобства использования, почти всегда важны. Мораль - не относиться к руководящим принципам излишне религиозно, особенно в небольших проектах.
Скотт Вишневски

6
+1 - Не совсем ответил на вопрос ОП - Цель ИМ - Но, тем не менее, очень полезно.
bzarah

5
@ScottWisniewski: Я думаю, вам не хватает серьезных моментов. Руководящие принципы Framework просто не применимы к большим проектам, они применимы к средним и некоторым небольшим проектам. Они становятся чрезмерными, когда вы всегда пытаетесь применить их к программе Hello-World. Например, ограничение интерфейсов до 5 методов всегда является хорошим практическим правилом независимо от размера приложения. Еще одна вещь, которую вы упускаете: маленькое приложение сегодня может стать большим приложением завтра. Поэтому лучше строить его, руководствуясь хорошими принципами, применимыми к большим приложениям, чтобы, когда придет время масштабировать, вам не пришлось переписывать много кода.
Фил

2
Я не совсем понимаю, как выполнение (большей части) рекомендаций по дизайну может привести к тому, что 8-часовой проект внезапно займет 1 неделю. Пример: Присвоение имени virtual protectedшаблонному методу DoSomethingCoreвместо DoSomething- не такая уж большая дополнительная работа, и вы четко даете понять, что это шаблонный метод ... IMNSHO, люди, которые пишут приложения без учета API ( But.. I'm not a framework developer, I don't care about my API!), - это именно те люди, которые пишут много дублированных ( а также недокументированный и обычно нечитаемый) код, а не наоборот.
Laoujin

45

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

Руководства по дизайну интерфейсов и .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 
{    
   ...
} 

27
Кроме того, мы можем полностью использовать дженерики с интерфейсами маркеров, но не с атрибутами.
Jordão

18
Хотя мне нравятся атрибуты и то, как они выглядят с декларативной точки зрения, они не являются первоклассными гражданами во время выполнения и требуют значительного количества относительно низкоуровневой сантехники для работы.
Джесси С. Слайсер

4
@ Жордао - Это была моя мысль. В качестве примера, если я хочу абстрагироваться от кода доступа к базе данных (скажем, от Linq до Sql), наличие общего интерфейса делает это НАМНОГО проще. Фактически, я не думаю, что можно было бы написать такую ​​абстракцию с атрибутами, поскольку вы не можете привести атрибут к атрибуту и ​​не можете использовать их в дженериках. Я полагаю, вы могли бы использовать пустой базовый класс, от которого происходят все остальные классы, но это более или менее похоже на пустой интерфейс. Кроме того, если позже вы поймете, что вам нужна общая функциональность, механизм уже существует.
tandrewnichols

23

Поскольку в каждом другом ответе говорится, что «их следует избегать», было бы полезно получить объяснение, почему.

Во-первых, почему используются интерфейсы-маркеры: они существуют для того, чтобы позволить коду, использующему объект, реализующий его, проверить, реализуют ли они указанный интерфейс, и по-разному относиться к объекту, если он это делает.

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

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

Таким образом, «маркер» - это метаданные о классе. Эти метаданные не используются самим классом и имеют значение только для (некоторого!) Внешнего клиентского кода, так что он может обрабатывать объект определенным образом. Поскольку они имеют значение только для клиентского кода, метаданные должны быть в клиентском коде, а не в API класса.

Разница между «интерфейсом маркера» и обычным интерфейсом состоит в том, что интерфейс с методами сообщает внешнему миру, как его можно использовать, тогда как пустой интерфейс подразумевает, что он сообщает внешнему миру, как его следует использовать.


1
Основная цель любого интерфейса - различать классы, которые обещают соблюдать контракт, связанный с этим интерфейсом, и классы, которые этого не делают. Хотя интерфейс также отвечает за предоставление вызывающих подписей любых членов, необходимых для выполнения контракта, именно контракт, а не члены, определяет, должен ли конкретный интерфейс быть реализован определенным классом. Если в контракте IConstructableFromString<T>указано, что класс Tможет быть реализован только в том IConstructableFromString<T>случае, если у него есть статический член ...
supercat

... public static T ProduceFromString(String params);, класс-компаньон для интерфейса может предлагать метод public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; если бы в клиентском коде был такой метод T[] MakeManyThings<T>() where T:IConstructableFromString<T>, можно было бы определять новые типы, которые могли бы работать с клиентским кодом, без необходимости изменять клиентский код для работы с ними. Если бы метаданные были в клиентском коде, было бы невозможно создать новые типы для использования существующим клиентом.
supercat

Но в контракте между Tи классом, который его использует, у IConstructableFromString<T>вас есть метод в интерфейсе, который описывает некоторое поведение, поэтому это не интерфейс маркера.
Tom B

Статический метод, который должен иметь класс, не является частью интерфейса. Статические члены в интерфейсах реализуются самими интерфейсами; у интерфейса нет возможности ссылаться на статический член в реализующем классе.
суперкар

Метод может определить с помощью Reflection, имеет ли универсальный тип конкретный статический метод, и выполнить этот метод, если он существует, но фактический процесс поиска и выполнения статического метода ProduceFromStringв приведенном выше примере не будет включать интерфейс любым способом, за исключением того, что интерфейс будет использоваться в качестве маркера, чтобы указать, какие классы следует ожидать для реализации необходимой функции.
суперкар

8

Интерфейсы-маркеры иногда могут быть неизбежным злом, если язык не поддерживает размеченные типы объединения .

Предположим, вы хотите определить метод, который ожидает аргумент, тип которого должен быть в точности одним из A, B или C. Во многих функционально-ориентированных языках (например, F # ) такой тип может быть четко определен как:

type Arg = 
    | AArg of A 
    | BArg of B 
    | CArg of C

Однако в языках OO-first, таких как C #, это невозможно. Единственный способ добиться здесь чего-то подобного - определить интерфейс IArg и «пометить» им A, B и C.

Конечно, вы можете избежать использования интерфейса маркера, просто приняв тип «объект» в качестве аргумента, но тогда вы потеряете выразительность и некоторую степень безопасности типов.

Размеченные типы объединения чрезвычайно полезны и существуют в функциональных языках не менее 30 лет. Как ни странно, до сих пор все основные объектно-ориентированные языки игнорируют эту возможность - хотя на самом деле она не имеет ничего общего с функциональным программированием как таковым, а принадлежит системе типов.


Стоит отметить, что, поскольку у a Foo<T>будет отдельный набор статических полей для каждого типа T, нетрудно иметь общий класс, содержащий статические поля, содержащие делегатов для обработки a T, и предварительно заполнить эти поля функциями для обработки каждого типа, которым класс является предполагается работать с. Использование общего ограничения интерфейса для типа Tбудет проверять во время компиляции, что предоставленный тип, по крайней мере, утвержден как действительный, даже если он не сможет гарантировать, что это действительно так.
supercat

6

Интерфейс маркера - это просто пустой интерфейс. Класс будет реализовывать этот интерфейс как метаданные, которые по какой-то причине будут использоваться. В C # вы бы чаще использовали атрибуты для разметки класса по тем же причинам, по которым вы использовали бы интерфейс маркера на других языках.


4

Интерфейс маркера позволяет пометить класс таким образом, чтобы он был применен ко всем классам-потомкам. «Чистый» маркерный интерфейс ничего не определяет и не наследует; более полезным типом маркерных интерфейсов может быть тот, который «наследует» другой интерфейс, но не определяет новых членов. Например, если существует интерфейс «IReadableFoo», можно также определить интерфейс «IImmutableFoo», который будет вести себя как «Foo», но обещает любому, кто его использует, ничего не изменит его значение. Подпрограмма, которая принимает IImmutableFoo, сможет использовать его, как IReadableFoo, но подпрограмма будет принимать только классы, которые были объявлены как реализующие IImmutableFoo.

Я не могу придумать много вариантов использования «чистых» интерфейсов маркеров. Единственное, о чем я могу думать, - это если EqualityComparer (of T) .Default вернет Object.Equals для любого типа, который реализует IDoNotUseEqualityComparer, даже если этот тип также реализовал IEqualityComparer. Это позволило бы иметь незапечатанный неизменяемый тип без нарушения принципа подстановки Лискова: если тип запечатывает все методы, связанные с проверкой равенства, производный тип может добавлять дополнительные поля и делать их изменяемыми, но мутация таких полей не будет t быть видимым при использовании любых методов базового типа. Было бы неплохо иметь незапечатанный неизменяемый класс и либо избегать любого использования EqualityComparer.Default, либо доверять производным классам, чтобы не реализовывать IEqualityComparer,


4

Эти два метода расширения решат большинство проблем, которые, как утверждает Скотт, отдают предпочтение интерфейсам маркеров перед атрибутами:

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 раз больше времени с первым шаблоном по сравнению со вторым, как утверждает Скотт.


1
По-прежнему нет дженериков.
Ян Кемп

1

Маркеры - это пустые интерфейсы. Маркер либо есть, либо его нет.

класс Foo: IConfidential

Здесь мы отмечаем Foo как конфиденциальную. Никаких дополнительных свойств или атрибутов не требуется.


0

Интерфейс маркера - это полностью пустой интерфейс, не имеющий тела / данных-членов / реализации.
При необходимости класс реализует интерфейс маркера, он просто « маркирует »; означает, что он сообщает JVM, что конкретный класс предназначен для клонирования, поэтому разрешите его клонирование. Этот конкретный класс предназначен для сериализации своих объектов, поэтому разрешите сериализацию его объектов.


0

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.