W 2000 roku mój kolega powiedział mi, że to anty-wzorzec, aby publiczne metody były wirtualne lub abstrakcyjne.
Na przykład uważał taką klasę za niezbyt dobrze zaprojektowaną:
public abstract class PublicAbstractOrVirtual
{
public abstract void Method1(string argument);
public virtual void Method2(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
// default implementation
}
}
Stwierdził to
- twórca klasy pochodnej, która implementuje
Method1
i zastępuje,Method2
musi powtórzyć sprawdzanie poprawności argumentów. - na wypadek, gdyby twórca klasy podstawowej zdecydował się dodać coś wokół dostosowywalnej części
Method1
lubMethod2
później, nie może tego zrobić.
Zamiast tego mój kolega zaproponował takie podejście:
public abstract class ProtectedAbstractOrVirtual
{
public void Method1(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
this.Method1Core(argument);
}
public void Method2(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
this.Method2Core(argument);
}
protected abstract void Method1Core(string argument);
protected virtual void Method2Core(string argument)
{
// default implementation
}
}
Powiedział mi, że upublicznianie publicznych metod (lub właściwości) jest tak samo złe, jak upublicznianie pól. Dzięki zawijaniu pól we właściwości można w razie potrzeby przechwycić później dostęp do tych pól. To samo dotyczy publicznych wirtualnych / abstrakcyjnych członków: zawijanie ich w sposób pokazany w ProtectedAbstractOrVirtual
klasie pozwala programistom klasy podstawowej przechwytywać wszelkie wywołania, które przechodzą do metod wirtualnych / abstrakcyjnych.
Ale nie uważam tego za wytyczne projektowe. Nawet Microsoft tego nie przestrzega: wystarczy spojrzeć na Stream
klasę, aby to sprawdzić.
Co sądzisz o tej wytycznej? Czy to ma sens, czy uważasz, że to komplikuje API?
źródło
virtual
pozwalają na opcjonalne zastąpienie. Twoja metoda powinna być prawdopodobnie publiczna, ponieważ może nie zostać zastąpiona. Tworzenie metodabstract
zmusza do ich przesłonięcia; prawdopodobnie powinnyprotected
, ponieważ nie są szczególnie przydatne wpublic
kontekście.protected
jest najbardziej przydatny, gdy chcesz narazić prywatnych członków klasy abstrakcyjnej na klasy pochodne. W każdym razie nie martwię się szczególnie o opinię twojego przyjaciela; wybierz modyfikator dostępu, który najlepiej pasuje do konkretnej sytuacji.Odpowiedzi:
Powiedzenie
miesza przyczynę i skutek. Zakłada się, że każda nadpisywana metoda wymaga niepoprawnego sprawdzania poprawności argumentów. Ale jest na odwrót:
Jeśli ktoś chce zaprojektować metodę w taki sposób, aby zapewniała pewne stałe sprawdzanie poprawności argumentów we wszystkich pochodnych klasy (lub - bardziej ogólnie - w części konfigurowalnej i niemożliwej do dostosowania), wówczas sensowne jest, aby punkt wejścia był nie-wirtualny , a zamiast tego zapewnia wirtualną lub abstrakcyjną metodę dla dostosowywanej części, która jest nazywana wewnętrznie.
Ale istnieje wiele przykładów, gdzie to ma sens mieć publiczną wirtualną metodę, ponieważ nie jest stały zakaz konfigurowalny część: spojrzenie na standardowych metod, takich jak
ToString
lubEquals
czyGetHashCode
- by poprawić konstrukcjęobject
klasy mają te nie publiczne i wirtualny w tym samym czasie? Nie wydaje mi sięLub, jeśli chodzi o własny kod: kiedy kod w klasie bazowej wreszcie i celowo wygląda tak
takie oddzielenie
Method1
iMethod1Core
komplikuje tylko rzeczy bez wyraźnego powodu.źródło
ToString()
metody Microsoft lepiej uczynił ją nie-wirtualną i wprowadził metodę wirtualnego szablonuToStringCore()
. Dlaczego: z tego powodu:ToString()
- Uwaga dla spadkobierców . Oświadczają, żeToString()
nie powinny zwracać wartości null. Mogliby sprostać temu żądaniu poprzez wdrożenieToString() => ToStringCore() ?? string.Empty
.string.Empty
, zauważyłeś? I zaleca wiele innych rzeczy, których nie można wymusić w kodzie przez wprowadzenie czegoś takiego jakToStringCore
metoda. Ta technika prawdopodobnie nie jest odpowiednim narzędziemToString
.anyObjectIGot.ToString()
zamiastanyObjectIGot?.ToString()
” - jak to ma znaczenie? TwojeToStringCore()
podejście zapobiegnie zwróceniu łańcucha zerowego, ale nadal spowoduje wyrzucenie znaku,NullReferenceException
jeśli obiekt ma wartość zerową.public virtual
, ale są przypadki, w którychpublic virtual
jest w porządku. Moglibyśmy argumentować za pustą częścią, której nie można dostosowywać, co czyni kod kodem na przyszłość ... Ale to nie działa. Cofanie się i zmienianie może spowodować uszkodzenie typów pochodnych. W ten sposób nic nie zarabia.Robienie tego w sposób sugerowany przez kolegę zapewnia większą elastyczność implementatorowi klasy podstawowej. Ale z tym wiąże się także większa złożoność, która zwykle nie jest uzasadniona domniemanymi korzyściami.
Pamiętaj, że większa elastyczność dla implementatora klasy podstawowej odbywa się kosztem mniejszej elastyczności dla strony nadrzędnej. Otrzymują pewne narzucone zachowania, o które nie mogą szczególnie dbać. Dla nich sprawy stały się bardziej sztywne. Może to być uzasadnione i pomocne, ale wszystko zależy od scenariusza.
Konwencja nazewnictwa w celu implementacji tego (o czym wiem) polega na zarezerwowaniu dobrej nazwy interfejsu publicznego i poprzedzeniu nazwy metody wewnętrznej słowem „Do”.
Jednym z użytecznych przypadków jest sytuacja, w której wykonywana czynność wymaga konfiguracji i zamknięcia. Jak otworzyć strumień i zamknąć go po zakończeniu overrider. Ogólnie ten sam rodzaj inicjalizacji i finalizacji. Jest to prawidłowy wzorzec do użycia, ale nie ma sensu narzucać jego zastosowania we wszystkich abstrakcyjnych i wirtualnych scenariuszach.
źródło
BindingList
. Ponadto znalazłem gdzieś zalecenie, może ich Wytyczne dotyczące projektowania ram lub coś podobnego. I: to postfiks . ;-)W języku C ++ jest to nazywane wzorcem interfejsu innego niż wirtualny (NVI). (Kiedyś nazywało się to Metodą Szablonu. To było mylące, ale niektóre starsze artykuły mają taką terminologię.) NVI promuje Herb Sutter, który napisał o tym przynajmniej kilka razy. Myślę, że jeden z najwcześniej jest tutaj .
Jeśli dobrze pamiętam, założeniem jest, że klasa pochodna nie powinna zmieniać tego, co robi klasa podstawowa, ale jak to robi.
Na przykład Kształt może mieć metodę Przenieś w celu przeniesienia kształtu. Konkretne implementacje (np. Kwadrat i koła) nie powinny bezpośrednio zastępować ruchu, ponieważ Shape określa, co oznacza ruch (na poziomie koncepcyjnym). Kwadrat może mieć szczegóły implementacji różnicy niż Okrąg pod względem sposobu wewnętrznej reprezentacji pozycji, więc będą musieli zastąpić jakąś metodę, aby zapewnić funkcjonalność Przenieś.
W prostych przykładach często sprowadza się to do publicznego Move, który po prostu deleguje całą pracę do prywatnego wirtualnego ReallyDoTheMove, więc wydaje się, że to dużo narzutu bez korzyści.
Ale ta korespondencja jeden do jednego nie jest wymagana. Na przykład możesz dodać metodę Animate do publicznego interfejsu API Shape, który może ją zaimplementować, wywołując ReallyDoTheMove w pętli. W efekcie powstają dwa interfejsy API publicznych metod innych niż wirtualne, które opierają się na jednej prywatnej metodzie abstrakcyjnej. Twoje kręgi i kwadraty nie muszą wykonywać żadnej dodatkowej pracy, podobnie jak Animate .
Klasa podstawowa definiuje interfejs publiczny używany przez konsumentów i definiuje interfejs operacji prymitywnych, których potrzebuje do zaimplementowania tych metod publicznych. Typy pochodne są odpowiedzialne za zapewnienie implementacji tych prymitywnych operacji.
Nie znam żadnych różnic między C # i C ++, które mogłyby zmienić ten aspekt projektowania klas.
źródło