Implementowanie interfejsu, gdy nie potrzebujesz jednej z właściwości

31

Całkiem proste. Implementuję interfejs, ale jest jedna właściwość, która jest niepotrzebna dla tej klasy i w rzeczywistości nie powinna być używana. Mój początkowy pomysł polegał na zrobieniu czegoś takiego:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Przypuszczam, że nie ma w tym nic złego, ale nie wydaje się to „właściwe”. Czy ktoś wcześniej spotkał się z podobną sytuacją? Jeśli tak, to jak do tego podszedłeś?

Chris Pratt
źródło
1
Przypominam sobie, że w języku C # jest jakaś częściowo używana klasa, która implementuje interfejs, ale wyraźnie stwierdza w dokumentacji, że pewna metoda nie jest zaimplementowana. Spróbuję sprawdzić, czy mogę to znaleźć.
Mag Xy
Z pewnością byłbym zainteresowany, aby to zobaczyć, jeśli możesz to znaleźć.
Chris Pratt
23
Mogę wskazać wiele przypadków tego w bibliotece .NET - I WSZYSTKIE ZOSTAŁY UZNAWANE ZA ZŁE OKRESOWE BŁĘDY . Jest to kultowe i powszechne naruszenie zasady substytucji Liskowa - powody, dla których nie należy naruszać LSP, można znaleźć w mojej odpowiedzi tutaj
Jimmy Hoffa
3
Czy musisz wdrożyć ten konkretny interfejs, czy możesz wprowadzić interfejs i go używać?
Christopher Schultz
6
„jedna właściwość, która jest niepotrzebna dla tej klasy” - To, czy część interfejsu jest niezbędna, zależy od klientów interfejsu, a nie od implementatorów. Jeśli klasa nie może w rozsądny sposób zaimplementować elementu interfejsu, klasa nie jest odpowiednia dla tego interfejsu. Może to oznaczać, że interfejs jest źle zaprojektowany - prawdopodobnie próbuje zrobić za dużo - ale to nie pomaga klasie.
Sebastian Redl,

Odpowiedzi:

51

Jest to klasyczny przykład tego, jak ludzie decydują się na naruszenie zasady substytucji Liskowa. Zdecydowanie odradzam, ale zachęciłbym do ewentualnie innego rozwiązania:

  1. Być może klasa, którą piszesz, nie zapewnia funkcjonalności zalecanej przez interfejs, jeśli nie wykorzystuje wszystkich elementów interfejsu.
  2. Alternatywnie, interfejs ten może robić wiele rzeczy i może być rozdzielony zgodnie z zasadą segregacji interfejsu.

Jeśli pierwszy przypadek dotyczy Ciebie, po prostu nie implementuj interfejsu dla tej klasy. Potraktujcie to jak gniazdka elektrycznego, gdzie dziura ziemia jest niepotrzebny, więc nie rzeczywiście dołączyć do ziemi. Nie podłączasz niczego z uziemieniem i nic wielkiego! Ale gdy tylko użyjesz czegoś, co wymaga gruntu - możesz być w spektakularnej porażce. Lepiej nie dziurawić fałszywej dziury. Więc jeśli twoja klasa nie robi właściwie tego, co zamierza interfejs, nie implementuj interfejsu.


Oto kilka szybkich bitów z wikipedii:

Zasadę podstawienia Liskowa można po prostu sformułować w następujący sposób: „Nie wzmacniaj warunków wstępnych i nie osłabiaj warunków wstępnych”.

Mówiąc bardziej formalnie, zasada podstawienia Liskowa (LSP) jest szczególną definicją relacji podtytułu, zwanej (silnym) podtypem behawioralnym, która została początkowo wprowadzona przez Barbarę Liskov w przemówieniu na konferencji w 1987 r. Zatytułowanym Abstrakcja danych i hierarchia. Jest to relacja semantyczna, a nie tylko syntaktyczna, ponieważ ma ona na celu zagwarantowanie semantycznej interoperacyjności typów w hierarchii , [...]

Aby uzyskać semantyczną interoperacyjność i zastępowalność między różnymi realizacjami tych samych umów - potrzebujesz ich wszystkich, aby zobowiązały się do tych samych zachowań.


Zasada segregacji interfejsów mówi, że interfejsy powinny być podzielone na spójne zestawy, tak że nie potrzebujesz interfejsu, który robi wiele różnych czynności, gdy potrzebujesz tylko jednego obiektu. Pomyśl jeszcze raz o interfejsie gniazda elektrycznego, może również mieć termostat, ale utrudniłoby to zainstalowanie gniazda elektrycznego i może utrudnić korzystanie z niego do celów innych niż ogrzewanie. Podobnie jak gniazdko elektryczne z termostatem, duże interfejsy są trudne do wdrożenia i trudne w użyciu.

Zasada segregacji interfejsów (ISP) stanowi, że żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa. [1] ISP dzieli bardzo duże interfejsy na mniejsze i bardziej szczegółowe, dzięki czemu klienci będą musieli wiedzieć tylko o interesujących ich metodach.

Jimmy Hoffa
źródło
Absolutnie. Prawdopodobnie dlatego przede wszystkim nie wydawało mi się to właściwe. Czasami potrzebujesz tylko delikatnego przypomnienia, że ​​robisz coś głupiego. Dzięki.
Chris Pratt,
@ChrisPratt to bardzo często popełniany błąd, dlatego formalizm może być cenny - klasyfikacja zapachów kodu pomaga szybciej je zidentyfikować i przywołać wcześniej stosowane rozwiązania.
Jimmy Hoffa,
4

Dla mnie wygląda dobrze, jeśli to twoja sytuacja.

Wydaje mi się jednak, że twój interfejs (lub jego użycie) jest zepsuty, jeśli klasa wyprowadzająca nie implementuje go w całości. Rozważ podzielenie tego interfejsu.

Oświadczenie: Wymaga to wielokrotnego dziedziczenia, aby wykonać poprawnie, i nie mam pojęcia, czy C # obsługuje to.

Lekkość Wyścigi z Moniką
źródło
6
C # nie obsługuje wielokrotnego dziedziczenia klas, ale obsługuje je dla interfejsów.
mgw854,
2
Myślę, że masz rację. Interfejs jest w końcu umową i nawet jeśli wiem, że ta konkretna klasa nie będzie używana w sposób, który psuje wszystko, co korzysta z interfejsu, jeśli ta właściwość jest wyłączona, to nie jest oczywiste.
Chris Pratt
@ChrisPratt: Tak.
Wyścigi lekkości z Monicą
4

Natknąłem się na tę sytuację. W rzeczywistości, jak wskazano w innym miejscu, BCL ma takie przypadki ... Postaram się podać lepsze przykłady i podać uzasadnienie:

Gdy masz już dostarczony interfejs, który przechowujesz ze względu na kompatybilność i ...

  • Interfejs zawiera przestarzałe lub zdyskredytowane elementy. Na przykład BlockingCollection<T>.ICollection.SyncRoot(między innymi), choć sam w sobie ICollection.SyncRootnie jest przestarzały, będzie rzucał NotSupportedException.

  • Interfejs zawiera elementy, które są udokumentowane jako opcjonalne i które implementacja może zgłosić wyjątek. Na przykład w MSDN, co do IEnumerator.Resettego mówi:

Metoda resetowania zapewnia interoperacyjność COM. Nie musi to koniecznie zostać wdrożone; zamiast tego implementator może po prostu zgłosić NotSupportedException.

  • Przez pomyłkę w konstrukcji interfejsu powinien być przede wszystkim więcej niż jeden interfejs. W BCL jest powszechnym wzorem do implementacji wersji kontenerów tylko do odczytu NotSupportedException. Zrobiłem to sam, to czego oczekuje teraz ... robię ICollection<T>.IsReadOnlyzwrot truewięc można powiedzieć im appart. Prawidłowym projektem byłoby mieć czytelną wersję interfejsu, a następnie dziedziczy pełny interfejs.

  • Nie ma lepszego interfejsu do użycia. Na przykład mam klasę, która pozwala na dostęp do elementów według indeksu, sprawdzanie, czy zawiera element i na jakim indeksie ma pewien rozmiar, możesz skopiować go do tablicy ... wydaje się, że to zadanie, IList<T>ale moja klasa ma stały rozmiar i nie obsługuje dodawania ani usuwania, więc działa bardziej jak tablica niż lista. Ale nie ma tego IArray<T>w BCL .

  • Interfejs należy do interfejsu API, który jest przeniesiony na wiele platform, a przy implementacji konkretnej platformy niektóre jej części nie są obsługiwane. Idealnie byłoby istnieć jakiś sposób, aby to wykryć, aby przenośny kod korzystający z takiego API mógł decydować o tym, czy wywołać te części, ale jeśli je wywołasz, to jest całkowicie właściwe NotSupportedException. Jest to szczególnie prawdziwe, jeśli jest to port nowej platformy, który nie był przewidziany w pierwotnym projekcie.


Zastanów się także, dlaczego nie jest obsługiwane?

Czasami InvalidOperationExceptionjest lepszym rozwiązaniem. Na przykład jeszcze jednym sposobem na dodanie polimorfizmu do klasy jest posiadanie różnych implementacji interfejsu wewnętrznego, a twój kod wybiera, który z nich utworzyć w zależności od parametrów podanych w konstruktorze klasy. [Jest to szczególnie przydatne, jeśli wiesz, że zestaw opcji jest stały i nie chcesz zezwalać na wprowadzanie klas stron trzecich przez wstrzykiwanie zależności.] Zrobiłem to, aby backportować ThreadLocal, ponieważ implementacja śledzenia i śledzenia nie jest zbyt daleko, a co ThreadLocal.Valuesrzuca się na implementację bez śledzenia?InvalidOperationExceptionnawet jeśli nie zależy to od stanu obiektu. W tym przypadku sam przedstawiłem klasę i wiedziałem, że ta metoda musi zostać zaimplementowana przez zgłoszenie wyjątku.

Czasami wartość domyślna ma sens. Na przykład we ICollection<T>.IsReadOnlywspomnianym powyżej sensownym jest zwrócenie „prawdy” lub „fałszu” w zależności od przypadku. Więc ... co to jest semantyka IFoo.Bar? może być jakaś sensowna wartość domyślna do zwrócenia.


Dodatek: jeśli masz kontrolę nad interfejsem (i nie musisz pozostać przy nim dla kompatybilności), nie powinno być przypadku, w którym musisz rzucić NotSupportedException. Chociaż może być konieczne podzielenie interfejsu na dwa lub więcej mniejszych interfejsów, aby mieć odpowiednie dopasowanie do twojej skrzynki, co może prowadzić do „zanieczyszczenia” w ekstremalnych sytuacjach.

Theraot
źródło
0

Czy ktoś wcześniej spotkał się z podobną sytuacją?

Tak, zrobili to projektanci bibliotek .Net. Klasa Dictionary robi to, co robisz. Wykorzystuje jawną implementację, aby skutecznie ukryć * niektóre metody IDictionary. Jest to wyjaśnione lepiej tutaj , ale do podsumowania, w celu wykorzystania dodać słownik'S, CopyTo lub usunąć metodami, które mają KeyValuePairs, najpierw trzeba do oddania obiektu do IDictionary.

* To nie „ukrywanie” metod w ścisłym znaczeniu słowa „ukryj”, tak jak używa tego Microsoft . Ale w tym przypadku nie znam lepszego terminu.

user2023861
źródło
... co jest okropne.
Wyścigi lekkości z Monicą
Dlaczego głosowanie negatywne?
user2023861,
2
Prawdopodobnie dlatego, że nie odpowiedziałeś na pytanie. Ish Raczej.
Wyścigi lekkości z Monicą
@LightnessRacesinOrbit, zaktualizowałem swoją odpowiedź, aby była bardziej przejrzysta. Mam nadzieję, że to pomoże OP.
user2023861,
2
Wygląda na to, że odpowiada mi pytanie. Fakt, że może to być dobry lub zły pomysł, nie wydaje się być objęty zakresem pytania, ale nadal może być przydatny do wskazywania odpowiedzi.
Ellesedil
-6

Zawsze możesz po prostu zaimplementować i zwrócić wartość łagodną lub domyślną. W końcu to tylko własność. Może zwrócić 0 (domyślna właściwość int) lub dowolną wartość, która ma sens w twojej implementacji (int.MinValue, int.MaxValue, 1, 42 itd.)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Zgłoszenie wyjątku wydaje się złą formą.

Jon Raynor
źródło
2
Czemu? Jak poprawia się zwracanie nieprawidłowych danych?
Wyścigi lekkości z Monicą
1
To łamie LSP: wyjaśnienie dlaczego Jimmy Hoffa wyjaśnia.
5
Jeśli nie ma możliwych poprawnych wartości zwracanych, lepiej zgłosić wyjątek niż zwrócić niepoprawną wartość. Bez względu na to, co robisz, Twój program ulegnie awarii, jeśli przypadkowo wywoła tę właściwość. Ale jeśli rzucisz wyjątek, będzie oczywiste, dlaczego działa nieprawidłowo.
Tanner Swett
Nie rozumiem teraz, w jaki sposób wartość byłaby niepoprawna, gdy to Ty implementujesz interfejs! To twoja implementacja, może być czymkolwiek chcesz.
Jon Raynor,
Co jeśli poprawna wartość była nieznana w czasie kompilacji?
Andy,