Czy ma sens definiowanie interfejsu, jeśli mam już klasę abstrakcyjną?

12

Mam klasę z pewnymi domyślnymi / współdzielonymi funkcjami. Używam abstract classdo tego:

public interface ITypeNameMapper
{
    string Map(TypeDefinition typeDefinition);
}

public abstract class TypeNameMapper : ITypeNameMapper
{
    public virtual string Map(TypeDefinition typeDefinition)
    {
        if (typeDefinition is ClassDefinition classDefinition)
        {
            return Map(classDefinition);
        }
        ...

        throw new ArgumentOutOfRangeException(nameof(typeDefinition));
    }

    protected abstract string Map(ClassDefinition classDefinition);
}

Jak widać, mam również interfejs ITypeNameMapper. Czy zdefiniowanie tego interfejsu ma sens, jeśli mam już klasę abstrakcyjną TypeNameMapperlub abstract classwystarczy?

TypeDefinition w tym minimalnym przykładzie jest również abstrakcyjny.

Konrad
źródło
4
Po prostu FYI - w OOD sprawdzanie typów w kodzie oznacza, że ​​hierarchia klas jest niepoprawna - nakazuje tworzenie klas pochodnych na podstawie sprawdzanego typu. W takim przypadku potrzebny jest interfejs ITypeMapper, który określa metodę Map (), a następnie implementuje ten interfejs zarówno w klasach TypeDefinition, jak i ClassDefinition. W ten sposób Twój ClassAssembler po prostu iteruje listę ITypeMappers, wywołując Map () na każdym z nich, i pobiera pożądany ciąg znaków z obu typów, ponieważ każdy z nich ma własną implementację.
Rodney P. Barbati
1
Używanie klasy bazowej zamiast interfejsu, o ile nie jest to absolutnie konieczne, nazywa się „wypalaniem bazy”. Jeśli można to zrobić za pomocą interfejsu, zrób to za pomocą interfejsu. Klasa abstrakcyjna staje się pomocnym dodatkiem. artima.com/intv/dotnet.html W szczególności zdalne sterowanie wymaga korzystania z MarshalByRefObject, więc jeśli chcesz zostać przeniesiony, nie możesz czerpać z niczego innego.
Ben
@ RodneyP.Barbati nie będzie działać, jeśli chcę mieć wielu twórców map dla tego samego typu, które mogę przełączać w zależności od sytuacji
Konrad
1
@Ben pali bazę, która jest interesująca. Dzięki za klejnot!
Konrad
@Konrad To jest niepoprawne - nic nie stoi na przeszkodzie, aby zaimplementować interfejs, wywołując inną implementację interfejsu, dlatego każdy typ może mieć implementację wtykową, którą można ujawnić za pośrednictwem implementacji tego typu.
Rodney P. Barbati,

Odpowiedzi:

31

Tak, ponieważ C # nie zezwala na wielokrotne dziedziczenie oprócz interfejsów.

Więc jeśli mam klasę, która jest zarówno TypeNameMapper, jak i SomethingelseMapper, mogę:

class MultiFunctionalClass : ITypeNameMapper, ISomethingelseMapper 
{
    private TypeNameMapper map1
    private SomethingelseMapper map2

    public string Map(TypeDefinition typeDefinition) { return map1.Map(typeDefintion);}

    public string Map(OtherDef otherDef) { return map2.Map(orderDef); }
}
Ewan
źródło
4
@Liath: Pojedyncza odpowiedzialność jest w rzeczywistości czynnikiem przyczyniającym się do tego, dlaczego konieczne jest dziedziczenie. Rozważmy trzy możliwe cechy: ICanFly, ICanRun, ICanSwim. Musisz stworzyć 8 klas stworzeń, po jednej dla każdej kombinacji cech (RRRR, YYN, YNY, ..., NNY, NNN). SRP nakazuje, abyś zaimplementował każde zachowanie (latanie, bieganie, pływanie), a następnie zaimplementował je ponownie na 8 stworzeniach. Kompozycja byłaby tutaj jeszcze lepsza, ale ignorując kompozycję na sekundę, powinieneś już widzieć potrzebę dziedziczenia / implementacji wielu osobnych zachowań wielokrotnego użytku z powodu SRP.
Flater
4
@Konrad o to mi chodzi. Jeśli napiszesz interfejs i nigdy go nie potrzebujesz, koszt to 3 LoC. Jeśli nie napiszesz interfejsu i stwierdzisz, że go potrzebujesz, kosztem może być refaktoryzacja całej aplikacji.
Ewan
3
(1) Implementacja interfejsów jest dziedziczeniem? (2) Zarówno pytanie, jak i odpowiedź powinny zostać zakwalifikowane. "To zależy." (3) Wielokrotne dziedziczenie wymaga naklejki ostrzegawczej. (4) Mógłbym pokazać ci krytyczne błędy formalne K interface.. nadmiarowe implementacje, które powinny znajdować się w bazie - które ewoluują niezależnie !, warunkowe zmiany logiczne napędzające to śmieci z powodu braku metod szablonów, zepsutego hermetyzacji, nieistniejącej abstrakcji. Moje ulubione: klasy 1-metodowe, z których każda implementuje własną metodę 1 interface, każda w / tylko jedna instancja. ... itd. itd. itd.
radarbob
3
W kodzie jest niewidoczny współczynnik tarcia. Czujesz to, gdy musisz czytać zbędne interfejsy. Potem pokazuje, jak się nagrzewa, jak prom kosmiczny uderza w atmosferę, gdy implementacja interfejsu przenika do klasy abstrakcyjnej. To Strefa Zmierzchu, a to Challenger wahadłowca. Akceptując swój los, obserwuję szalejącą plazmę z nieustraszonym, oderwanym zdumieniem. Bezlitosne tarcie rozrywa fasadę perfekcji, odkładając rozbitego kadłuba do produkcji z grzmiącym grzmotem.
radarbob
4
@radarbob Poetic. ^ _ ^ Zgodziłbym się; koszt interfejsów jest niski, ale nie istnieje. Nie tyle w ich pisaniu, ale w sytuacji debugowania są jeszcze 1-2 kroki, aby dostać się do faktycznego zachowania, które mogą szybko zsumować się, gdy osiągniesz głębokość kilkudziesięciu poziomów abstrakcji. W bibliotece zwykle jest tego warte. Ale w aplikacji refaktoryzacja jest lepsza niż przedwczesne uogólnienie.
Errorsatz
1

Interfejsy i klasy abstrakcyjne służą różnym celom:

  • Interfejsy definiują interfejsy API i należą do klientów, a nie do implementacji.
  • Jeśli klasy współużytkują implementacje, możesz skorzystać z klasy abstrakcyjnej .

W twoim przykładzie interface ITypeNameMapperokreśla potrzeby klientów i abstract class TypeNameMappernie dodaje żadnej wartości.

Geoffrey Hale
źródło
2
Klasy abstrakcyjne należą również do klientów. Nie wiem co przez to rozumiesz. Wszystko, co publicnależy do klienta. TypeNameMapperdodaje wiele wartości, ponieważ oszczędza implementatorowi napisanie wspólnej logiki.
Konrad
2
To, czy interfejs jest przedstawiany jako, interfaceczy nie, jest istotne tylko w tym, że C # zabrania pełnego wielokrotnego dziedziczenia.
Deduplicator
0

Cała koncepcja interfejsów została stworzona w celu wspierania rodziny klas posiadających wspólne API.

To wyraźnie mówi, że każde użycie interfejsu oznacza (lub oczekuje się, że) będzie więcej niż jedną implementację jego specyfikacji.

Frameworki DI zamulają wody tutaj, ponieważ wiele z nich wymaga interfejsu, chociaż będzie zawsze tylko jedna implementacja - dla mnie jest to nieracjonalne obciążenie ogólne, ponieważ w większości przypadków jest to po prostu bardziej złożony i wolniejszy sposób wywoływania nowego, ale to jest co to jest.

Odpowiedź leży w samym pytaniu. Jeśli masz klasę abstrakcyjną, przygotowujesz się do utworzenia więcej niż jednej klasy pochodnej ze wspólnym interfejsem API. Dlatego użycie interfejsu jest wyraźnie wskazane.

Rodney P. Barbati
źródło
2
„Jeśli masz klasę abstrakcyjną, ... użycie interfejsu jest wyraźnie wskazane.” - Mój wniosek jest odwrotny: jeśli masz klasę abstrakcyjną, użyj jej tak, jakby to był interfejs (jeśli DI nie bawi się nią, rozważ inną implementację). Utwórz interfejs tylko wtedy, gdy naprawdę nie działa.
maaartinus
1
Ditto, @maaartinus. I nawet „… jakby to był interfejs”. Publiczni członkowie klasy to interfejs. To samo dotyczy „możesz to zrobić później”; dotyczy to sedna podstaw projektowania 101.
radarbob
@maaartinus: Jeśli ktoś tworzy interfejs od samego początku, ale używa odniesień typu klasy abstrakcyjnej w całym miejscu, istnienie interfejsu nic nie pomoże. Jeśli jednak kod klienta używa typu interfejsu zamiast typu klasy abstrakcyjnej, gdy tylko jest to możliwe, znacznie to ułatwi, jeśli konieczne będzie stworzenie implementacji, która różni się wewnętrznie od klasy abstrakcyjnej. Zmiana kodu klienta w tym momencie, aby korzystać z interfejsu, będzie o wiele bardziej bolesna niż zaprojektowanie kodu klienta, aby robił to od samego początku.
supercat
1
@ supercat Zgadzam się, zakładając, że piszesz bibliotekę. Jeśli jest to aplikacja, nie widzę żadnego problemu zastępującego wszystkie wystąpienia jednocześnie. Moje środowisko Eclipse może to zrobić (Java, a nie C #), ale jeśli nie, jest to po prostu find ... -exec perl -pi -e ...naprawienie trywialnych błędów kompilacji. Chodzi mi o to: korzyść z posiadania (obecnie) bezużytecznej rzeczy jest znacznie większa niż możliwe przyszłe oszczędności.
maaartinus
Pierwsze zdanie byłoby poprawne, jeśli zaznaczyłeś interfacejako kod (mówisz o C # - interfacesach, a nie abstrakcyjnym interfejsie, jak każdy zestaw klas / funkcji / itp.), I zakończyłeś je "... ale wciąż zakazujesz pełnej wielokrotności dziedzictwo." Nie ma potrzeby, interfacejeśli masz MI.
Deduplicator
0

Jeśli chcemy wyraźnie odpowiedzieć na pytanie, autor mówi: „Czy sensowne jest zdefiniowanie tego interfejsu, jeśli mam już klasę abstrakcyjną TypeNameMapper lub klasa abstrakcyjna wystarczy?”

Odpowiedź brzmi: tak i nie - tak, powinieneś utworzyć interfejs, nawet jeśli masz już abstrakcyjną klasę podstawową (ponieważ nie powinieneś odnosić się do abstrakcyjnej klasy bazowej w żadnym kodzie klienta) i nie, ponieważ nie powinieneś był tworzyć abstrakcyjna klasa bazowa przy braku interfejsu.

Oczywiste jest, że nie włożyłeś wystarczającej uwagi w API, które próbujesz zbudować. Najpierw wymyśl interfejs API, a następnie w razie potrzeby zapewnij częściową implementację. Fakt, że twoja abstrakcyjna klasa bazowa sprawdza kod w kodzie, wystarcza, aby powiedzieć, że nie jest to właściwa abstrakcja.

Podobnie jak w większości rzeczy w OOD, kiedy jesteś w ruchu i dobrze wykonujesz swój model obiektowy, twój kod poinformuje cię, co będzie dalej. Zacznij od interfejsu, zaimplementuj ten interfejs w potrzebnych klasach. Jeśli zauważysz, że piszesz podobny kod, wyodrębnij go do abstrakcyjnej klasy bazowej - ważny jest interfejs, abstrakcyjna klasa podstawowa jest tylko pomocnikiem i może istnieć więcej niż jedna.

Rodney P. Barbati
źródło
-1

Odpowiedź na to pytanie jest trudna bez znajomości reszty aplikacji.

Zakładając, że używasz jakiegoś modelu DI i zaprojektowałeś swój kod tak, aby był możliwie jak najbardziej rozszerzalny (abyś mógł TypeNameMapperłatwo dodawać nowe ), powiedziałbym tak.

Powody dla:

  • Skutecznie dostajesz go za darmo, ponieważ klasa podstawowa implementuje to interface, czego nie musisz się martwić w żadnych implementacjach potomnych
  • Większość frameworków IoC i bibliotek Mocking oczekuje interfaces. Chociaż wiele z nich pracuje z klasami abstrakcyjnymi, nie zawsze jest to pewne i jestem wielkim zwolennikiem podążania tą samą ścieżką, co pozostałe 99% użytkowników biblioteki, tam gdzie to możliwe.

Powody przeciwko:

  • Ściśle mówiąc (jak już zauważyłeś) NAPRAWDĘ nie musisz
  • Jeśli class/ interfacezmienia się, pojawia się trochę dodatkowego obciążenia

Biorąc wszystko pod uwagę, powiedziałbym, że powinieneś stworzyć interface. Przyczyny są dość znikome, ale chociaż możliwe jest użycie abstrakcji w kpinach i IoC, jest to o wiele łatwiejsze dzięki interfejsom. Ostatecznie jednak nie skrytykowałbym kodu kolegi, gdyby poszedł w drugą stronę.

Liath
źródło