Rozważ następujący projekt
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
Myślisz, że coś tu jest nie tak? Dla mnie zajęcia Karla i Johna powinny być tylko instancjami zamiast zajęć, ponieważ są dokładnie takie same jak:
Person karl = new Person("Karl");
Person john = new Person("John");
Dlaczego miałbym tworzyć nowe klasy, gdy instancje są wystarczające? Klasy nic nie dodają do instancji.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler Garcia
źródło
źródło
Odpowiedzi:
Nie ma potrzeby posiadania określonych podklas dla każdej osoby.
Masz rację, powinny to być instancje.
Cel podklas: rozszerzenie klas nadrzędnych
Podklasy służą do rozszerzania funkcjonalności zapewnianej przez klasę nadrzędną. Na przykład możesz mieć:
Klasa nadrzędna,
Battery
która może miećPower()
coś i może miećVoltage
właściwość,I podklasę
RechargeableBattery
, która może dziedziczyćPower()
iVoltage
, ale może również byćRecharge()
d.Zauważ, że możesz przekazać instancję
RechargeableBattery
klasy jako parametr do dowolnej metody, która przyjmujeBattery
argument jako argument. Nazywa się to zasadą podstawienia Liskowa , jedną z pięciu zasad SOLID. Podobnie w rzeczywistości, jeśli mój odtwarzacz MP3 akceptuje dwie baterie AA, mogę je zastąpić dwiema bateriami AA.Zauważ, że czasami trudno jest ustalić, czy musisz użyć pola, czy podklasy, aby przedstawić różnicę między czymś. Na przykład, jeśli musisz obsługiwać akumulatory AA, AAA i 9-woltowe, czy stworzyłbyś trzy podklasy lub użyłby wyliczenia? „Zamień podklasę na pola” w Refaktoryzacji autorstwa Martina Fowlera, strona 232, może dać ci kilka pomysłów i sposobu przejścia od jednego do drugiego.
W twoim przykładzie
Karl
iJohn
nie rozszerzaj niczego, ani nie zapewniają one żadnej dodatkowej wartości: możesz mieć dokładnie taką samą funkcjonalność, używającPerson
bezpośrednio klasy. Posiadanie większej liczby wierszy kodu bez dodatkowej wartości nigdy nie jest dobre.Przykład uzasadnienia biznesowego
Co może być uzasadnieniem biznesowym, w którym sensowne byłoby utworzenie podklasy dla konkretnej osoby?
Załóżmy, że tworzymy aplikację, która zarządza osobami pracującymi w firmie. Aplikacja zarządza również uprawnieniami, więc Helen, księgowa, nie może uzyskać dostępu do repozytorium SVN, ale Thomas i Mary, dwaj programiści, nie mogą uzyskać dostępu do dokumentów związanych z rachunkowością.
Jimmy, wielki szef (założyciel i dyrektor generalny firmy) ma bardzo szczególne uprawnienia, których nikt nie ma. Może na przykład zamknąć cały system lub zwolnić osobę. Masz pomysł.
Najgorszym modelem dla takiej aplikacji jest posiadanie klas takich jak:
ponieważ duplikacja kodu powstanie bardzo szybko. Nawet w bardzo podstawowym przykładzie czterech pracowników zduplikujesz kod między klasami Thomas i Mary. To popchnie cię do stworzenia wspólnej klasy nadrzędnej
Programmer
. Ponieważ możesz mieć także wielu księgowych, prawdopodobnie również utworzyszAccountant
zajęcia.Teraz zauważasz, że posiadanie klasy
Helen
nie jest zbyt przydatne, a także przechowywanieThomas
iMary
: większość kodu działa w każdym razie na wyższym poziomie - na poziomie księgowych, programistów i Jimmy'ego. Serwer SVN nie dba o to, czy to Thomas czy Mary potrzebują dostępu do dziennika - musi tylko wiedzieć, czy jest programistą czy księgowym.W rezultacie usuwasz klasy, których nie używasz:
„Ale mogę utrzymać Jimmy'ego w obecnej postaci, ponieważ zawsze byłby tylko jeden dyrektor generalny, jeden wielki szef - Jimmy”. Co więcej, Jimmy jest często używany w twoim kodzie, który faktycznie wygląda bardziej tak, a nie jak w poprzednim przykładzie:
Problem z tym podejściem polega na tym, że Jimmy wciąż może zostać potrącony przez autobus, i byłby nowy dyrektor generalny. Albo zarząd może zdecydować, że Mary jest tak wielka, że powinna być nowym dyrektorem generalnym, a Jimmy zostałby zdegradowany do stanowiska sprzedawcy, więc teraz musisz przejść cały kod i wszystko zmienić.
źródło
Głupio jest używać tego rodzaju struktury klas tylko po to, by zmieniać szczegóły, które oczywiście powinny być ustawialnymi polami w instancji. Ale to jest szczególne w twoim przykładzie.
Tworzenie różnych
Person
klas jest prawie na pewno złym pomysłem - musiałbyś zmienić swój program i dokonać ponownej kompilacji za każdym razem, gdy nowa Osoba wchodzi do Twojej domeny. Nie oznacza to jednak, że dziedziczenie z istniejącej klasy, tak że gdzieś zwracany jest inny ciąg, nie jest czasem przydatne.Różnica polega na tym, że oczekuje się, że byty reprezentowane przez tę klasę będą się różnić. Zakłada się, że ludzie prawie zawsze przychodzą i odchodzą, i oczekuje się, że prawie każda poważna aplikacja zajmująca się użytkownikami będzie mogła dodawać i usuwać użytkowników w czasie wykonywania. Ale jeśli modelujesz takie rzeczy, jak różne algorytmy szyfrowania, obsługa nowej i tak jest prawdopodobnie poważną zmianą i sensowne jest wynalezienie nowej klasy, której
myName()
metoda zwraca inny ciąg (i którejperform()
metoda prawdopodobnie robi coś innego).źródło
W większości przypadków nie. Twój przykład jest naprawdę dobrym przykładem, w którym to zachowanie nie wnosi rzeczywistej wartości.
Narusza to również zasadę Otwartego Zamknięcia , ponieważ podklasy zasadniczo nie są rozszerzeniem, lecz modyfikują wewnętrzne funkcjonowanie rodzica. Co więcej, publiczny konstruktor rodzica jest teraz irytujący w podklasach, a zatem interfejs API stał się mniej zrozumiały.
Jednak czasami, jeśli kiedykolwiek będziesz mieć tylko jedną lub dwie specjalne konfiguracje, które są często używane w całym kodzie, czasem jest wygodniej i mniej czasochłonnie po prostu podklasować rodzica, który ma złożony konstruktor. W takich szczególnych przypadkach nie widzę nic złego w takim podejściu. Nazwijmy to konstruktorem curry j / k
źródło
Jeśli taki jest zakres praktyki, to zgadzam się, że jest to zła praktyka.
Jeśli John i Karl zachowują się inaczej, sytuacja się nieco zmienia. Może być tak, że
person
ma metodę, wcleanRoom(Room room)
której okazuje się, że John jest świetnym współlokatorem i bardzo skutecznie sprząta, ale Karl nie jest i nie bardzo czyści pokój.W takim przypadku sensowne byłoby, aby były one własną podklasą ze zdefiniowanym zachowaniem. Są lepsze sposoby na osiągnięcie tego samego (na przykład
CleaningBehavior
klasy), ale przynajmniej w ten sposób nie jest straszne naruszenie zasad OO.źródło
W niektórych przypadkach wiesz, że będzie tylko określona liczba instancji, a wtedy użycie tego podejścia byłoby w porządku (choć moim zdaniem brzydkie). Lepiej jest użyć wyliczenia, jeśli pozwala na to język.
Przykład Java:
źródło
Wiele osób już odpowiedziało. Myślałem, że dam własną perspektywę.
Dawno, dawno temu pracowałem nad aplikacją (i nadal tak robię), która tworzy muzykę.
Aplikacja miała abstrakcyjną
Scale
klasę z kilku podklas:CMajor
,DMinor
itpScale
wyglądał mniej więcej tak więc:Generatory muzyki współpracowały z konkretną
Scale
instancją w celu generowania muzyki. Użytkownik wybiera skalę z listy, z której ma być generowana muzyka.Pewnego dnia przyszedł mi do głowy fajny pomysł: dlaczego nie pozwolić użytkownikowi na tworzenie własnych wag? Użytkownik wybiera notatki z listy, naciska przycisk, a nowa skala zostanie dodana do listy dostępnych skal.
Ale nie byłem w stanie tego zrobić. Stało się tak, ponieważ wszystkie skale są już ustawione w czasie kompilacji - ponieważ są wyrażone jako klasy. Potem uderzyło mnie to:
Często intuicyjne jest myślenie w kategoriach „nadklas i podklas”. Prawie wszystko można wyrazić za pomocą tego systemu: nadklasa
Person
i podklasyJohn
orazMary
; nadklasaCar
i podklasyVolvo
orazMazda
; nadklasaMissile
i podklasySpeedRocked
,LandMine
orazTrippleExplodingThingy
.Myślenie w ten sposób jest bardzo naturalne, szczególnie dla osoby stosunkowo nowej w OO.
Ale zawsze powinniśmy pamiętać, że klasy to szablony , a obiekty to zawartość wlewana do tych szablonów . Możesz wlać dowolną zawartość do szablonu, tworząc niezliczone możliwości.
Wypełnienie szablonu nie jest zadaniem podklasy. To zadanie obiektu. Zadaniem podklasy jest dodanie faktycznej funkcjonalności lub rozwinięcie szablonu .
I dlatego powinienem był stworzyć konkretną
Scale
klasę zNote[]
polem i pozwolić obiektom wypełnić ten szablon ; ewentualnie przez konstruktora lub coś takiego. I w końcu tak zrobiłem.Za każdym razem, gdy projektujesz szablon w klasie (np. Pusty
Note[]
element, który należy wypełnić lubString name
pole, któremu należy przypisać wartość), pamiętaj, że wypełnianie szablonu jest zadaniem obiektów tej klasy ( lub ewentualnie osoby tworzące te obiekty). Podklasy mają na celu zwiększenie funkcjonalności, a nie wypełnianie szablonów.Możesz mieć ochotę stworzyć „nadklasę
Person
, podklasyJohn
iMary
” system, tak jak to zrobiłeś, ponieważ podoba ci się formalność, jaką to daje.W ten sposób możesz po prostu powiedzieć
Person p = new Mary()
zamiastPerson p = new Person("Mary", 57, Sex.FEMALE)
. Sprawia, że rzeczy są bardziej zorganizowane i bardziej uporządkowane. Ale, jak powiedzieliśmy, tworzenie nowej klasy dla każdej kombinacji danych nie jest dobrym podejściem, ponieważ przesadza z kodem za darmo i ogranicza cię pod względem możliwości działania.Oto rozwiązanie: użyj podstawowej fabryki, może nawet być statyczna. Tak jak:
W ten sposób możesz łatwo korzystać z „ustawień wstępnych” „dostarczanych z programem”, takich jak:,
Person mary = PersonFactory.createMary()
ale zastrzegasz sobie również prawo do dynamicznego projektowania nowych osób, na przykład w przypadku, gdy chcesz pozwolić użytkownikowi to zrobić . Na przykład:Lub jeszcze lepiej: zrób coś takiego:
Poniosło mnie. Myślę, że masz pomysł.
Podklasy nie mają na celu wypełnienia szablonów ustawionych przez ich nadklasy. Podklasy mają na celu dodanie funkcjonalności . Obiekty mają wypełniać szablony, po to są.
Nie powinieneś tworzyć nowej klasy dla każdej możliwej kombinacji danych. (Tak jak nie powinienem był tworzyć nowej
Scale
podklasy dla każdej możliwej kombinacjiNote
s).Jest wytyczną: za każdym razem, gdy tworzysz nową podklasę, zastanów się, czy dodaje ona nową funkcjonalność, która nie istnieje w tej podklasie. Jeśli odpowiedź na to pytanie brzmi „nie”, być może próbujesz „wypełnić szablon” nadklasy, w takim przypadku po prostu stwórz obiekt. (I ewentualnie Fabryka z „ustawieniami wstępnymi”, aby ułatwić życie).
Mam nadzieję, że to pomaga.
źródło
Jest to wyjątek od „tutaj powinny być instancje” - choć nieco ekstremalny.
Jeśli chcesz, aby kompilator wymuszał uprawnienia dostępu do metod na podstawie tego, czy jest to Karl czy John (w tym przykładzie), zamiast pytać implementację metody o sprawdzenie, która instancja została przekazana, możesz użyć różnych klas. Wymuszając różne klasy zamiast tworzenia instancji, umożliwia się sprawdzanie różnic między „Karl” i „John” w czasie kompilacji, a nie w czasie wykonywania, a może to być przydatne - szczególnie w kontekście bezpieczeństwa, w którym kompilatorowi można zaufać bardziej niż w czasie wykonywania kontrole kodu (na przykład) bibliotek zewnętrznych.
źródło
Powielanie kodu jest uważane za złą praktykę .
Dzieje się tak przynajmniej częściowo, ponieważ linie kodów, a zatem rozmiar bazy kodów, koreluje z kosztami utrzymania i wadami .
Oto odpowiednia logika zdrowego rozsądku w tym konkretnym temacie:
1) Gdybym musiał to zmienić, ile miejsc musiałbym odwiedzić? Tutaj mniej znaczy lepiej.
2) Czy istnieje powód, dla którego odbywa się to w ten sposób? W twoim przykładzie - nie mogło być - ale w niektórych przykładach może istnieć jakiś powód, aby to zrobić. Jedno, co mogę sobie wyobrazić z góry głowy, to niektóre frameworki, takie jak wczesne Grails, w których dziedziczenie niekoniecznie działało ładnie z niektórymi wtyczkami do GORM. Bardzo rzadko.
3) Czy to jest czystsze i łatwiejsze do zrozumienia w ten sposób? Znowu nie ma tego w twoim przykładzie - ale istnieją pewne wzorce dziedziczenia, które mogą być bardziej rozciągliwe, w których oddzielne klasy są w rzeczywistości łatwiejsze do utrzymania (przychodzą na myśl pewne zastosowania wzorców poleceń lub strategii).
źródło
Istnieje przypadek użycia: utworzenie odwołania do funkcji lub metody jako zwykłego obiektu.
Widać to bardzo często w kodzie Java GUI:
Kompilator pomaga tutaj, tworząc nową anonimową podklasę.
źródło