Ostatnio niepokoją mnie zastosowania klas abstrakcyjnych.
Czasami klasa abstrakcyjna jest wcześniej tworzona i działa jako szablon tego, jak działają klasy pochodne. Oznacza to mniej więcej, że zapewniają one pewną funkcjonalność na wysokim poziomie, ale pomijają pewne szczegóły do zaimplementowania przez klasy pochodne. Klasa abstrakcyjna określa potrzebę tych szczegółów, wprowadzając pewne metody abstrakcyjne. W takich przypadkach klasa abstrakcyjna działa jak plan, ogólny opis funkcjonalności lub jakkolwiek chcesz to nazwać. Nie można go używać samodzielnie, ale musi być wyspecjalizowany, aby zdefiniować szczegóły, które zostały pominięte we wdrożeniu wysokiego poziomu.
Innym razem zdarza się, że klasa abstrakcyjna jest tworzona po utworzeniu niektórych klas „pochodnych” (ponieważ klasy macierzystej / abstrakcyjnej nie ma, nie zostały jeszcze wyprowadzone, ale wiesz, co mam na myśli). W takich przypadkach klasa abstrakcyjna jest zwykle używana jako miejsce, w którym można umieścić dowolny rodzaj wspólnego kodu, który zawiera bieżące klasy pochodne.
Po dokonaniu powyższych obserwacji zastanawiam się, który z tych dwóch przypadków powinien być regułą. Czy jakikolwiek szczegół powinien zostać przekazany do klasy abstrakcyjnej tylko dlatego, że obecnie są one wspólne dla wszystkich klas pochodnych? Czy powinien istnieć wspólny kod, który nie jest częścią funkcjonalności wysokiego poziomu?
Czy kod, który może nie mieć żadnego znaczenia dla samej klasy abstrakcyjnej, powinien istnieć tylko dlatego, że zdarza się, że jest wspólny dla klas pochodnych?
Podam przykład: Klasa abstrakcyjna A ma metodę a () i metodę abstrakcyjną aq (). Metoda aq (), w obu pochodnych klasach AB i AC, wykorzystuje metodę b (). Czy b () przeniósł się do A? Jeśli tak, to w przypadku, gdyby ktoś patrzył tylko na A (udawajmy, że nie ma AB i AC), istnienie b () nie miałoby większego sensu! Czy to źle? Czy ktoś powinien spojrzeć na klasę abstrakcyjną i zrozumieć, co się dzieje bez odwiedzania klas pochodnych?
Szczerze mówiąc, w momencie zadawania tego pytania, mam skłonność wierzyć, że pisanie abstrakcyjnej klasy, która ma sens bez konieczności szukania klas pochodnych, jest kwestią czystego kodu i czystej architektury. Ι Nie podoba mi się pomysł, że klasa abstrakcyjna, która działa jak zrzut dowolnego kodu, jest powszechna we wszystkich klasach pochodnych.
Co myślisz / ćwiczysz?
źródło
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- Dlaczego? Czy nie jest możliwe, że podczas projektowania i rozwijania aplikacji odkryłem, że kilka klas ma wspólną funkcjonalność, którą można naturalnie przekształcić w klasę abstrakcyjną? Czy muszę być wystarczająco jasnowidzem, aby zawsze to przewidzieć przed napisaniem kodu dla klas pochodnych? Jeśli nie jestem tak bystry, czy zabrania mi się przeprowadzania takiego refaktoryzacji? Czy muszę wyrzucić swój kod i zacząć od nowa?Odpowiedzi:
To, o co pytasz, to gdzie umieścić
b()
, i, w innym sensie, pytanie brzmi, czyA
jest najlepszym wyborem jako bezpośrednia superklasa dlaAB
iAC
.Wydaje się, że są trzy możliwości:
b()
w obuAB
iAC
ABAC-Parent
która dziedziczyA
i wprowadzab()
, a następnie jest używana jako bezpośrednia superklasa dlaAB
iAC
b()
wA
(nie wiedząc, czy inna klasa przyszłościAD
będzie chciałb()
, czy nie)Dopóki inna klasa
AD
, która nie chce sięb()
prezentować, wydaje się właściwym wyborem.W takim momencie, jako
AD
prezent, możemy zmienić podejście do podejścia w (2) - w końcu to oprogramowanie!źródło
b()
żadnej klasy. Zrób z niego bezpłatną funkcję, która bierze wszystkie swoje dane jako argumenty i ma obie te funkcjeAB
iAC
wywołuje ją Oznacza to, że nie musisz go przenosić ani tworzyć żadnych klas podczas dodawaniaAD
.b()
nie potrzebuje stanu długowiecznego.ac
która będzie wywoływaćb
zamiastac
być abstrakcyjna?Klasa abstrakcyjna nie jest przeznaczona do wysypywania różnych funkcji lub danych, które są wrzucane do klasy abstrakcyjnej, ponieważ jest to wygodne.
Jedną z praktycznych zasad, która wydaje się być najbardziej niezawodnym i możliwym do rozszerzenia podejściem zorientowanym obiektowo, jest: „ Wolę kompozycję niż dziedziczenie ”. Klasa abstrakcyjna jest prawdopodobnie najlepiej traktowana jako specyfikacja interfejsu, która nie zawiera żadnego kodu.
Jeśli masz jakąś metodę, która jest rodzajem metody bibliotecznej, która idzie w parze z klasą abstrakcyjną, metoda, która jest najbardziej prawdopodobnym sposobem wyrażenia pewnej funkcjonalności lub zachowania, których zwykle potrzebują klasy pochodzące z klasy abstrakcyjnej, wówczas sensowne jest utworzenie klasa pomiędzy klasą abstrakcyjną a innymi klasami, w których ta metoda jest dostępna. Ta nowa klasa zapewnia szczególną instancję klasy abstrakcyjnej definiującej konkretną alternatywę implementacyjną lub ścieżkę za pomocą tej metody.
Ideą klasy abstrakcyjnej jest posiadanie abstrakcyjnego modelu tego, co klasy pochodne, które faktycznie implementują klasę abstrakcyjną, powinny zapewniać jako usługę lub zachowanie. Dziedziczenie jest łatwe w użyciu i dlatego wiele razy najbardziej przydatne klasy składają się z różnych klas wykorzystujących wzór mixin .
Zawsze jednak pojawiają się pytania dotyczące zmiany, co się zmieni i jak to się zmieni i dlaczego to się zmieni.
Dziedziczenie może prowadzić do kruchych i kruchych ciał źródła (patrz także Dziedziczenie: przestań go już używać! ).
źródło
b()
jest to czysta funkcja. Może to być funktoid, szablon lub coś innego. Używam wyrażenia „metoda biblioteczna” jako jakiegoś komponentu, czy to czystej funkcji, obiektu COM, czy czegokolwiek, co jest używane do funkcjonalności zapewnianej przez rozwiązanie.Twoje pytanie sugeruje albo podejście do klas abstrakcyjnych. Uważam jednak, że powinieneś traktować je jako kolejne narzędzie w swoim zestawie narzędzi. I wtedy pojawia się pytanie: dla jakich zadań / problemów klasy abstrakcyjne są właściwym narzędziem?
Jednym z doskonałych przypadków użycia jest wdrożenie wzorca metody szablonu . Umieszczasz całą logikę niezmienną w klasie abstrakcyjnej, a logikę wariantową w jej podklasach. Zauważ, że wspólna logika sama w sobie jest niekompletna i niefunkcjonalna. W większości przypadków chodzi o implementację algorytmu, w którym kilka kroków jest zawsze takich samych, ale co najmniej jeden krok jest różny. Umieść ten jeden krok jako metodę abstrakcyjną, która jest wywoływana z jednej z funkcji wewnątrz klasy abstrakcyjnej.
Myślę, że twój pierwszy przykład jest w zasadzie opisem wzorca metody szablonu (popraw mnie, jeśli się mylę), dlatego uważam to za całkowicie poprawny przypadek użycia klas abstrakcyjnych.
W drugim przykładzie uważam, że użycie klas abstrakcyjnych nie jest optymalnym wyborem, ponieważ istnieją lepsze metody radzenia sobie ze wspólną logiką i zduplikowanym kodem. Powiedzmy, że masz klasę abstrakcyjną
A
, klas pochodnychB
iC
i obie klasy pochodzące udostępnić jakąś logikę w postaci metodys()
. Aby zdecydować o właściwym podejściu do pozbycia się duplikacji, ważne jest, aby wiedzieć, czy metodas()
jest częścią interfejsu publicznego, czy nie.Jeśli tak nie jest (jak we własnym konkretnym przykładzie z metodą
b()
), sprawa jest dość prosta. Po prostu stwórz z niej osobną klasę, wyodrębniając kontekst potrzebny do wykonania operacji. Jest to klasyczny przykład kompozycji zamiast dziedziczenia . Jeśli kontekst jest niewielki lub nie jest potrzebny, prosta funkcja pomocnicza może już wystarczyć, jak sugerowano w niektórych komentarzach.Jeśli
s()
jest częścią interfejsu publicznego, staje się nieco trudniejszy. Zakładając, żes()
nie ma to nic wspólnegoB
i nieC
jest zA
tobą spokrewniony , nie powinieneś wkładać dos()
środkaA
. Więc gdzie to położyć? Argumentowałbym za deklaracją oddzielnego interfejsuI
, który określas()
. Potem znowu, należy utworzyć oddzielną klasę, która zawiera logikę za wdrażanies()
i zarównoB
iC
od niej zależne.Ostatni, tu jest link do doskonałej odpowiedzi na pytanie o ciekawej, dzięki czemu może pomóc Ci zdecydować, kiedy należy przejść do interfejsu i kiedy do klasy abstrakcyjnej:
źródło
Wydaje mi się, że brakuje ci punktu orientacji obiektu, zarówno logicznie, jak i technicznie. Opisujesz dwa scenariusze: grupowanie typowego zachowania typów w klasie bazowej i polimorfizm. Są to oba uzasadnione zastosowania klasy abstrakcyjnej. Ale to, czy powinieneś uczynić klasę abstrakcyjną, czy nie, zależy od modelu analitycznego, a nie możliwości technicznych.
Powinieneś rozpoznać typ, który nie ma inkarnacji w prawdziwym świecie, a jednak stanowi podstawę dla określonych typów, które istnieją. Przykład: zwierzę. Nie ma czegoś takiego jak zwierzę. Zawsze jest to pies, kot lub cokolwiek innego, ale nie ma prawdziwego zwierzęcia. Jednak Animal je wszystkie.
Potem mówisz o poziomach. Poziomy nie są częścią umowy, jeśli chodzi o dziedziczenie. Nie uznaje się również wspólnych danych ani wspólnych zachowań, to podejście techniczne, które prawdopodobnie nie pomoże. Powinieneś rozpoznać stereotyp, a następnie wstawić klasę podstawową. Jeśli nie ma takiego stereotypu, możesz być lepszy z kilkoma interfejsami implementowanymi przez wiele klas.
Nazwa twojej abstrakcyjnej klasy wraz z jej członkami powinna mieć sens. Musi być niezależny od dowolnej klasy pochodnej, zarówno pod względem technicznym, jak i logicznym. Podobnie jak Animal może mieć metodę abstrakcyjną Jeść (która byłaby polimorficzna) i logiczne właściwości abstrakcyjne IsPet i IsLifestock, co ma sens bez wiedzy o kotach, psach lub świniach. Każda zależność (techniczna lub logiczna) powinna iść tylko w jedną stronę: od potomka do bazy. Same klasy podstawowe również nie powinny mieć wiedzy o klasach malejących.
źródło
Pierwszy przypadek z twojego pytania:
Drugi przypadek:
Następnie twoje pytanie :
IMO ma dwa różne scenariusze, dlatego nie można narysować jednej reguły, aby zdecydować, który projekt należy zastosować.
W pierwszym przypadku mamy takie rzeczy, jak wzorzec metody szablonów, wspomniany już w innych odpowiedziach.
W drugim przypadku podałeś przykład abstrakcyjnej klasy A odbierającej metodę ab (); IMO metodę b () należy przenieść tylko wtedy, gdy ma to sens dla WSZYSTKICH innych klas pochodnych. Innymi słowy, jeśli przenosisz go, ponieważ jest używany w dwóch miejscach, prawdopodobnie nie jest to dobry wybór, ponieważ jutro może być nowa konkretna klasa pochodna, w której b () nie ma żadnego sensu. Ponadto, jak powiedziałeś, jeśli spojrzysz na klasę A osobno, a b () również nie ma sensu w tym kontekście, to prawdopodobnie jest to wskazówka, że nie jest to również dobry wybór projektu.
źródło
Jakiś czas temu miałem dokładnie takie same pytania!
Kiedy natknąłem się na tę kwestię, odkryłem, że istnieje jakaś ukryta koncepcja, której nie znalazłem. I ta koncepcja najprawdopodobniej powinna być wyrażona za pomocą jakiegoś obiektu wartości . Masz bardzo abstrakcyjny przykład, więc obawiam się, że nie mogę pokazać, co mam na myśli, używając twojego kodu. Ale tutaj jest przypadek z mojej własnej praktyki. Miałem dwie klasy, które reprezentowały sposób wysłania żądania do zewnętrznego zasobu i parsowania odpowiedzi. Istniały podobieństwa w sposobie formułowania żądań i analizie odpowiedzi. Tak więc wyglądała moja hierarchia:
Taki kod wskazuje, że po prostu niewłaściwie używam dziedziczenia. W ten sposób można go refaktoryzować:
Od tego czasu dziedziczę dziedzictwo bardzo ostrożnie, ponieważ wiele razy mnie raniono.
Od tego czasu doszedłem do kolejnego wniosku dotyczącego dziedziczenia. Jestem zdecydowanie przeciwny dziedziczeniu opartemu na wewnętrznej strukturze . Łamie enkapsulację, jest krucha, w końcu jest proceduralna. A kiedy rozkładam domenę we właściwy sposób , dziedziczenie po prostu nie zdarza się często.
źródło