Kiedy używać klas abstrakcyjnych zamiast interfejsów z metodami rozszerzenia w C #?

70

„Klasa abstrakcyjna” i „interfejs” są podobnymi pojęciami, a interfejs jest bardziej abstrakcyjny z tych dwóch. Jednym z czynników różnicujących jest to, że klasy abstrakcyjne zapewniają w razie potrzeby implementacje metod dla klas pochodnych. Jednak w języku C # ten czynnik różnicujący został zmniejszony dzięki niedawnemu wprowadzeniu metod rozszerzenia, które umożliwiają implementację metod interfejsu. Innym czynnikiem różnicującym jest to, że klasa może odziedziczyć tylko jedną klasę abstrakcyjną (tj. Nie ma wielokrotnego dziedziczenia), ale może implementować wiele interfejsów. To sprawia, że ​​interfejsy są mniej restrykcyjne i bardziej elastyczne. Więc w C #, kiedy powinniśmy używać klas abstrakcyjnych zamiast interfejsów z metodami rozszerzenia?

Godnym uwagi przykładem modelu interfejsu + metody rozszerzenia jest LINQ, w którym funkcjonalność zapytania jest zapewniona dla każdego typu, który implementuje IEnumerableza pomocą wielu metod rozszerzenia.

Gulszan
źródło
2
I możemy używać „Właściwości” również w interfejsach.
Gulshan
1
Brzmi raczej jak C # niż ogólne pytanie OOD. (Większość innych języków OO nie ma metod ani właściwości rozszerzeń.)
Péter Török
4
Metody rozszerzeń nie rozwiązują do końca problemu, który rozwiązują klasy abstrakcyjne, dlatego powód nie jest inny niż w Javie lub innym podobnym języku z pojedynczym dziedziczeniem.
Job
3
Potrzeba implementacji metod klasy abstrakcyjnej nie została zmniejszona przez metody rozszerzeń. Metody rozszerzeń nie mogą uzyskać dostępu do prywatnych pól członkowskich, ani nie mogą być deklarowane jako wirtualne i zastępowane w klasach pochodnych.
zooone9243,

Odpowiedzi:

63

Kiedy potrzebujesz części klasy do zaimplementowania. Najlepszym przykładem, jaki użyłem, jest wzorzec metody szablonu .

public abstract class SomethingDoer
{
    public void Do()
    {
        this.DoThis();
        this.DoThat();
    }

    protected abstract void DoThis();
    protected abstract void DoThat();
}

W ten sposób można zdefiniować kroki, które będą podejmowane po wywołaniu funkcji Do (), bez znajomości szczegółów ich implementacji. Klasy pochodne muszą implementować metody abstrakcyjne, ale nie metodę Do ().

Metody rozszerzeń niekoniecznie spełniają część równania „musi być częścią klasy”. Ponadto metody iirc, rozszerzenia nie mogą (jak się wydaje) mieć zasięg publiczny.

edytować

Pytanie jest bardziej interesujące niż pierwotnie przypisałem. Po dalszych badaniach Jon Skeet odpowiedział na takie pytanie w SO na korzyść użycia interfejsów + metod rozszerzenia. Ponadto, potencjał minusem jest przy użyciu odbicia przeciwko hierarchii obiektów zaprojektowanych w ten sposób.

Osobiście mam problem z dostrzeżeniem korzyści płynących ze zmiany obecnie powszechnej praktyki, ale widzę też kilka wad w tym zakresie.

Należy zauważyć, że można zaprogramować w ten sposób w wielu językach za pomocą klas Utility. Rozszerzenia tylko dostarczają cukier składniowy, aby metody wyglądały tak, jakby należały do ​​klasy.

Steven Evers
źródło
Pobij mnie do tego ..
Giorgi
1
Ale to samo można zrobić z interfejsami i metodami rozszerzenia. Tam metody, które definiujesz w abstrakcyjnej klasie bazowej, jak Do()zostaną zdefiniowane jako metoda rozszerzenia. Metody pozostawione do definicji klas pochodnych, takie jak, DoThis()będą członkami głównego interfejsu. Dzięki interfejsom możesz obsługiwać wiele implementacji / dziedziczenia. I zobacz to- odetocode.com/blogs/scott/archive/2009/10/05/…
Gulshan
@Gulshan: Ponieważ coś można zrobić na więcej niż jeden sposób, nie powoduje to, że wybrany sposób jest nieważny. Korzystanie z interfejsów nie ma żadnej przewagi. Sama klasa abstrakcyjna jest interfejsem. Nie potrzebujesz interfejsów do obsługi wielu implementacji.
ThomasX
Ponadto mogą występować wady modelu interfejsu + rozszerzenia. Weźmy na przykład bibliotekę Linq. To świetnie, ale jeśli trzeba go użyć tylko raz w tym pliku kodu, pełna biblioteka Linq będzie dostępna w IntelliSense na KAŻDYM liczniku w pliku kodu. Może to znacznie spowolnić działanie IDE.
KeithS
-1, ponieważ nie wykazałeś, że klasy abstrakcyjne lepiej pasują do metody szablonów niż interfejsy
Benjamin Hodgson
25

Chodzi o to , co chcesz modelować:

Klasy abstrakcyjne działają w oparciu o dziedziczenie . Będąc tylko specjalnymi klasami podstawowymi, modelują one pewien związek .

Na przykład pies jest zwierzęciem, więc mamy

class Dog : Animal { ... }

Ponieważ nie ma sensu tworzenie ogólnego zwierzęcia (jak by to wyglądało?), TworzymyAnimalklasę podstawową abstract- ale nadal jest to klasa podstawowa. A Dogklasa nie ma sensu bez bycia Animal.

Z drugiej strony interfejsy to inna historia. Nie używają dziedziczenia, ale zapewniają polimorfizm (który można również zaimplementować z dziedziczeniem). Nie modelują związku „ jest” , ale w większym stopniu obsługuje .

Weźmy np. IComparable- obiekt, który umożliwia porównanie z innym .

Funkcjonalność klasy nie zależy od implementowanych interfejsów, interfejs zapewnia jedynie ogólny sposób dostępu do tej funkcji. Moglibyśmy wciąż Disposetematyce Graphicslub FileStreambez konieczności ich implementacji IDisposable. Interfejs tylko odnosi się do sposobów ze sobą.

Zasadniczo można dodawać i usuwać interfejsy podobnie jak rozszerzenia bez zmiany zachowania klasy, po prostu wzbogacając dostęp do niej. Nie możesz jednak zabrać klasy podstawowej, ponieważ obiekt stałby się bez znaczenia!

Dario
źródło
Kiedy zdecydujesz się zachować abstrakcję klasy podstawowej?
Gulshan
1
@Gulshan: Jeśli z jakiegoś powodu nie ma sensu tworzenie instancji klasy podstawowej. Możesz „stworzyć” Chihuahua lub białego rekina, ale nie możesz stworzyć zwierzęcia bez dalszej specjalizacji - w ten sposób stworzyłbyś Animalstreszczenie. Umożliwia to pisanie abstractfunkcji specjalizowanych przez klasy pochodne. Lub więcej w zakresie programowania - to jest chyba nie ma sensu, aby utworzyć UserControl, ale jak Button toUserControl , więc mamy ten sam rodzaj relacji klasa / klasy bazowej.
Dario
jeśli masz metody abstrakcyjne, niż masz klasę abstrakcyjną. I abstrakcyjne metody, które masz, gdy nie ma wartości dla ich implementacji (jak „idź” dla zwierząt). I oczywiście czasami nie chcesz zezwalać na obiekty jakiejś klasy ogólnej, niż czynisz to abstrakcyjnym.
Dainius
Nie ma żadnej reguły, która mówi, że jeśli gramatycznie i semantycznie sensownie jest powiedzieć „B to A”, to A musi być klasą abstrakcyjną. Powinieneś preferować interfejsy, dopóki naprawdę nie będziesz mógł uniknąć klasy. Udostępnianie kodu można wykonać poprzez kompozycję interfejsów itp. Ułatwia to uniknięcie malowania się w kącie z dziwnymi drzewami dziedziczenia.
sara
3

Właśnie zdałem sobie sprawę, że od lat nie korzystałem z klas abstrakcyjnych (przynajmniej nie stworzonych).

Klasa abstrakcyjna to dość dziwna bestia między interfejsem a implementacją. W klasach abstrakcyjnych jest coś, co mrowi mój zmysł pająka. Twierdziłbym, że nie należy „ujawniać” implementacji za interfejsem w żaden sposób (ani dokonywać żadnych powiązań).

Nawet jeśli istnieje potrzeba wspólnego zachowania między klasami implementującymi interfejs, użyłbym raczej niezależnej klasy pomocniczej niż klasy abstrakcyjnej.

Wybrałbym interfejsy.

Maglob
źródło
2
-1: Nie jestem pewien, ile masz lat, ale możesz chcieć nauczyć się wzorców projektowych, szczególnie wzorca metody szablonów. Wzory projektowe uczynią cię lepszym programistą i zakończą twoją ignorancję klas abstrakcyjnych.
Jim G.
To samo dotyczy mrowienia. Klasy, pierwsza i najważniejsza funkcja C # i Java, są funkcją do tworzenia typu i natychmiastowego łączenia go na zawsze z jedną implementacją.
Jesse Millikan
2

IMHO, myślę, że tutaj jest mieszanie pojęć.

Klasy używają pewnego rodzaju dziedziczenia, podczas gdy interfejsy używają innego rodzaju dziedziczenia.

Oba rodzaje dziedziczenia są ważne i przydatne, a każdy z nich ma swoje zalety i wady.

Zacznijmy od początku.

Historia z interfejsami zaczyna się dawno temu w C ++. W połowie lat 80., kiedy opracowano C ++, idea interfejsów jako innego rodzaju nie została jeszcze zrealizowana (przyjdzie z czasem).

C ++ miał tylko jeden rodzaj dziedziczenia, rodzaj dziedziczenia używany przez klasy, zwany dziedziczeniem implementacji.

Aby móc zastosować zalecenie GoF Book: „Aby zaprogramować interfejs, a nie implementację”, C ++ obsługiwał klasy abstrakcyjne i wielokrotne dziedziczenie implementacji, aby móc wykonywać interfejsy w języku C # (i przydatne!) .

C # wprowadza nowy rodzaj, interfejsy i nowy rodzaj dziedziczenia, dziedziczenie interfejsu, aby oferować co najmniej dwa różne sposoby obsługi „Programowania interfejsu, a nie implementacji”, z jednym ważnym zastrzeżeniem: tylko C # obsługuje wielokrotne dziedziczenie z dziedziczeniem interfejsu (a nie z dziedziczeniem implementacji).

Architektonicznymi powodami interfejsów w języku C # jest więc zalecenie GoF.

Mam nadzieję, że to może pomóc.

Z pozdrowieniami, Gastón

Gastón E. Nusimovich
źródło
1

Zastosowanie metod rozszerzenia do interfejsu jest przydatne do zastosowania wspólnego zachowania w klasach, które mogą mieć tylko wspólny interfejs. Takie klasy mogą już istnieć i mogą być zamknięte na rozszerzenia za pomocą innych środków.

W przypadku nowych projektów wolałbym stosować metody rozszerzeń na interfejsach. W przypadku hierarchii typów metody rozszerzenia umożliwiają przedłużenie zachowania bez konieczności otwierania całej hierarchii w celu modyfikacji.

Jednak metody rozszerzeń nie mogą uzyskać dostępu do prywatnej implementacji, więc na szczycie hierarchii, jeśli muszę obudować prywatną implementację za interfejsem publicznym, klasa abstrakcyjna byłaby jedynym sposobem.

Ed James
źródło
Nie, to nie jedyny bezpieczny sposób. Jest inny bezpieczniejszy sposób. To są metody rozszerzenia. Klienci w ogóle nie zostaną zainfekowani. Dlatego wspomniałem o interfejsach i metodach rozszerzenia.
Gulshan
@Ed James Myślę, że „mniej wydajny” = „bardziej elastyczny” Kiedy wybrałbyś mocniejszą klasę abstrakcyjną zamiast bardziej elastycznego interfejsu?
Gulshan
@Ed James W metodach rozszerzenia asp.mvc zastosowano od samego początku. Co myślicie o tym?
Gulshan
0

Myślę, że w C # wielokrotne dziedziczenie nie jest obsługiwane i dlatego koncepcja interfejsu została wprowadzona w C #.

W przypadku klas abstrakcyjnych wielokrotne dziedziczenie również nie jest obsługiwane. Łatwo jest więc zdecydować się na użycie abstrakcyjnych klas lub interfejsów.

Początkujący
źródło
Mówiąc ściślej, są to wyłącznie abstrakcyjne klasy, które w języku C # nazywane są „interfejsami”.
Johan Boulé