Яке призначення інтерфейсу маркера?


Відповіді:


77

Це трохи дотична на основі відповіді "Мітч Пшениці".

Як правило, щоразу, коли я бачу, як люди цитують керівні принципи проектування, я завжди хочу зазначити, що:

Як правило, ви повинні ігнорувати керівні принципи проектування каркасу більшу частину часу.

Це не через будь-яку проблему з керівними принципами проектування каркаса. Я думаю, що .NET framework - це фантастична бібліотека класів. Багато цієї фантастики випливає із керівних принципів проектування каркаса.

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

Багато пропозицій у ньому можуть допомогти вам робити такі речі:

  1. Можливо, це не найпростіший спосіб реалізації чогось
  2. Це може призвести до додаткового дублювання коду
  3. Може мати додаткові накладні витрати

Структура .net велика, справді велика. Він настільки великий, що було б абсолютно нерозумно вважати, що хтось має детальну інформацію про кожен його аспект. Насправді, набагато безпечніше вважати, що більшість програмістів часто стикаються з частинами фреймворку, які вони ніколи раніше не використовували.

У цьому випадку основними цілями розробника API є:

  1. Дотримуйтесь речей, що відповідають решті фреймворку
  2. Усуньте непотрібну складність у площі поверхні API

Керівні принципи проектування фреймворків підштовхують розробників до створення коду, який відповідає цим цілям.

Це означає робити такі дії, як уникати шарів успадкування, навіть якщо це означає дублювання коду або виштовхування всіх винятків, що викидають код до "точок входу", а не використання спільних помічників (щоб сліди стека мали більше сенсу в налагоджувачі), і багато інших подібних речей.

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

Якщо насправді мільйони розробників використовують ваші API або ваша база коду справді велика (скажімо, понад 100 КБ LOC), то дотримання цих вказівок може дуже допомогти.

Якщо 5 мільйонів розробників витрачають 15 хвилин на вивчення API, а не 60 хвилин на його вивчення, то це дає чисту економію 428 людських років. Це багато часу.

У більшості проектів, однак, не задіяні мільйони розробників, або 100 000 + LOC. У типовому проекті, припустимо, 4 розробники та близько 50 тис. Локалів, набір припущень значно відрізняється. Розробники команди будуть набагато краще розуміти, як працює код. Це означає, що набагато більше сенсу оптимізувати для швидкого виробництва високоякісного коду та зменшення кількості помилок та зусиль, необхідних для внесення змін.

Проведення 1 тижня на розробку коду, який відповідає фреймворку .net, проти 8 годин написання коду, який легко змінити і має менше помилок, може призвести до:

  1. Пізні проекти
  2. Нижчі бонуси
  3. Збільшена кількість помилок
  4. Більше часу проводили в офісі, а менше часу на пляжі, п’ючи маргарити.

Без 4999999 інших розробників покривати витрати зазвичай не варто.

Наприклад, тестування інтерфейсів маркерів зводиться до одного виразу "є", і в результаті отримується менше коду, який шукає атрибути.

Тож моя порада:

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

12
Я особисто бачу будь-який код, який я пишу як бібліотеку, який мені доведеться використовувати пізніше. Мені насправді все одно, чи споживання широко поширене чи ні - дотримання вказівок підвищує послідовність і зменшує подив, коли мені потрібно поглянути на свій код і зрозуміти його роками пізніше ...
Рід Копсі,

16
Я не кажу, що рекомендації погані. Я кажу, що вони повинні бути різними, залежно від розміру кодової бази та кількості користувачів. Багато керівних принципів проектування базуються на таких речах, як підтримка двійкової порівняльності, що не так важливо для "внутрішніх" бібліотек, що використовуються кількістю проектів, як для чогось типу BCL. Інші рекомендації, такі як ті, що стосуються зручності використання, майже завжди важливі. Мораль полягає в тому, щоб не бути надмірно релігійними щодо настанов, особливо щодо невеликих проектів.
Скотт Вішневський

6
+1 - Не зовсім відповів на запитання ОП - Мета ІМ - Але тим не менш дуже корисний.
bzarah

5
@ScottWisniewski: Я думаю, ви втрачаєте серйозні моменти. Основні принципи просто не стосуються великих проектів, вони стосуються середніх та деяких малих проектів. Вони стають надмірними, коли ви завжди намагаєтесь застосувати їх до програми 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!) - це саме ті люди, які пишуть багато дублікатів ( а також недокументований і, як правило, нечитабельний) код, а не навпаки.
Лауджин

44

Інтерфейси маркерів використовуються для позначення можливостей класу як реалізації певного інтерфейсу під час виконання.

Рекомендації щодо дизайну інтерфейсу та .NET типу - Дизайн інтерфейсу не рекомендує використовувати інтерфейси маркерів на користь використання атрибутів в 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
Хоча я люблю атрибути і те, як вони виглядають з декларативної точки зору, вони не є громадянами першого класу під час роботи і потребують значної кількості сантехніки відносно низького рівня для роботи.
Jesse C. Slicer

4
@ Jordão - Це саме моя думка. Як приклад, якщо я хочу абстрагувати код доступу до бази даних (скажімо, Linq to 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>вас є метод в інтерфейсі, який описує деяку поведінку, тому це не інтерфейс маркера.
Том Б

Статичний метод, який повинен мати клас, не є частиною інтерфейсу. Статичні члени в інтерфейсах реалізуються самими інтерфейсами; немає ніякого способу для інтерфейсу посилатися на статичного члена в реалізовуючому класі.
суперкот

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

8

Інтерфейси маркерів іноді можуть бути необхідним злом, коли мова не підтримує дискриміновані типи об'єднань .

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

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

Однак у мовах, що працюють на основі OO, таких як C #, це неможливо. Єдиний спосіб досягти чогось подібного тут - визначити інтерфейс IArg та "позначити" A, B та C ним.

Звичайно, ви можете уникнути використання інтерфейсу маркера, просто прийнявши тип "об'єкт" як аргумент, але тоді ви втратите виразність і певний рівень безпеки типу.

Дискриміновані типи об’єднань надзвичайно корисні і існують у функціональних мовах щонайменше 30 років. Дивно, але на сьогоднішній день всі основні мови ОО ігнорували цю функцію - хоча вона насправді не має нічого спільного з функціональним програмуванням сама по собі, але належить до системи типів.


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

6

Інтерфейс маркера - це просто порожній інтерфейс. Клас реалізував би цей інтерфейс як метадані, які будуть використані з якихось причин. У C # ви частіше використовуєте атрибути для розмітки класу з тих самих причин, що і інтерфейс маркера в інших мовах.


4

Інтерфейс маркера дозволяє позначити клас так, щоб він застосовувався до всіх класів-нащадків. "Чистий" інтерфейс маркера не може нічого визначити або успадкувати; більш корисним типом інтерфейсів маркерів може бути той, який "успадковує" інший інтерфейс, але не визначає нових членів. Наприклад, якщо є інтерфейс "IReadableFoo", можна також визначити інтерфейс "IImmutableFoo", який поводиться як "Foo", але обіцяє кожному, хто його використовує, що ніщо не змінить його значення. Підпрограма, яка приймає IImmutableFoo, могла б використовувати його, як і IReadableFoo, але підпрограма приймала б лише класи, які були оголошені як реалізовуючі IImmutableFoo.

Я не можу придумати багато варіантів використання "чистого" інтерфейсу маркера. Єдиний, про який я можу думати, - це якщо EqualityComparer (з 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

Тут ми позначаємо Фу як конфіденційну. Не потрібні фактичні додаткові властивості чи атрибути.


0

Інтерфейс маркера - це загальний порожній інтерфейс, який не має тіла / членів даних / реалізації.
Клас реалізує маркерний інтерфейс, коли потрібно, це просто " позначити "; означає, що він повідомляє JVM, що конкретний клас призначений для клонування, тому дозвольте йому клонувати. Цей конкретний клас призначений для серіалізації його об’єктів, тому, будь ласка, дозвольте його об’єктам отримувати серіалізацію.


0

Інтерфейс маркера - це насправді лише процедурне програмування на мові OO. Інтерфейс визначає договір між виконавцями та споживачами, крім інтерфейсу маркера, оскільки інтерфейс маркера не визначає нічого, крім самого себе. Отже, безпосередньо за межі інтерфейсу маркера виходить з ладу основна мета - бути інтерфейсом.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.