Jaki jest cel interfejsu znacznika?

Odpowiedzi:

77

To trochę styczna, oparta na odpowiedzi „Mitch Wheat”.

Generalnie za każdym razem, gdy widzę, jak ludzie cytują wytyczne dotyczące projektowania frameworka, zawsze lubię wspomnieć, że:

Generalnie powinieneś ignorować wytyczne dotyczące projektowania frameworka przez większość czasu.

Nie wynika to z żadnego problemu z wytycznymi dotyczącymi projektowania frameworka. Myślę, że platforma .NET to fantastyczna biblioteka klas. Wiele z tej fantastyczności wypływa z wytycznych projektowania frameworka.

Jednak wytyczne projektowe nie dotyczą większości kodu napisanego przez większość programistów. Ich celem jest umożliwienie stworzenia dużego frameworka, z którego korzystają miliony programistów, a nie usprawnienie pisania bibliotek.

Wiele zawartych w nim sugestii może pomóc Ci wykonać następujące czynności:

  1. Może nie być najprostszym sposobem na wdrożenie czegoś
  2. Może spowodować dodatkowe zduplikowanie kodu
  3. Może mieć dodatkowe obciążenie w czasie wykonywania

Framework .net jest duży, naprawdę duży. Jest tak duży, że absolutnie nierozsądne byłoby zakładanie, że ktoś ma szczegółową wiedzę na temat każdego jego aspektu. W rzeczywistości znacznie bezpieczniej jest założyć, że większość programistów często napotyka części frameworka, z których nigdy wcześniej nie korzystali.

W takim przypadku głównymi celami projektanta API są:

  1. Zachowaj spójność z resztą frameworka
  2. Eliminacja niepotrzebnej złożoności w obszarze powierzchni interfejsu API

Wytyczne dotyczące projektowania frameworka zachęcają programistów do tworzenia kodu, który spełnia te cele.

Oznacza to robienie rzeczy takich jak unikanie warstw dziedziczenia, nawet jeśli oznacza to powielanie kodu lub wypychanie całego wyjątku z wyrzucaniem kodu do „punktów wejścia” zamiast używania współdzielonych pomocników (aby ślady stosu miały więcej sensu w debugerze), i wiele innych podobnych rzeczy.

Głównym powodem, dla którego te wytyczne sugerują używanie atrybutów zamiast interfejsów znaczników, jest to, że usunięcie interfejsów znaczników sprawia, że ​​struktura dziedziczenia biblioteki klas jest znacznie bardziej przystępna. Diagram klas z 30 typami i 6 warstwami hierarchii dziedziczenia jest bardzo onieśmielający w porównaniu do diagramu z 15 typami i 2 warstwami hierarchii.

Jeśli naprawdę miliony programistów używają twoich interfejsów API lub twoja baza kodu jest naprawdę duża (powiedzmy ponad 100 000 LOC), przestrzeganie tych wskazówek może bardzo pomóc.

Jeśli 5 milionów programistów poświęci 15 minut na naukę interfejsu API, zamiast spędzać 60 minut na jego nauce, w rezultacie uzyskamy oszczędności netto w wysokości 428 osobolat. To dużo czasu.

Większość projektów nie angażuje jednak milionów programistów ani 100 000 + LOC. W typowym projekcie, na przykład z 4 programistami i około 50 tys. Lokalizacji, zestaw założeń jest znacznie inny. Deweloperzy w zespole będą mieli znacznie lepsze zrozumienie działania kodu. Oznacza to, że o wiele bardziej sensowna jest optymalizacja pod kątem szybkiego tworzenia kodu wysokiej jakości oraz zmniejszenia liczby błędów i wysiłku potrzebnego do wprowadzenia zmian.

Spędzenie 1 tygodnia na tworzeniu kodu zgodnego z frameworkiem .net, w porównaniu z 8 godzinami na pisanie kodu, który jest łatwy do zmiany i ma mniej błędów, może skutkować:

  1. Późne projekty
  2. Niższe premie
  3. Zwiększona liczba błędów
  4. Więcej czasu spędzonego w biurze, a mniej czasu na plaży, popijając margarity.

Bez 4 999 999 innych programistów, którzy pochłoną koszty, zazwyczaj nie jest to tego warte.

Na przykład testowanie interfejsów znaczników sprowadza się do pojedynczego wyrażenia „is” i skutkuje mniejszą ilością kodu niż szukanie atrybutów.

Więc moja rada brzmi:

  1. Jeśli tworzysz biblioteki klas (lub widżety interfejsu użytkownika) przeznaczone do szerokiego użytku, postępuj religijnie zgodnie z wytycznymi ramowymi.
  2. Rozważ przyjęcie niektórych z nich, jeśli masz w projekcie ponad 100 000 LOC
  3. W przeciwnym razie zignoruj ​​je całkowicie.
Scott Wiśniewski
źródło
12
Osobiście widzę każdy kod, który napiszę jako bibliotekę, którego będę potrzebował później. Nie obchodzi mnie, czy konsumpcja jest powszechna, czy nie - przestrzeganie wytycznych zwiększa spójność i zmniejsza zdziwienie, gdy muszę spojrzeć na mój kod i zrozumieć go po latach ...
Reed Copsey
16
Nie mówię, że wytyczne są złe. Mówię, że powinny być różne, w zależności od rozmiaru bazy kodu i liczby użytkowników. Wiele wskazówek projektowych opiera się na takich rzeczach, jak zachowanie porównywalności binarnej, co nie jest tak ważne dla bibliotek „wewnętrznych” używanych przez kilka projektów, jak w przypadku czegoś takiego jak BCL. Inne wytyczne, takie jak te związane z użytecznością, są prawie zawsze ważne. Morał nie jest przesadnie religijny, jeśli chodzi o wytyczne, szczególnie w przypadku małych projektów.
Scott Wisniewski
6
+1 - Niezupełnie odpowiedział na pytanie PO - Cel MI - Ale mimo to bardzo pomocny.
bzarah
5
@ScottWisniewski: Myślę, że brakuje ci poważnych punktów. Wytyczne ramowe po prostu nie dotyczą dużych projektów, mają zastosowanie do średnich i niektórych małych projektów. Stają się nadmiernym zabijaniem, gdy zawsze próbujesz zastosować je w programie Hello-World. Na przykład ograniczenie interfejsów do 5 metod jest zawsze dobrą zasadą, niezależnie od rozmiaru aplikacji. Kolejna rzecz, za którą tęsknisz, mała aplikacja dzisiaj może stać się dużą aplikacją jutra. Dlatego lepiej jest budować go, kierując się dobrymi zasadami, które mają zastosowanie do dużych aplikacji, aby nie trzeba było ponownie pisać dużej ilości kodu, gdy przychodzi czas na skalowanie.
Phil
2
Nie bardzo rozumiem, w jaki sposób przestrzeganie (większości) wytycznych projektowych skutkowałoby 8-godzinnym projektem nagle trwającym 1 tydzień. Np .: Nazywanie virtual protectedmetody szablonowej DoSomethingCorezamiast metody nie DoSomethingwymaga dodatkowej pracy i jasno komunikujesz, że jest to metoda szablonowa ... IMNSHO, ludzie, którzy piszą aplikacje bez uwzględnienia API ( But.. I'm not a framework developer, I don't care about my API!), to dokładnie ci ludzie, którzy piszą dużo zduplikowanych a także nieudokumentowany i zwykle nieczytelny) kod, a nie na odwrót.
Laoujin
44

Interfejsy znaczników służą do oznaczania możliwości klasy jako implementującej określony interfejs w czasie wykonywania.

Na interfejs projektowania i .NET Wytyczne projektowe Typ - Interfejs Projekt zniechęcać do korzystania z interfejsów znacznik na rzecz korzystania atrybuty w C #, ale jak @Jay Bazuzi zwraca uwagę, że łatwiej jest sprawdzić interfejsów markerów niż dla atrybutów:o is I

Więc zamiast tego:

public interface IFooAssignable {} 

public class FooAssignableAttribute : IFooAssignable 
{
    ...
}

Wytyczne .NET zalecają to zrobić:

public class FooAssignableAttribute : Attribute 
{
    ...
}

[FooAssignable]
public class Foo 
{    
   ...
} 
Mitch Wheat
źródło
27
Ponadto możemy w pełni używać typów ogólnych z interfejsami znaczników, ale nie z atrybutami.
Jordão,
18
Chociaż uwielbiam atrybuty i to, jak wyglądają z deklaratywnego punktu widzenia, nie są one obywatelami pierwszej klasy w czasie wykonywania i wymagają znacznej ilości instalacji hydraulicznych stosunkowo niskiego poziomu.
Jesse C. Slicer,
4
@ Jordão - To była dokładnie moja myśl. Na przykład, jeśli chcę abstrakcyjny kod dostępu do bazy danych (powiedzmy Linq do Sql), posiadanie wspólnego interfejsu sprawia, że ​​jest to DUŻO łatwiejsze. W rzeczywistości nie sądzę, aby było możliwe napisanie tego rodzaju abstrakcji z atrybutami, ponieważ nie można rzutować na atrybut i nie można ich używać w rodzajach. Przypuszczam, że można by użyć pustej klasy bazowej, z której wywodzą się wszystkie inne klasy, ale wydaje się to mniej więcej tym samym, co posiadanie pustego interfejsu. Ponadto, jeśli później zdasz sobie sprawę, że potrzebujesz współdzielonej funkcjonalności, mechanizm już działa.
tandrewnichols
23

Ponieważ każda inna odpowiedź zawierała stwierdzenie „należy ich unikać”, warto byłoby mieć wyjaśnienie, dlaczego.

Po pierwsze, dlaczego używane są interfejsy znaczników: istnieją, aby umożliwić kodowi, który używa obiektu, który go implementuje, sprawdzenie, czy implementuje wspomniany interfejs i traktuje obiekt inaczej, jeśli tak.

Problem z tym podejściem polega na tym, że przerywa hermetyzację. Sam obiekt ma teraz pośrednią kontrolę nad tym, jak będzie używany na zewnątrz. Ponadto ma wiedzę na temat systemu, w którym będzie używany. Poprzez zastosowanie interfejsu znacznika definicja klasy sugeruje, że oczekuje się, że będzie używany w miejscu, które sprawdza istnienie znacznika. Ma ukrytą wiedzę o środowisku, w którym jest używany, i próbuje zdefiniować, jak powinno być używane. Jest to sprzeczne z ideą hermetyzacji, ponieważ ma wiedzę o implementacji części systemu, która istnieje całkowicie poza jego własnym zakresem.

W praktyce ogranicza to przenośność i możliwość ponownego użycia. Jeśli klasa jest ponownie używana w innej aplikacji, interfejs również musi zostać skopiowany i może nie mieć żadnego znaczenia w nowym środowisku, co czyni go całkowicie zbędnym.

W związku z tym „znacznik” to metadane dotyczące klasy. Te metadane nie są używane przez samą klasę i mają znaczenie tylko dla (niektórych!) Zewnętrznego kodu klienta, dzięki czemu może on traktować obiekt w określony sposób. Ponieważ ma znaczenie tylko dla kodu klienta, metadane powinny znajdować się w kodzie klienta, a nie w interfejsie API klasy.

Różnica między „interfejsem znacznika” a normalnym interfejsem polega na tym, że interfejs z metodami mówi światu zewnętrznemu, jak może być używany, podczas gdy pusty interfejs oznacza, że ​​mówi światu zewnętrznemu, jak powinien być używany.

Tom B.
źródło
1
Podstawowym celem każdego interfejsu jest rozróżnienie między klasami, które obiecują przestrzegać kontraktu związanego z tym interfejsem, a tymi, które tego nie robią. Chociaż interfejs jest również odpowiedzialny za dostarczanie sygnatur wywołujących wszystkich członków niezbędnych do wypełnienia kontraktu, to kontrakt, a nie elementy członkowskie, określa, czy dany interfejs powinien być implementowany przez określoną klasę. Jeśli kontrakt dla IConstructableFromString<T>określa, że ​​klasa Tmoże być implementowana tylko IConstructableFromString<T>wtedy, gdy ma statyczny element członkowski ...
supercat
... public static T ProduceFromString(String params);, klasa towarzysząca interfejsowi może oferować metodę public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>; gdyby kod klienta miał taką metodę T[] MakeManyThings<T>() where T:IConstructableFromString<T>, można by zdefiniować nowe typy, które mogłyby współpracować z kodem klienta bez konieczności modyfikowania kodu klienta, aby sobie z nimi poradzić. Gdyby metadane znajdowały się w kodzie klienta, nie byłoby możliwe utworzenie nowych typów do użycia przez istniejącego klienta.
supercat
Ale kontrakt między Ti klasą, która go używa, polega na tym, IConstructableFromString<T>że masz w interfejsie metodę opisującą pewne zachowanie, więc nie jest to interfejs znacznika.
Tom B
Metoda statyczna, którą klasa musi mieć, nie jest częścią interfejsu. Statyczne elementy członkowskie w interfejsach są implementowane przez same interfejsy; nie ma możliwości, aby interfejs odwoływał się do statycznego elementu członkowskiego w klasie implementującej.
superkat
Metoda może określić za pomocą Reflection, czy typ ogólny ma określoną metodę statyczną i wykonać tę metodę, jeśli istnieje, ale faktyczny proces wyszukiwania i wykonywania metody statycznej ProduceFromStringw powyższym przykładzie nie wymagałby interfejsu w jakikolwiek sposób, z wyjątkiem tego, że interfejs byłby używany jako znacznik wskazujący, jakie klasy powinny zaimplementować niezbędną funkcję.
superkat
8

Interfejsy znaczników mogą czasami być złem koniecznym, gdy język nie wspiera związków dyskryminowanych typów .

Załóżmy, że chcesz zdefiniować metodę, która oczekuje argumentu, którego typ musi być dokładnie jednym z A, B lub C.W wielu językach funkcjonalnych (takich jak F # ) taki typ można jednoznacznie zdefiniować jako:

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

Jednak w językach OO-first, takich jak C #, nie jest to możliwe. Jedynym sposobem na osiągnięcie czegoś podobnego tutaj jest zdefiniowanie interfejsu IArg i „zaznaczenie” nim A, B i C.

Oczywiście możesz uniknąć używania interfejsu znaczników, akceptując po prostu "obiekt" typu jako argument, ale wtedy stracisz wyrazistość i pewien stopień bezpieczeństwa typów.

Związki dyskryminacyjne są niezwykle przydatne i istnieją w językach funkcjonalnych od co najmniej 30 lat. Co dziwne, do dziś wszystkie popularne języki OO ignorują tę funkcję - chociaż w rzeczywistości nie ma ona nic wspólnego z programowaniem funkcjonalnym jako takim, ale należy do systemu typów.

Marc Sigrist
źródło
Warto zauważyć, że ponieważ a Foo<T>będzie miał oddzielny zestaw pól statycznych dla każdego typu T, nie jest trudno mieć klasę ogólną zawierającą pola statyczne zawierające delegatów do przetwarzania a Ti wstępnie wypełnić te pola funkcjami obsługującymi każdy typ, jaki ma klasa powinien pracować. Użycie ogólnego ograniczenia interfejsu dla typu Tsprawdzałoby w czasie kompilatora, czy podany typ przynajmniej został zgłoszony jako prawidłowy, nawet jeśli nie byłby w stanie zapewnić, że tak jest.
supercat
6

Interfejs znacznika to po prostu interfejs, który jest pusty. Klasa implementowałaby ten interfejs jako metadane do użycia z jakiegoś powodu. W C # częściej używałbyś atrybutów do oznaczania klasy z tych samych powodów, dla których używałbyś interfejsu znaczników w innych językach.

Richard Anthony Hein
źródło
4

Interfejs znaczników umożliwia oznaczenie klasy w sposób, który zostanie zastosowany do wszystkich klas podrzędnych. „Czysty” interfejs znacznika niczego by nie definiował ani nie dziedziczył; bardziej użytecznym typem interfejsów znaczników może być taki, który „dziedziczy” inny interfejs, ale nie definiuje nowych członków. Na przykład, jeśli istnieje interfejs „IReadableFoo”, można by również zdefiniować interfejs „IImmutableFoo”, który zachowywałby się jak „Foo”, ale obiecałby każdemu, kto go używa, że ​​nic nie zmieni jego wartości. Procedura akceptująca IImmutableFoo mogłaby używać go tak, jak IReadableFoo, ale procedura akceptowała tylko klasy, które zostały zadeklarowane jako implementujące IImmutableFoo.

Nie przychodzi mi do głowy wiele zastosowań „czystych” interfejsów znaczników. Jedyne, o czym mogę pomyśleć, to gdyby EqualityComparer (z T) .Default zwróciłoby Object.Equals dla dowolnego typu, który zaimplementował IDoNotUseEqualityComparer, nawet jeśli typ zaimplementował również IEqualityComparer. Pozwoliłoby to na posiadanie niezamykanego niezmiennego typu bez naruszania zasady podstawiania Liskova: jeśli typ pieczętuje wszystkie metody związane z testowaniem równości, typ pochodny mógłby dodawać dodatkowe pola i sprawiać, że byłyby one zmienne, ale mutacja takich pól nie być widocznym przy użyciu metod bazowych. Może nie być straszne posiadanie niezapieczętowanej niezmiennej klasy i albo uniknięcie jakiegokolwiek użycia EqualityComparer.Default lub zaufania klas pochodnych, aby nie implementować IEqualityComparer,

supercat
źródło
4

Te dwie metody rozszerzające rozwiążą większość problemów, które według Scotta faworyzują interfejsy znaczników nad atrybutami:

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>()
}

Teraz masz:

if (o.HasAttribute<FooAssignableAttribute>())
{
    //...
}

przeciw:

if (o is IFooAssignable)
{
    //...
}

Nie widzę, jak zbudowanie API zajmie 5 razy dłużej z pierwszym wzorcem w porównaniu z drugim, jak twierdzi Scott.

Panie Anderson
źródło
1
Nadal nie ma leków generycznych.
Ian Kemp
1

Znaczniki to puste interfejsy. Znacznik jest albo tam, albo go nie ma.

klasa Foo: IConfidential

Tutaj oznaczamy Foo jako poufne. Nie są wymagane żadne dodatkowe właściwości ani atrybuty.

Rick O'Shea
źródło
0

Interfejs znacznika to całkowicie pusty interfejs, który nie ma treści / elementów składowych danych / implementacji.
Klasa implementuje interfejs znacznika, gdy jest to wymagane, służy po prostu do „ zaznaczania ”; oznacza, że ​​informuje maszynę JVM, że dana klasa jest przeznaczona do klonowania, więc pozwól jej na klonowanie. Ta konkretna klasa służy do serializacji swoich obiektów, więc proszę pozwolić na serializację jej obiektów.

Arun Raaj
źródło
0

Interfejs znaczników to tak naprawdę programowanie proceduralne w języku OO. Interfejs definiuje kontrakt między implementującymi a konsumentami, z wyjątkiem interfejsu znacznika, ponieważ interfejs znacznika definiuje tylko siebie. Tak więc zaraz po wyjściu z bramki interfejs znacznika zawodzi w podstawowym celu, jakim jest interfejs.

Ali Bayat
źródło