Czy powinienem używać metod abstrakcyjnych czy wirtualnych?

11

Jeśli założymy, że nie jest pożądane, aby klasa podstawowa była czystą klasą interfejsu i przy użyciu 2 poniższych przykładów, co jest lepszym podejściem przy użyciu abstrakcyjnej lub wirtualnej definicji klasy metody?

  • Zaletą „abstrakcyjnej” wersji jest to, że prawdopodobnie wygląda ona na czystszą i zmusza klasę pochodną do podania, miejmy nadzieję, znaczącej implementacji.

  • Zaletą wersji „wirtualnej” jest to, że można ją łatwo pobrać z innych modułów i wykorzystać do testowania bez dodawania zestawu podstawowych struktur, jakich wymaga wersja abstrakcyjna.

Wersja abstrakcyjna:

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Wersja wirtualna:

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}
Maczać
źródło
Dlaczego zakładamy, że interfejs nie jest pożądany?
Anthony Pegram,
Bez częściowego problemu trudno powiedzieć, że jedno jest lepsze od drugiego.
Codism
@Anthony: Interfejs nie jest pożądany, ponieważ istnieje przydatna funkcjonalność, która również przejdzie do tej klasy.
Dunk
4
return ReturnType.NotImplemented? Poważnie? Jeśli nie możesz odrzucić niezaimplementowanego typu w czasie kompilacji (możesz; użyj metod abstrakcyjnych), przynajmniej rzuć wyjątek.
Jan Hudec
3
@Dunk: Tutaj konieczne. Zwracane wartości pozostaną niezaznaczone.
Jan Hudec

Odpowiedzi:

15

Mój głos, gdybym konsumował wasze rzeczy, byłby za metodami abstrakcyjnymi. To idzie w parze z „wczesnym niepowodzeniem”. Dodanie wszystkich metod może być utrudnieniem w momencie deklaracji (choć każde przyzwoite narzędzie refaktoryzacyjne zrobi to szybko), ale przynajmniej wiem, co to jest problem natychmiast i go naprawię. Wolę to, niż debugowanie 6 miesięcy i zmian 12 osób później, aby zobaczyć, dlaczego nagle otrzymujemy niezaimplementowany wyjątek.

Erik Dietrich
źródło
Dobra uwaga dotycząca nieoczekiwanego otrzymania błędu Nieimplementowane. Co jest zaletą po stronie abstrakcyjnej, ponieważ otrzymasz błąd kompilacji zamiast błędu czasu wykonywania.
Dunk
3
+1 - Oprócz wczesnego niepowodzenia, spadkobiercy mogą natychmiast zobaczyć, jakie metody muszą wdrożyć za pośrednictwem „Implement Abstract Class”, zamiast robić to, co według nich było wystarczające, a następnie zawodzić w czasie wykonywania.
Telastyn
Zaakceptowałem tę odpowiedź, ponieważ niepowodzenie w czasie kompilacji było plusem, którego nie udało mi się wyświetlić, a także ze względu na odniesienie do używania IDE do automatycznego szybkiego wdrażania metod.
Dunk
25

Wersja wirtualna jest podatna na błędy i semantycznie niepoprawna.

Streszczenie mówi „ta metoda nie jest tu zaimplementowana. Musisz ją zaimplementować, aby ta klasa działała”

Virtual mówi „Mam domyślną implementację, ale możesz mnie zmienić, jeśli potrzebujesz”

Jeśli Twoim ostatecznym celem jest testowalność, interfejsy są zwykle najlepszą opcją. (ta klasa robi x, a ta klasa to ax). Może być konieczne podzielenie klas na mniejsze elementy, aby działało to dobrze.

Tom Squires
źródło
3

To zależy od wykorzystania twojej klasy.

Jeśli metody mają rozsądną implementację „pustą”, masz wiele metod i często zastępujesz tylko kilka z nich, wtedy użycie virtualmetod ma sens. Na przykład ExpressionVisitorjest realizowany w ten sposób.

W przeciwnym razie uważam, że powinieneś użyć abstractmetod.

W idealnym przypadku nie powinieneś mieć metod, które nie są zaimplementowane, ale w niektórych przypadkach jest to najlepsze podejście. Ale jeśli zdecydujesz się to zrobić, takie metody powinny rzucać NotImplementedException, a nie zwracać jakąś specjalną wartość.

svick
źródło
Chciałbym zauważyć, że „NotImplementedException” często oznacza błąd pominięcia, podczas gdy „NotSupportedException” oznacza jawny wybór. Poza tym zgadzam się.
Anthony Pegram
Warto zauważyć, że wiele metod jest zdefiniowanych w kategoriach „Rób wszystko, co konieczne, aby spełnić wszelkie zobowiązania związane z X”. Wywołanie takiej metody na obiekcie, który nie ma elementów związanych z X, może być bezcelowe, ale nadal byłoby dobrze zdefiniowane. Ponadto, jeśli obiekt może mieć lub nie mieć żadnych zobowiązań związanych z X, to ogólnie rzecz biorąc czystsze i bardziej skuteczne jest bezwarunkowe powiedzenie „Spełnij wszystkie swoje zobowiązania związane z X”, niż najpierw zapytać, czy ma jakieś obowiązki i warunkowo poprosić go o ich spełnienie .
supercat
1

Sugerowałbym, aby ponownie rozważyć zdefiniowanie oddzielnego interfejsu, który implementuje klasa podstawowa, a następnie zastosować podejście abstrakcyjne.

Kod obrazowania taki jak ten:

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

W ten sposób rozwiązuje się następujące problemy:

  1. Dzięki temu, że cały kod korzystający z obiektów pochodzących z AbstractVersion można teraz zaimplementować w celu otrzymania interfejsu IVersion, oznacza to, że można je łatwiej testować jednostkowo.

  2. Wersja 2 produktu może następnie zaimplementować interfejs IVersion2, aby zapewnić dodatkową funkcjonalność bez naruszania istniejącego kodu klienta.

na przykład.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Warto również przeczytać o odwróceniu zależności, aby ta klasa nie zawierała zakodowanych na stałe zależności, które uniemożliwiają skuteczne testowanie jednostek.

Michael Shaw
źródło
Głosowałem za umożliwienie obsługi różnych wersji. Jednak próbowałem i ponownie próbowałem efektywnie wykorzystywać klasy interfejsów, jako normalną część projektu, i zawsze zdaję sobie sprawę, że klasy interfejsów nie zapewniają żadnej / małej wartości i faktycznie zaciemniają kod, zamiast ułatwiać życie. Bardzo rzadko zdarza się, że wiele klas dziedziczy po innych, takich jak klasa interfejsu, i nie ma sporej liczby wspólnych elementów, których nie można udostępnić. Klasy abstrakcyjne zwykle działają lepiej, gdy otrzymuję wbudowany aspekt udostępniania, którego interfejsy nie zapewniają.
Dunk
A używanie dziedziczenia z dobrymi nazwami klas zapewnia znacznie bardziej intuicyjny sposób łatwego zrozumienia klas (a tym samym systemu) niż kilka nazw funkcjonalnych klas interfejsów, które trudno jest połączyć mentalnie jako całość. Ponadto używanie klas interfejsów zwykle powoduje powstanie mnóstwa dodatkowych klas, przez co system jest trudniejszy do zrozumienia w jeszcze inny sposób.
Dunk
-2

Wstrzykiwanie zależności zależy od interfejsów. Oto krótki przykład. Class Student ma funkcję o nazwie CreateStudent, która wymaga parametru implementującego interfejs „IReporting” (z metodą ReportAction). Po utworzeniu ucznia wywołuje ReportAction dla konkretnego parametru klasy. Jeśli system jest skonfigurowany do wysyłania wiadomości e-mail po utworzeniu ucznia, wysyłamy konkretną klasę, która wysyła wiadomość e-mail w implementacji ReportAction, lub możemy wysłać inną konkretną klasę, która wysyła dane wyjściowe do drukarki w implementacji ReportAction. Idealne do ponownego użycia kodu.

zrozumiałem
źródło
1
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami poczynionymi i wyjaśnionymi w poprzednich 5 odpowiedziach
gnat