Jaki jest cel interfejsu znacznika?
Odpowiedzi:
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:
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ą:
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ć:
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:
virtual protected
metody szablonowej DoSomethingCore
zamiast metody nie DoSomething
wymaga 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.
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
{
...
}
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.
IConstructableFromString<T>
określa, że klasa T
może być implementowana tylko IConstructableFromString<T>
wtedy, gdy ma statyczny element członkowski ...
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.
T
i 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.
ProduceFromString
w 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ę.
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.
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 T
i 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 T
sprawdzał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.
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.
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,
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.
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.
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.
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.