Яке призначення інтерфейсу маркера?
Відповіді:
Це трохи дотична на основі відповіді "Мітч Пшениці".
Як правило, щоразу, коли я бачу, як люди цитують керівні принципи проектування, я завжди хочу зазначити, що:
Як правило, ви повинні ігнорувати керівні принципи проектування каркасу більшу частину часу.
Це не через будь-яку проблему з керівними принципами проектування каркаса. Я думаю, що .NET framework - це фантастична бібліотека класів. Багато цієї фантастики випливає із керівних принципів проектування каркаса.
Однак керівні принципи проектування не застосовуються до більшості кодів, написаних більшістю програмістів. Їх мета - створити великий фреймворк, який використовується мільйонами розробників, а не зробити бібліотечне написання більш ефективним.
Багато пропозицій у ньому можуть допомогти вам робити такі речі:
Структура .net велика, справді велика. Він настільки великий, що було б абсолютно нерозумно вважати, що хтось має детальну інформацію про кожен його аспект. Насправді, набагато безпечніше вважати, що більшість програмістів часто стикаються з частинами фреймворку, які вони ніколи раніше не використовували.
У цьому випадку основними цілями розробника API є:
Керівні принципи проектування фреймворків підштовхують розробників до створення коду, який відповідає цим цілям.
Це означає робити такі дії, як уникати шарів успадкування, навіть якщо це означає дублювання коду або виштовхування всіх винятків, що викидають код до "точок входу", а не використання спільних помічників (щоб сліди стека мали більше сенсу в налагоджувачі), і багато інших подібних речей.
Основною причиною того, що ці рекомендації пропонують використовувати атрибути замість інтерфейсів маркерів, є те, що видалення інтерфейсів маркера робить структуру успадкування бібліотеки класів набагато доступнішою. Діаграма класів із 30 типами та 6 рівнями ієрархії успадкування дуже страшна порівняно з діаграмою з 15 типами та 2 рівнями ієрархії.
Якщо насправді мільйони розробників використовують ваші API або ваша база коду справді велика (скажімо, понад 100 КБ LOC), то дотримання цих вказівок може дуже допомогти.
Якщо 5 мільйонів розробників витрачають 15 хвилин на вивчення API, а не 60 хвилин на його вивчення, то це дає чисту економію 428 людських років. Це багато часу.
У більшості проектів, однак, не задіяні мільйони розробників, або 100 000 + LOC. У типовому проекті, припустимо, 4 розробники та близько 50 тис. Локалів, набір припущень значно відрізняється. Розробники команди будуть набагато краще розуміти, як працює код. Це означає, що набагато більше сенсу оптимізувати для швидкого виробництва високоякісного коду та зменшення кількості помилок та зусиль, необхідних для внесення змін.
Проведення 1 тижня на розробку коду, який відповідає фреймворку .net, проти 8 годин написання коду, який легко змінити і має менше помилок, може призвести до:
Без 4999999 інших розробників покривати витрати зазвичай не варто.
Наприклад, тестування інтерфейсів маркерів зводиться до одного виразу "є", і в результаті отримується менше коду, який шукає атрибути.
Тож моя порада:
virtual protected
методу шаблону DoSomethingCore
замість DoSomething
не є такою великою додатковою роботою, і ви чітко повідомляєте, що це метод шаблону ... IMNSHO, люди, які пишуть програми, не враховуючи API ( But.. I'm not a framework developer, I don't care about my API!
) - це саме ті люди, які пишуть багато дублікатів ( а також недокументований і, як правило, нечитабельний) код, а не навпаки.
Інтерфейси маркерів використовуються для позначення можливостей класу як реалізації певного інтерфейсу під час виконання.
Рекомендації щодо дизайну інтерфейсу та .NET типу - Дизайн інтерфейсу не рекомендує використовувати інтерфейси маркерів на користь використання атрибутів в 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, таких як C #, це неможливо. Єдиний спосіб досягти чогось подібного тут - визначити інтерфейс IArg та "позначити" A, B та C ним.
Звичайно, ви можете уникнути використання інтерфейсу маркера, просто прийнявши тип "об'єкт" як аргумент, але тоді ви втратите виразність і певний рівень безпеки типу.
Дискриміновані типи об’єднань надзвичайно корисні і існують у функціональних мовах щонайменше 30 років. Дивно, але на сьогоднішній день всі основні мови ОО ігнорували цю функцію - хоча вона насправді не має нічого спільного з функціональним програмуванням сама по собі, але належить до системи типів.
Foo<T>
матиме окремий набір статичних полів для кожного типу T
, не важко мати загальний клас, який міститиме статичні поля, що містять делегатів для обробки a T
, і попередньо заповнити ці поля функціями для обробки кожного типу класу. повинен працювати з. Використання загального обмеження інтерфейсу на тип T
перевіряло б під час компілятора, що наданий тип принаймні стверджував, що є дійсним, навіть незважаючи на те, що він не зміг би переконатись, що він був насправді.
Інтерфейс маркера - це просто порожній інтерфейс. Клас реалізував би цей інтерфейс як метадані, які будуть використані з якихось причин. У C # ви частіше використовуєте атрибути для розмітки класу з тих самих причин, що і інтерфейс маркера в інших мовах.
Інтерфейс маркера дозволяє позначити клас так, щоб він застосовувався до всіх класів-нащадків. "Чистий" інтерфейс маркера не може нічого визначити або успадкувати; більш корисним типом інтерфейсів маркерів може бути той, який "успадковує" інший інтерфейс, але не визначає нових членів. Наприклад, якщо є інтерфейс "IReadableFoo", можна також визначити інтерфейс "IImmutableFoo", який поводиться як "Foo", але обіцяє кожному, хто його використовує, що ніщо не змінить його значення. Підпрограма, яка приймає IImmutableFoo, могла б використовувати його, як і IReadableFoo, але підпрограма приймала б лише класи, які були оголошені як реалізовуючі IImmutableFoo.
Я не можу придумати багато варіантів використання "чистого" інтерфейсу маркера. Єдиний, про який я можу думати, - це якщо EqualityComparer (з 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, що конкретний клас призначений для клонування, тому дозвольте йому клонувати. Цей конкретний клас призначений для серіалізації його об’єктів, тому, будь ласка, дозвольте його об’єктам отримувати серіалізацію.
Інтерфейс маркера - це насправді лише процедурне програмування на мові OO. Інтерфейс визначає договір між виконавцями та споживачами, крім інтерфейсу маркера, оскільки інтерфейс маркера не визначає нічого, крім самого себе. Отже, безпосередньо за межі інтерфейсу маркера виходить з ладу основна мета - бути інтерфейсом.