Czy abstrakcyjne klasy / metody są przestarzałe?

37

Kiedyś tworzyłem wiele abstrakcyjnych klas / metod. Potem zacząłem używać interfejsów.

Teraz nie jestem pewien, czy interfejsy nie powodują, że klasy abstrakcyjne stają się przestarzałe.

Potrzebujesz w pełni abstrakcyjnej klasy? Zamiast tego utwórz interfejs. Potrzebujesz abstrakcyjnej klasy z pewną implementacją? Utwórz interfejs, stwórz klasę. Dziedzicz klasę, zaimplementuj interfejs. Dodatkową korzyścią jest to, że niektóre klasy mogą nie potrzebować klasy nadrzędnej, a jedynie zaimplementować interfejs.

Czy zatem abstrakcyjne klasy / metody są przestarzałe?

Borys Jankow
źródło
Co powiesz na to, że wybrany język programowania nie obsługuje interfejsów? Wydaje mi się, że tak jest w przypadku C ++.
Bernard
7
@Bernard: w C ++ klasa abstrakcyjna jest interfejsem we wszystkim oprócz nazwy. To, że mogą robić więcej niż „czyste” interfejsy, nie jest wadą.
gbjbaanb
@gbjbaanb: Przypuszczam. Nie przypominam sobie używania ich jako interfejsów, ale raczej do zapewnienia domyślnych implementacji.
Bernard
Interfejsy są „walutą” odniesień do obiektów. Mówiąc ogólnie, są one podstawą zachowania polimorficznego. Klasy abstrakcyjne służą innym celom, które Deadalnix doskonale wyjaśnia.
jiggy
4
Czy to nie jest powiedzenie „czy środki transportu są przestarzałe, teraz mamy samochody?” Tak, przez większość czasu korzystasz z samochodu. Ale niezależnie od tego, czy potrzebujesz czegoś innego niż samochód, czy nie, tak naprawdę nie byłoby słuszne powiedzenie „Nie muszę używać środków transportu”. Interfejs jest prawie taki sam jak klasa abstrakcyjna bez żadnej implementacji i ze specjalną nazwą, nie?
Jack V.

Odpowiedzi:

111

Nie.

Interfejsy nie mogą zapewniać domyślnej implementacji, klasy abstrakcyjne i metoda mogą. Jest to szczególnie przydatne, aby uniknąć powielania kodu w wielu przypadkach.

Jest to również bardzo dobry sposób na zmniejszenie sprzężenia sekwencyjnego. Bez metody / klas abstrakcyjnych nie można wdrożyć wzorca metody szablonu. Proponuję zajrzeć do tego artykułu z Wikipedii: http://en.wikipedia.org/wiki/Template_method_pattern

deadalnix
źródło
3
@deadalnix Szablon metody szablonu może być dość niebezpieczny. Możesz łatwo skończyć z wysoce sprzężonym kodem i zacząć hakować kod, aby rozszerzyć szablonową klasę abstrakcyjną do obsługi „jeszcze jednego przypadku”.
quant_dev
9
To bardziej przypomina anty-wzór. Możesz uzyskać dokładnie to samo z kompozycją względem interfejsu. To samo dotyczy klas cząstkowych z niektórymi metodami abstrakcyjnymi. A następnie zmuszając kod klienta do podklasy i zastąpić je, trzeba mieć wdrożenie być wstrzykiwany . Zobacz: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos
4
To wcale nie wyjaśnia WIĘCEJ, jak zmniejszyć sprzężenie sekwencyjne. Zgadzam się, że istnieje kilka sposobów na osiągnięcie tego celu, ale proszę odpowiedz na problem, o którym mówisz, zamiast ślepo powoływać się na jakąś zasadę. Skończysz programowanie kultu ładunku, jeśli nie potrafisz wyjaśnić, dlaczego jest to związane z rzeczywistym problemem.
deadalnix
3
@deadalnix: Mówienie, że sekwencyjne sprzężenie jest najlepiej redukowane za pomocą wzorca szablonu, jest programowaniem kultowym ładunku (przy użyciu wzoru stan / strategia / fabryka może również działać), podobnie jak założenie, że zawsze musi być zmniejszone. Sprzężenie sekwencyjne jest często jedynie konsekwencją ujawnienia bardzo drobnoziarnistej kontroli nad czymś, co oznacza jedynie kompromis. To wciąż nie znaczy, że nie mogę napisać opakowania, które nie ma sprzężenia sekwencyjnego przy użyciu kompozycji. W rzeczywistości jest to o wiele łatwiejsze.
back2dos
4
Java ma teraz domyślne implementacje interfejsów. Czy to zmienia odpowiedź?
raptortech97,
15

Istnieje metoda abstrakcyjna, więc możesz wywoływać ją z klasy bazowej, ale implementować ją w klasie pochodnej. Twoja klasa podstawowa wie:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Jest to dość przyjemny sposób na wykonanie dziury w środkowym wzorze bez wiedzy klasy pochodnej na temat działań związanych z konfiguracją i czyszczeniem. Bez abstrakcyjnych metod, trzeba polegać na klasę pochodną realizacji DoTaski pamiętając, aby zadzwonić base.DoSetup()i base.DoCleanup()przez cały czas.

Edytować

Ponadto, dzięki deadalnix za opublikowanie linku do Wzorca Metody Szablonu , co opisałem powyżej, bez znajomości nazwy. :)

Scott Whitlock
źródło
2
+1, wolę twoją odpowiedź od deadalnix '. Należy pamiętać, że można wdrożyć metodę szablonu w sposób bardziej prosto do przodu za pomocą delegatów (w języku C #):public void DoTask(Action doWork)
Jn
1
@Joh - prawda, ale niekoniecznie tak ładnie, w zależności od interfejsu API. Jeśli twoja klasa podstawowa jest, Fruita twoja klasa pochodna jest Apple, chcesz zadzwonić myApple.Eat(), a nie myApple.Eat((a) => howToEatApple(a)). Nie chcesz Appleteż dzwonić base.Eat(() => this.howToEatMe()). Myślę, że czystsze jest po prostu zastąpienie metody abstrakcyjnej.
Scott Whitlock,
1
@ Scott Whitlock: Twój przykład używania jabłek i owoców jest zbyt oderwany od rzeczywistości, aby ocenić, czy wybranie dziedziczenia jest lepsze niż ogólnie dla delegatów. Oczywiście odpowiedź brzmi „to zależy”, więc pozostawia wiele miejsca na debaty ... W każdym razie uważam, że metody szablonów występują często podczas refaktoryzacji, np. Podczas usuwania zduplikowanego kodu. W takich przypadkach zwykle nie chcę zadzierać z hierarchią typów i wolę unikać dziedziczenia. Ten rodzaj operacji jest łatwiejszy do wykonania z lambdas.
Joh
To pytanie ilustruje coś więcej niż proste zastosowanie wzorca metody szablonu - aspekt publiczny / niepubliczny jest znany w społeczności C ++ jako wzorzec interfejsu niebędącego interfejsem wirtualnym . Mając publiczną funkcję inną niż wirtualna, owiń wirtualną funkcję - nawet jeśli początkowo pierwsza nie robi nic poza wywoływaniem drugiej - pozostawiasz punkt dostosowania do konfiguracji / czyszczenia, logowania, profilowania, kontroli bezpieczeństwa itp., Które mogą być potrzebne później.
Tony
10

Nie, nie są przestarzałe.

W rzeczywistości istnieje niejasna, ale fundamentalna różnica między klasami / metodami abstrakcyjnymi a interfejsami.

jeśli zbiór klas, w których jedna z nich musi być użyta, mają wspólne zachowanie, które dzielą (klasy powiązane, to znaczy), to przejdź do klas / metod abstrakcyjnych.

Przykład: urzędnik, urzędnik, dyrektor - wszystkie te klasy mają wspólną funkcję CalculateSalary (), używają abstrakcyjnych klas podstawowych.CalculateSalary () mogą być różnie zaimplementowane, ale na przykład istnieją inne rzeczy, takie jak GetAttendance (), które mają wspólną definicję w klasie podstawowej.

Jeśli w twoich klasach nie ma nic wspólnego (klasy niepowiązane, w wybranym kontekście) między nimi, ale ma działanie, które różni się znacznie pod względem implementacji, przejdź do interfejsu.

Przykład: klasy nie związane z krowami, ławkami, samochodami, telesope, ale Isortable może tam być, aby posortować je w tablicy.

Różnica ta jest zwykle ignorowana, gdy podchodzi się do niej z perspektywy polimorficznej. Ale osobiście uważam, że są sytuacje, w których jeden jest odpowiedni od drugiego z powodu wyjaśnionego powyżej.

WinW
źródło
6

Oprócz innych dobrych odpowiedzi istnieje zasadnicza różnica między interfejsami a klasami abstrakcyjnymi, o której nikt specjalnie nie wspomniał, a mianowicie, że interfejsy są znacznie mniej niezawodne i dlatego nakładają znacznie większe obciążenie testowe niż klasy abstrakcyjne. Na przykład rozważ ten kod C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Gwarantuję, że muszę przetestować tylko dwie ścieżki kodu. Autor Frobbit może polegać na fakcie, że frobber jest czerwony lub zielony.

Jeśli zamiast tego mówimy:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Teraz nie wiem absolutnie nic o skutkach tego wezwania do Froba. Muszę mieć pewność, że cały kod we Frobbit jest odporny na wszelkie możliwe implementacje IFrobbera , nawet implementacje osób niekompetentnych (źle) lub aktywnie wrogo nastawionych do mnie lub moich użytkowników (znacznie gorzej).

Klasy abstrakcyjne pozwalają uniknąć tych wszystkich problemów; Użyj ich!

Eric Lippert
źródło
1
Problem, o którym mówisz, powinien zostać rozwiązany za pomocą algebraicznych typów danych, zamiast zginania klas do punktu, w którym naruszają zasadę otwarcia / zamknięcia.
back2dos
1
Po pierwsze, nigdy nie powiedziałem, że jest to dobre lub moralne . Wkładasz to do moich ust. Po drugie (o co mi chodzi): to tylko marna wymówka dla brakującej funkcji językowej. Wreszcie istnieje rozwiązanie bez klas abstrakcyjnych: pastebin.com/DxEh8Qfz . W przeciwieństwie do tego, twoje podejście wykorzystuje zagnieżdżone i abstrakcyjne klasy, wrzucając wszystko oprócz kodu wymagającego bezpieczeństwa razem w wielką kulę błota. Nie ma dobrego powodu, dla którego RedFrobber lub GreenFrobber powinny być powiązane z ograniczeniami, które chcesz egzekwować. Zwiększa sprzężenie i blokuje wiele decyzji bez żadnych korzyści.
back2dos
1
Mówienie, że metody abstrakcyjne są przestarzałe, jest bez wątpienia błędne, ale twierdzenie z drugiej strony, że powinny być one lepsze niż interfejsy, jest mylące. Są po prostu narzędziem do rozwiązywania innych problemów niż interfejsy.
Groo,
2
„istnieje zasadnicza różnica [...] interfejsy są znacznie mniej niezawodne [...] niż klasy abstrakcyjne”. Nie zgadzam się, że różnica, którą zilustrowałeś w swoim kodzie, opiera się na ograniczeniach dostępu, które, jak sądzę, nie mają żadnego powodu, aby znacząco różnić się między interfejsami a klasami abstrakcyjnymi. Może być tak w C #, ale pytanie jest niezależne od języka.
Joh
1
@ back2dos: Chciałbym tylko zauważyć, że rozwiązanie Erica faktycznie używa algebraicznego typu danych: klasa abstrakcyjna jest rodzajem sumy. Wywołanie metody abstrakcyjnej jest takie samo, jak dopasowanie wzorca dla zestawu wariantów.
Rodrick Chapman
4

Sam to powiesz:

Potrzebujesz abstrakcyjnej klasy z pewną implementacją? Utwórz interfejs, stwórz klasę. Dziedzicz klasę, zaimplementuj interfejs

brzmi to dużo pracy w porównaniu do „dziedziczenia klasy abstrakcyjnej”. Możesz wykonać pracę dla siebie, podchodząc do kodu z „purystycznego” punktu widzenia, ale uważam, że mam już dość do zrobienia, nie próbując zwiększać obciążenia bez praktycznych korzyści.

gbjbaanb
źródło
Nie wspominając już o tym, że jeśli klasa nie dziedziczy interfejsu (co nie zostało wspomniane, powinno), musisz napisać funkcje przekazywania w niektórych językach.
Sjoerd
Umm, dodatkowy „dużo pracy”, o którym wspomniałeś, to to, że stworzyłeś interfejs i kazałeś go zaimplementować - czy to naprawdę wydaje się dużo pracy? Poza tym interfejs daje oczywiście możliwość implementacji interfejsu z nowej hierarchii, która nie działałaby z abstrakcyjną klasą bazową.
Bill K
4

Jak skomentowałem w poście @deadnix: Częściowe implementacje są anty-wzorcem, pomimo tego, że wzór szablonu je formalizuje.

Czyste rozwiązanie dla tego przykładu wikipedii wzorca szablonu :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

To rozwiązanie jest lepsze, ponieważ wykorzystuje kompozycję zamiast dziedziczenia . Wzorzec szablonu wprowadza zależność między wdrożeniem reguł Monopoly a wdrożeniem sposobu uruchamiania gier. Są to jednak dwie zupełnie różne obowiązki i nie ma żadnego dobrego powodu, aby je łączyć.

back2dos
źródło
+1. Na marginesie: „Częściowe implementacje są anty-wzorcem, pomimo tego, że wzór szablonu je formalizuje”. Opis na wikipedii jasno określa wzorzec, tylko przykład kodu jest „zły” (w tym sensie, że wykorzystuje dziedziczenie, gdy nie jest potrzebne i istnieje prostsza alternatywa, jak pokazano powyżej). Innymi słowy, nie sądzę, by sam wzorzec był winny, tylko sposób, w jaki ludzie go wdrażają.
Joh
2

Nie. Nawet proponowana alternatywa obejmuje użycie klas abstrakcyjnych. Ponadto, ponieważ nie określiłeś języka, idę dalej i powiem, że kod ogólny jest lepszą opcją niż kruche dziedziczenie. Klasy abstrakcyjne mają znaczącą przewagę nad interfejsami.

DeadMG
źródło
-1. Nie rozumiem, co „kod ogólny a dziedziczenie” ma wspólnego z tym pytaniem. Powinieneś zilustrować lub uzasadnić, dlaczego „klasy abstrakcyjne mają znaczącą przewagę nad interfejsami”.
Joh
1

Klasy abstrakcyjne nie są interfejsami. Są to klasy, których nie można utworzyć.

Potrzebujesz w pełni abstrakcyjnej klasy? Zamiast tego utwórz interfejs. Potrzebujesz abstrakcyjnej klasy z pewną implementacją? Utwórz interfejs, stwórz klasę. Dziedzicz klasę, zaimplementuj interfejs. Dodatkową korzyścią jest to, że niektóre klasy mogą nie potrzebować klasy nadrzędnej, a jedynie zaimplementować interfejs.

Ale wtedy miałbyś nie abstrakcyjną, bezużyteczną klasę. Do wypełnienia dziury funkcjonalnej w klasie podstawowej wymagane są metody abstrakcyjne.

Na przykład biorąc pod uwagę tę klasę

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Jak zaimplementowałbyś tę podstawową funkcjonalność w klasie, która nie ma Frob()ani IsFrobbingNeeded?

konfigurator
źródło
1
Interfejsu nie można również utworzyć.
Sjoerd
@Sjoerd: Ale potrzebujesz klasy podstawowej ze wspólną implementacją; to nie może być interfejs.
konfigurator
1

Jestem twórcą frameworka serwletu, w którym klasy abstrakcyjne odgrywają istotną rolę. Powiedziałbym więcej: potrzebuję metod pół abstrakcyjnych, kiedy metoda musi zostać zastąpiona w 50% przypadków i chciałbym zobaczyć ostrzeżenie kompilatora, że ​​ta metoda nie została zastąpiona. Rozwiązuję problem dodając adnotacje. Wracając do pytania, istnieją dwa różne przypadki użycia klas abstrakcyjnych i interfejsów, a do tej pory nikt nie jest przestarzały.

Dmitriy R.
źródło
0

Nie sądzę, że są one przestarzałe przez interfejsy, ale mogą być przestarzałe ze względu na strategię.

Podstawowym zastosowaniem klasy abstrakcyjnej jest odroczenie części implementacji; mówiąc „ta część implementacji klasy może być inna”.

Niestety klasa abstrakcyjna zmusza klienta do zrobienia tego poprzez dziedziczenie . Natomiast wzorzec strategii pozwoli ci osiągnąć ten sam wynik bez dziedziczenia. Klient może tworzyć instancje twojej klasy, a nie zawsze definiować własne, a „implementacja” (zachowanie) klasy może się zmieniać dynamicznie. Wzorzec strategii ma dodatkowo tę zaletę, że zachowanie można zmienić w czasie wykonywania, nie tylko w czasie projektowania, a także ma znacznie słabsze powiązanie między zaangażowanymi typami.

Dave Cousineau
źródło
0

Zasadniczo napotykałem o wiele więcej problemów konserwacyjnych związanych z czystymi interfejsami niż ABC, nawet ABC używane z wielokrotnym dziedziczeniem. YMMV - nie wiem, może nasz zespół użył ich nieodpowiednio.

To powiedziawszy, jeśli użyjemy analogii ze świata rzeczywistego, ile jest wykorzystania czystych interfejsów całkowicie pozbawionych funkcjonalności i stanu? Jeśli użyję USB jako przykładu, jest to dość stabilny interfejs (myślę, że jesteśmy teraz na USB 3.2, ale zachował również kompatybilność wsteczną).

Jednak nie jest to interfejs bezstanowy. Nie jest pozbawiony funkcjonalności. To bardziej jak abstrakcyjna klasa bazowa niż czysty interfejs. W rzeczywistości jest bliżej konkretnej klasy o bardzo specyficznych wymaganiach funkcjonalnych i stanowych, a jedyną abstrakcją jest to, co podłącza się do portu i jest jedyną możliwą do zastąpienia częścią.

W przeciwnym razie byłby to po prostu „dziura” w komputerze o znormalizowanym formacie i o wiele luźniejszych wymaganiach funkcjonalnych, które nie zrobiłyby nic samodzielnie, dopóki każdy producent nie wymyślił własnego sprzętu, aby coś zrobić, w tym momencie staje się znacznie słabszym standardem i niczym więcej niż „dziurą” i specyfikacją tego, co należy zrobić, ale nie ma centralnego przepisu na to, jak to zrobić. Tymczasem możemy skończyć na 200 różnych sposobów, po tym jak wszyscy producenci sprzętu wymyślą własne sposoby na dołączenie funkcjonalności i stanu do tej „dziury”.

I w tym momencie możemy mieć niektórych producentów, którzy wprowadzają inne problemy niż inni. Jeśli potrzebujemy zaktualizować specyfikację, możemy mieć 200 różnych konkretnych implementacji portów USB, przy czym całkowicie różne sposoby radzenia sobie ze specyfikacją wymagają aktualizacji i przetestowania. Niektórzy producenci mogą opracowywać de facto standardowe implementacje, które dzielą między sobą (analogiczna klasa bazowa implementująca ten interfejs), ale nie wszyscy. Niektóre wersje mogą być wolniejsze niż inne. Niektóre mogą mieć lepszą przepustowość, ale gorsze opóźnienia lub odwrotnie. Niektóre mogą zużywać więcej energii baterii niż inne. Niektóre mogą się łuszczyć i nie działać z całym sprzętem, który powinien współpracować z portami USB. Niektóre mogą wymagać przyłączenia reaktora jądrowego do działania, który ma tendencję do zatrucia radiacyjnego użytkowników.

I to właśnie znalazłem osobiście z czystymi interfejsami. Mogą istnieć pewne przypadki, w których mają one sens, na przykład po prostu modelowanie kształtu płyty głównej na podstawie procesora. Analogie współczynników kształtu są w rzeczywistości prawie bezpaństwowe i pozbawione funkcjonalności, jak w przypadku analogicznej „dziury”. Ale często uważam za wielki błąd, gdy drużyny uważają, że jest to jakoś lepsze we wszystkich przypadkach, nawet nie w pobliżu.

Wręcz przeciwnie, uważam, że ABC może rozwiązać znacznie więcej przypadków niż interfejsów, jeśli są to dwie możliwości, chyba że twój zespół jest tak gigantyczny, że tak naprawdę pożądane jest posiadanie analogicznej powyżej równoważnej liczby 200 konkurencyjnych implementacji USB zamiast jednego centralnego standardu utrzymać. W poprzednim zespole, w którym pracowałem, faktycznie musiałem mocno walczyć, aby poluzować standard kodowania, aby umożliwić ABC i wielokrotne dziedziczenie, a przede wszystkim w odpowiedzi na opisane wyżej problemy konserwacyjne.


źródło