Czy dozwolone jest używanie jawnej implementacji interfejsu do ukrywania członków w C #?

14

Rozumiem, jak pracować z interfejsami i implementacją jawnego interfejsu w języku C #, ale zastanawiałem się, czy ukrywanie niektórych elementów, które nie byłyby często używane, jest złym rozwiązaniem. Na przykład:

public interface IMyInterface
{
    int SomeValue { get; set; }
    int AnotherValue { get; set; }
    bool SomeFlag { get; set; }

    event EventHandler SomeValueChanged;
    event EventHandler AnotherValueChanged;
    event EventHandler SomeFlagChanged;
}

Wiem z góry, że wydarzenia, choć przydatne, bardzo rzadko byłyby wykorzystywane. Pomyślałem więc, że warto ukryć je przed klasą implementacyjną poprzez jawną implementację, aby uniknąć bałaganu IntelliSense.

Kyle Baran
źródło
3
Jeśli odwołujesz się do instancji za pośrednictwem interfejsu, i tak by ich nie ukrył, ukryłby się tylko wtedy, gdy odniesienie dotyczy konkretnego typu.
Andy
1
Pracowałem z facetem, który robił to regularnie. Doprowadziło mnie to do szału.
Joel Etherton
... i zacznij używać agregacji.
mikalai

Odpowiedzi:

39

Tak, ogólnie jest to zła forma.

Pomyślałem więc, że warto ukryć je przed klasą implementacyjną poprzez jawną implementację, aby uniknąć bałaganu IntelliSense.

Jeśli twój IntelliSense jest zbyt zagracony, twoja klasa jest za duża. Jeśli twoja klasa jest zbyt duża, prawdopodobnie robi zbyt wiele rzeczy i / lub źle zaprojektowana.

Napraw swój problem, a nie objaw.

Telastyn
źródło
Czy należy go używać do usuwania ostrzeżeń kompilatora o nieużywanych członkach?
Gusdor
11
„Rozwiąż swój problem, a nie objaw”. +1
FreeAsInBeer
11

Użyj funkcji do tego, do czego była przeznaczona. Ograniczanie bałaganu przestrzeni nazw nie jest jednym z nich.

Uzasadnione powody nie polegają na „ukrywaniu się” i organizowaniu, lecz na rozwiązywaniu niejednoznaczności i specjalizacji. Jeśli zaimplementujesz dwa interfejsy definiujące „Foo”, semantyka dla Foo może się różnić. Naprawdę nie jest to lepszy sposób na rozwiązanie tego, z wyjątkiem jawnych interfejsów. Alternatywą jest uniemożliwienie implementacji nakładających się interfejsów lub zadeklarowanie, że interfejsy nie mogą powiązać semantyki (co zresztą jest po prostu kwestią konceptualną). Obie są słabymi alternatywami. Czasami praktyczność przebija elegancję.

Niektórzy autorzy / eksperci uważają to za kłopot funkcji (metody nie są tak naprawdę częścią modelu obiektowego typu). Ale podobnie jak wszystkie funkcje, ktoś go potrzebuje.

Ponownie nie użyłbym tego do ukrywania się lub organizacji. Istnieją sposoby na ukrycie członków przed intellisense.

[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
codenheim
źródło
„Są wybitni autorzy / eksperci, którzy uważają to za kludge cechy”. Kto jest? Jaka jest proponowana alternatywa? Istnieje co najmniej jeden problem (tj. Specjalizacja metod interfejsu w klasie podinterface / implementacji), która wymagałaby dodania innej funkcji do C # w celu rozwiązania tego problemu przy braku jawnej implementacji (patrz ADO.NET i kolekcje ogólne) .
jhominal
@jhominal - CLR przez C # Jeffrey Richter specjalnie używa słowa kludge. C ++ sobie z tym radzi, programista musi jawnie odwoływać się do implementacji metody podstawowej, aby ujednoznacznić lub użyć rzutowania. Wspomniałem już, że alternatywy nie są zbyt atrakcyjne. Ale jest to aspekt pojedynczego dziedziczenia i interfejsów, a nie ogólnie OOP.
codenheim
Można również wspomnieć, że Java również nie ma jawnej implementacji, pomimo tego samego projektu „pojedynczego dziedziczenia implementacji + interfejsów”. Jednak w Javie typ zwracanej metody zastępowanej może być wyspecjalizowany, eliminując część potrzeby jawnej implementacji. (W języku C # podjęto inną decyzję projektową, prawdopodobnie częściowo z powodu włączenia właściwości).
jhominal
@jhominal - Jasne, że po kilku minutach zaktualizuję. Dzięki.
codenheim
Ukrywanie może być całkowicie odpowiednie w sytuacjach, w których klasa może implementować metodę interfejsu w sposób, który jest zgodny z umową interfejsu, ale nie jest użyteczny . Na przykład, chociaż nie podoba mi się idea klas, w których IDisposable.Disposecoś robi, ale ma inną nazwę Close, to uważam, że jest to całkowicie rozsądne dla klasy, której umowa wyraźnie wyklucza stwierdzenie, że kod może tworzyć i porzucać instancje bez wywoływania Disposejawnej implementacji interfejsu do zdefiniuj IDisposable.Disposemetodę, która nic nie robi.
supercat
5

Mogę być oznaką niechlujnego kodu, ale czasami prawdziwy świat jest bałagan. Jednym z przykładów .NET Framework jest ReadOnlyColection, który ukrywa ustawiającą mutację [], Dodaj i Ustaw.

IE ReadOnlyCollection implementuje IList <T>. Zobacz także moje pytanie do SO kilka lat temu, kiedy się nad tym zastanawiałem.

Esben Skov Pedersen
źródło
Cóż, to będzie również mój własny interfejs, więc nie ma sensu robić tego źle od samego początku.
Kyle Baran
2
Nie powiedziałbym, że ukrywa mutanta, ale raczej, że w ogóle go nie zapewnia. Lepszym przykładem może być ConcurrentDictionaryimplementacja różnych elementów IDictionary<T>jawnie, tak aby konsumenci ConcurrentDictionaryA) nie dzwonili do nich bezpośrednio, a B) mogą korzystać z metod, które wymagają implementacji, IDictionary<T>jeśli jest to absolutnie konieczne. Np. Użytkownicy ConcurrentDictionary<T> powinni zadzwonić TryAddzamiast Addunikać niepotrzebnych wyjątków.
Brian
Oczywiście Addjawne wdrożenie zmniejsza również bałagan. TryAddmożna zrobić wszystko, co Addmożna zrobić ( Addjest zaimplementowane jako wezwanie do TryAdd, więc zapewnienie obu byłoby zbędne). Jednak TryAddkoniecznie ma inną nazwę / podpis, więc nie jest możliwe wdrożenie IDictionarybez tej nadmiarowości. Jawna implementacja rozwiązuje ten problem w czysty sposób.
Brian
Z punktu widzenia konsumenta metody są ukryte.
Esben Skov Pedersen
1
Piszesz o IReadonlyCollection <T>, ale link do ReadOnlyCollection <T>. Pierwszy w interfejsie, drugi to klasa. IReadonlyCollection <T> nie implementuje IList <T>, nawet jeśli ReadonlyCollection <T> tak.
Jørgen Fogh
2

Jest to dobra forma, aby zrobić to, gdy członek jest bezcelowy w kontekście. Na przykład, jeśli utworzysz kolekcję tylko do odczytu, która implementuje się IList<T>przez delegowanie do obiektu wewnętrznego, _wrappedmożesz mieć coś takiego:

public T this[int index]
{
  get
  {
     return _wrapped[index];
  }
}
T IList<T>.this[int index]
{
  get
  {
    return this[index];
  }
  set
  {
    throw new NotSupportedException("Collection is read-only.");
  }
}
public int Count
{
  get { return _wrapped.Count; }
}
bool ICollection<T>.IsReadOnly
{
  get
  {
    return true;
  }
}

Tutaj mamy cztery różne przypadki.

public T this[int index]jest zdefiniowany przez naszą klasę, a nie przez interfejs, a zatem nie jest oczywiście jawną implementacją, choć zauważ, że jest podobny do odczytu-zapisu T this[int index]zdefiniowanego w interfejsie, ale jest tylko do odczytu.

T IList<T>.this[int index]jest jawny, ponieważ jedna jego część (getter) jest idealnie dopasowana do powyższej właściwości, a druga część zawsze zgłasza wyjątek. Chociaż jest to niezbędne dla kogoś, kto uzyskuje dostęp do instancji tej klasy za pośrednictwem interfejsu, nie ma sensu, aby ktoś używał jej za pośrednictwem zmiennej typu klasy.

Podobnie, ponieważ bool ICollection<T>.IsReadOnlyzawsze będzie zwracać wartość true, absolutnie bezcelowe jest pisanie kodu przeciw typowi klasy, ale może mieć to zasadnicze znaczenie przy używaniu go przez typ interfejsu, dlatego też implementujemy go jawnie.

I odwrotnie, public int Countnie jest jawnie zaimplementowany, ponieważ może potencjalnie być użyteczny dla kogoś, kto korzysta z instancji za pośrednictwem własnego typu.

Ale w twoim przypadku „bardzo rzadko używanym” bardzo mocno skłaniałbym się ku nieużywaniu wyraźnej implementacji.

W przypadkach, w których zalecam użycie jawnej implementacji wywołanie metody przez zmienną typu klasy byłoby albo błędem (próba użycia indeksowanego setera), albo bezcelowym (sprawdzenie wartości, która zawsze będzie taka sama), więc ukrywanie chronisz użytkownika przed błędnym lub nieoptymalnym kodem. To znacznie różni się od kodu, który Twoim zdaniem jest rzadko używany. W tym celu mógłbym rozważyć użycie tego EditorBrowsableatrybutu, aby ukryć członka przed inteligencją, choć i tak byłbym zmęczony; mózgi ludzi mają już własne oprogramowanie do filtrowania tego, co ich nie interesuje.

Jon Hanna
źródło
Jeśli implementujesz interfejs, zaimplementuj interfejs. W przeciwnym razie jest to wyraźne naruszenie zasady substytucji Liskowa.
Telastyn
@Telastyn interfejs jest rzeczywiście zaimplementowany.
Jon Hanna
Ale umowa nie jest spełniona - w szczególności „To jest coś, co reprezentuje zmienną kolekcję o swobodnym dostępie”.
Telastyn
1
@Telastyn spełnia udokumentowaną umowę, szczególnie w świetle „Kolekcja, która jest tylko do odczytu, nie pozwala na dodawanie, usuwanie ani modyfikowanie elementów po utworzeniu kolekcji”. Zobacz msdn.microsoft.com/en-us/library/0cfatk9t%28v=vs.110%29.aspx
Jon Hanna
@Telastyn: Jeśli umowa zawierała zmienność, to dlaczego interfejs miałby zawierać IsReadOnlywłaściwość?
nikie