Czy wszystkie metody publiczne w klasie abstrakcyjnej powinny być oznaczone jako wirtualne?

12

Niedawno musiałem zaktualizować abstrakcyjną klasę bazową w niektórych OSS, których używałem, aby była bardziej testowalna, czyniąc je wirtualnymi (nie mogłem użyć interfejsu, ponieważ łączył dwa). To sprawiło, że pomyślałem, czy powinienem zaznaczyć wszystkie metody, których potrzebowałem wirtualny, czy też powinienem oznaczyć każdą publiczną metodę / właściwość wirtualną. I na ogół zgadzają się z Roy Osherove że każda metoda powinna być wirtualny, ale natknąłem się na ten artykuł, który mi do myślenia o tym, czy było to konieczne, czy nie . Dla uproszczenia ograniczę to jednak do klas abstrakcyjnych (jestem pewien, że wszystkie konkretne metody publiczne powinny być wirtualne).

Widziałem, gdzie możesz pozwolić, aby podklasa korzystała z metody, ale nie chcę, aby zastąpiła ona implementację. Jeśli jednak wierzysz, że zasada Liskowa będzie przestrzegana, to dlaczego nie pozwolisz, aby została ona nadpisana? Oznaczając go jako abstrakcyjny, i tak wymuszasz pewne przesłonięcie, więc wydaje mi się, że wszystkie metody publiczne w klasie abstrakcyjnej powinny być rzeczywiście oznaczone jako wirtualne.

Chciałem jednak zapytać na wypadek, gdyby było coś, o czym mógłbym nie myśleć. Czy wszystkie metody publiczne w klasie abstrakcyjnej powinny być wirtualne?

Justin Pihony
źródło
Może to pomóc: stackoverflow.com/questions/391483/…
NoChance
1
Być może powinieneś zastanowić się, dlaczego w C # domyślnie nie jest wirtualny, ale w Javie jest. Powód, dla którego prawdopodobnie dałby ci wgląd w odpowiedź.
Daniel Little,

Odpowiedzi:

9

Jeśli jednak wierzysz, że zasada Liskowa będzie przestrzegana, to dlaczego nie pozwolisz, aby została ona nadpisana?

Na przykład, ponieważ chcę, aby szkieletowa implementacja algorytmu została naprawiona i zezwalam (tylko) na określone części do (pod) definicji podklas. Jest to powszechnie znane jako wzorzec metody szablonów (moje podkreślenie poniżej):

Metoda szablonów zarządza zatem większym obrazem semantyki zadań i bardziej dopracowanymi szczegółami implementacji wyboru i sekwencji metod. Ten większy obraz nazywa abstrakcyjne i nieabstrakcyjne metody dla danego zadania. Metody nieabstrakcyjne są całkowicie kontrolowane przez metodę szablonów, ale metody abstrakcyjne, zaimplementowane w podklasach, zapewniają ekspresyjną moc i stopień swobody wzoru. Niektóre lub wszystkie metody abstrakcyjne mogą być wyspecjalizowane w podklasie, umożliwiając autorowi podklasy zapewnienie określonego zachowania przy minimalnych modyfikacjach większej semantyki. Metoda szablonu (która nie jest abstrakcyjna) pozostaje niezmieniona w tym wzorze, zapewniając, że podrzędne metody nieabstrakcyjne i metody abstrakcyjne są wywoływane w pierwotnie zamierzonej kolejności.

Aktualizacja

Kilka konkretnych przykładów projektów, nad którymi pracowałem:

  1. komunikowanie się ze starszym systemem mainframe za pośrednictwem różnych „ekranów”. Każdy ekran ma kilka pól o stałej nazwie, pozycji i długości, zawierających określone bity danych. Żądanie wypełnia określone pola określonymi danymi. Odpowiedź zwraca dane w co najmniej jednym innym polu. Każda transakcja podlega tej samej podstawowej logice, ale szczegóły są różne na każdym ekranie. W kilku różnych projektach zastosowaliśmy metodę szablonów, aby zaimplementować stały szkielet logiki obsługi ekranu, jednocześnie umożliwiając podklasom zdefiniowanie szczegółów specyficznych dla ekranu.
  2. eksport / import danych konfiguracyjnych w tabelach DB do / z plików Excel. Ponownie podstawowy schemat przetwarzania pliku Excel i wstawiania / aktualizowania rekordów DB lub zrzucania rekordów do Excela jest taki sam dla każdej tabeli, ale szczegóły każdej tabeli są inne. Metoda szablonów jest więc bardzo oczywistym wyborem, aby wyeliminować duplikaty kodu i ułatwić zrozumienie i utrzymanie kodu.
  3. Generowanie dokumentów PDF. Każdy dokument ma tę samą strukturę, ale jego treść jest za każdym razem inna, w zależności od wielu czynników. Ponownie, metoda szablonów ułatwia oddzielenie stałego szkieletu algorytmu generowania od specyficznych dla danego przypadku, zmiennych szczegółów. W rzeczywistości. ma nawet zastosowanie na wielu poziomach, ponieważ dokument składa się z kilku sekcji , z których każda składa się z zerowej lub większej liczby pól . Metoda szablonu jest stosowana tutaj na 3 różnych poziomach.

W pierwszych dwóch przypadkach pierwotna implementacja starszego typu korzystała ze strategii , co skutkowało mnóstwem zduplikowanego kodu, który oczywiście z biegiem lat narastał subtelne różnice tu i tam, zawierał wiele zduplikowanych lub nieco różnych błędów i był bardzo trudny do utrzymania. Refaktoryzacja do metody szablonu (i niektórych innych ulepszeń, takich jak stosowanie adnotacji Java) zmniejszyła rozmiar kodu o około 40-70%.

To tylko najnowsze przykłady, które przychodzą mi na myśl. Mógłbym przytoczyć więcej przypadków z prawie każdego projektu, nad którym do tej pory pracowałem.

Péter Török
źródło
Samo cytowanie GoF nie jest odpowiedzią. Musisz podać rzeczywisty powód, aby zrobić coś takiego.
DeadMG
@DeadMG, podczas mojej kariery regularnie korzystałem z Metody Metod, więc pomyślałem, że to oczywiste, że jest to bardzo praktyczny i użyteczny wzorzec (ponieważ większość wzorców GoF to ... nie są to teoretyczne ćwiczenia akademickie, ale zebrane z rzeczywistych doświadczeń). Ale najwyraźniej nie wszyscy się z tym zgadzają ... dlatego dodam kilka konkretnych przykładów do mojej odpowiedzi.
Péter Török
2

Całkowicie uzasadnione, a czasem pożądane jest posiadanie nie-wirtualnych metod w abstrakcyjnej klasie bazowej; tylko dlatego, że jest to klasa abstrakcyjna, niekoniecznie oznacza, że ​​każda jej część powinna być użyteczna polimorficznie.

Na przykład możesz użyć idiomu „Wirtualny polimorfizm”, w którym funkcja jest wywoływana polimorficznie z nie-wirtualnej funkcji składowej, aby zapewnić spełnienie określonych warunków wstępnych lub dodatkowych przed wywołaniem funkcji wirtualnej

class MyAbstractBaseClass
{
protected:
    virtual void OverrideMe() = 0;
public:
    void CallMeFirst();

    void CallMe()
    {
        CallMeFirst();
        OverrideMe();
    }
};
Ben Cottrell
źródło
Twierdziłbym, że tak długo, jak ogólne zachowanie pozostaje takie samo, można to zrobić wirtualnie. Możesz wypełnić warunki przed / po użyciu innej implementacji (baza danych vs. w pamięci). W przeciwnym razie powinna to być funkcja prywatna?
Justin Pihony
2

Wystarczy, aby klasa zawierała JEDNĄ metodę wirtualną, aby klasa stała się abstrakcyjna. Możesz zwrócić uwagę na to, które metody chcesz wirtualne, a które nie, w zależności od rodzaju polimorfizmu, którego planujesz użyć.

appoll
źródło
1

Zadaj sobie pytanie, jakie zastosowanie ma metoda nie-wirtualna w klasie abstrakcyjnej. Taka metoda musiałaby mieć implementację, aby była użyteczna. Ale jeśli klasa ma implementację, czy nadal można ją nazwać klasą abstrakcyjną? Nawet jeśli pozwala na to język / kompilator, czy ma to sens? Osobiście nie sądzę. Miałbyś normalną klasę z abstrakcyjnymi metodami, które potomkowie powinni wdrożyć.

Moim podstawowym językiem jest Delphi, a nie C #. W Delphi, jeśli zaznaczysz streszczenie metody, musisz także oznaczyć ją jako wirtualną, inaczej kompilator narzeka. Nie śledziłem zbyt uważnie najnowszych zmian językowych, ale jeśli klasy abstrakcyjne są w Delphi lub przybędą do nich, oczekiwałbym, że kompilator będzie narzekał na wszelkie metody nie-wirtualne, metody prywatne i implementacje metod dla klasy oznaczonej jako abstrakcyjne na poziomie klasy.

Marjan Venema
źródło
2
Myślę, że sensowne jest posiadanie klasy abstrakcyjnej, która ma pewne metody abstrakcyjne, niektóre metody wirtualne i niektóre metody niebędące wirtualnymi. Myślę, że zawsze zależy to od tego, co dokładnie chcesz zrobić.
svick
3
Najpierw przejdę przez twoje C # q. Metoda abstrakcyjna w języku C # jest domyślnie wirtualna i nie może mieć implementacji. Jeśli chodzi o twój pierwszy punkt, klasa abstrakcyjna w języku C # może i powinna mieć jakąś implementację (w przeciwnym razie powinieneś po prostu użyć interfejsu). Chodzi o to, że klasa jest abstrakcyjna, MUSI być podklasą, jednak zawiera logikę, która (teoretycznie) wykorzysta wszystkie podklasy. To ogranicza duplikację kodu. Pytam o to, czy którekolwiek z tych wdrożeń powinny zostać zablokowane przed nadpisaniem (zasadniczo mówiąc, że jedyny sposób to sposób podstawowy).
Justin Pihony
@JustinPihony: dzięki. W Delphi, gdy zaznaczysz streszczenie metody, kompilator będzie narzekał, jeśli podasz jej implementację. Ciekawe, w jaki sposób różne języki w różny sposób wdrażają pojęcia, a tym samym stwarzają różne oczekiwania użytkownikom.
Marjan Venema
@svick: tak, to ma sens, po prostu nie nazwałbym tego klasą abstrakcyjną, ale klasą z abstrakcyjnymi metodami ... Ale myślę, że to może być moja interpretacja.
Marjan Venema
@MarjanVenema, ale to nie jest terminologia, z której korzysta C #. Jeśli zaznaczysz jakieś metody w klasie abstract, musisz także zaznaczyć całą klasę abstract.
svick
0

Jednak dopóki ufasz, że zasada Liskowa będzie przestrzegana, to dlaczego nie pozwolisz, by to zostało zastąpione?

Nie czynisz niektórych metod wirtualnymi, ponieważ nie uważasz, że tak jest. Ponadto, czyniąc niektóre metody nie wirtualnymi, sygnalizujesz dziedzicom, które metody należy wdrożyć.

Osobiście często sprawiam, że przeciążenia metod, które istnieją dla wygody, nie są wirtualne, aby użytkownicy klasy mogli mieć spójne ustawienia domyślne, a realizatorzy nie byli nawet w stanie popełnić błędu zerwania tego dorozumianego zachowania.

Telastyn
źródło