Wygląda na to, że Java ma moc deklarowania klas niepochodzących od wieków, a teraz C ++ też to ma. Jednak w świetle zasady Open / Close w SOLID, dlaczego miałoby to być przydatne? Dla mnie final
słowo kluczowe brzmi jak friend
- jest legalne, ale jeśli go używasz, najprawdopodobniej projekt jest nieprawidłowy. Podaj przykłady, w których klasa niepochodna byłaby częścią wspaniałego wzorca architektury lub wzornictwa.
54
final
? Wiele osób (w tym ja) uważa, że dobrym pomysłem jest stworzenie każdej klasy nieabstrakcyjnejfinal
.final
.Odpowiedzi:
final
wyraża zamiar . Informuje użytkownika o klasie, metodzie lub zmiennej „Ten element nie powinien się zmieniać, a jeśli chcesz go zmienić, nie zrozumiałeś istniejącego projektu”.Jest to ważne, ponieważ architektura programu byłaby naprawdę, bardzo trudna, gdybyś musiał przewidzieć, że każda klasa i każda metoda, którą kiedykolwiek napiszesz, może zostać zmieniona, aby zrobić coś zupełnie innego przez podklasę. O wiele lepiej jest zdecydować z góry, które elementy powinny być zmienne, a które nie, i wymusić niezmienność poprzez
final
.Możesz to również zrobić za pomocą komentarzy i dokumentów architektury, ale zawsze lepiej jest pozwolić kompilatorowi egzekwować, co może, niż mieć nadzieję, że przyszli użytkownicy będą czytać i przestrzegać dokumentacji.
źródło
Pozwala to uniknąć problemu delikatnej klasy podstawowej . Każda klasa ma zestaw niejawnych lub wyraźnych gwarancji i niezmienników. Zasada substytucji Liskowa stanowi, że wszystkie podtypy tej klasy muszą również zapewniać wszystkie te gwarancje. Jednak bardzo łatwo jest to złamać, jeśli nie korzystamy
final
. Na przykład, sprawdźmy hasło:Jeśli pozwolimy na zastąpienie tej klasy, jedna implementacja może zablokować wszystkich, inna może dać każdemu dostęp:
Zwykle nie jest to OK, ponieważ podklasy mają teraz zachowanie, które jest bardzo niezgodne z oryginałem. Jeśli naprawdę zamierzamy rozszerzyć klasę o inne zachowania, łańcuch odpowiedzialności byłby lepszy:
Problem staje się bardziej widoczny, gdy bardziej skomplikowana klasa wywołuje własne metody, a metody te można zastąpić. Czasami spotykam się z tym, gdy ładnie drukuję strukturę danych lub piszę HTML. Każda metoda odpowiada za niektóre widżety.
Teraz tworzę podklasę, która dodaje nieco więcej stylizacji:
Teraz ignorując przez chwilę, że nie jest to bardzo dobry sposób na generowanie stron HTML, co się stanie, jeśli chcę ponownie zmienić układ? Musiałbym stworzyć
SpiffyPage
podklasę, która jakoś otacza tę treść. Widzimy tu przypadkowe zastosowanie wzorca metody szablonu. Metody szablonów są dobrze zdefiniowanymi punktami rozszerzenia w klasie bazowej, które mają zostać zastąpione.A co się stanie, jeśli zmieni się klasa podstawowa? Jeśli zawartość HTML zmieni się za bardzo, może to spowodować uszkodzenie układu zapewnionego przez podklasy. Dlatego później nie jest naprawdę bezpiecznie zmieniać klasy podstawowej. Nie jest to oczywiste, jeśli wszystkie twoje klasy są w tym samym projekcie, ale bardzo zauważalne, jeśli klasa podstawowa jest częścią opublikowanego oprogramowania, na którym budują inni ludzie.
Gdyby ta strategia rozszerzenia była zamierzona, moglibyśmy pozwolić użytkownikowi na zmianę sposobu generowania każdej części. Albo może istnieć strategia dla każdego bloku, która może być zapewniona zewnętrznie. Lub możemy zagnieździć Dekoratorów. Byłoby to równoważne z powyższym kodem, ale o wiele bardziej wyraźne i znacznie bardziej elastyczne:
(Dodatkowy
top
parametr jest konieczny, aby upewnić się, że wywołaniawriteMainContent
przechodzące przez górną część łańcucha dekoratora. Emuluje to funkcję podklasy zwaną otwartą rekurencją .)Jeśli mamy wielu dekoratorów, możemy je teraz swobodnie mieszać.
O wiele częściej niż chęć nieznacznego dostosowania istniejącej funkcjonalności jest chęć ponownego wykorzystania części istniejącej klasy. Widziałem przypadek, w którym ktoś chciał klasy, w której można dodawać przedmioty i powtarzać je wszystkie. Prawidłowym rozwiązaniem byłoby:
Zamiast tego stworzyli podklasę:
To nagle oznacza, że cały interfejs
ArrayList
stał się częścią naszego interfejsu. Użytkownicy mogąremove()
rzeczy lubget()
rzeczy o określonych indeksach. To było przeznaczone w ten sposób? DOBRZE. Ale często nie zastanawiamy się dokładnie nad wszystkimi konsekwencjami.Dlatego wskazane jest, aby
extend
klasa bez dokładnego przemyślenia.final
wyjątki, chyba że zamierzasz zastąpić dowolną metodę.Istnieje wiele przykładów, w których ta „reguła” musi zostać złamana, ale zwykle prowadzi do dobrego, elastycznego projektu i pozwala uniknąć błędów spowodowanych niezamierzonymi zmianami w klasach podstawowych (lub niezamierzonym użyciem podklasy jako instancji klasy podstawowej ).
Niektóre języki mają bardziej rygorystyczne mechanizmy egzekwowania:
virtual
źródło
Dziwi mnie, że nikt jeszcze nie wspomniał o Effective Java, wydanie drugie autorstwa Joshua Blocha (który powinien być wymagany przynajmniej dla każdego programisty Java). Punkt 17 w książce szczegółowo to omawia i zatytułowany jest: „ Projekt i dokument do dziedziczenia lub zabraniam go ”.
Nie powtórzę wszystkich dobrych rad zawartych w książce, ale te konkretne akapity wydają się istotne:
źródło
Jednym z powodów
final
jest przydatność, ponieważ zapewnia, że nie można podklasować klasy w sposób, który naruszałby umowę klasy nadrzędnej. Takie podklasowanie stanowiłoby naruszenie SOLID (przede wszystkim „L”), a utworzenie klasyfinal
uniemożliwia.Jednym typowym przykładem jest uniemożliwienie podklasy niezmiennej klasy w sposób, który spowodowałby, że podklasa byłaby zmienna. W niektórych przypadkach taka zmiana zachowania może prowadzić do bardzo zaskakujących efektów, na przykład gdy używasz czegoś jako klucza na mapie, myśląc, że klucz jest niezmienny, podczas gdy w rzeczywistości używasz podklasy, którą można modyfikować.
W Javie można by wprowadzić wiele interesujących problemów bezpieczeństwa, gdybyś był w stanie podklasować
String
i sprawić, że będzie on mutowalny (lub sprawi, że będzie oddzwaniać do domu, gdy ktoś wywoła jego metody, a tym samym może wyciągnąć poufne dane z systemu) podczas przekazywania tych obiektów wokół jakiegoś wewnętrznego kodu związanego z ładowaniem klas i bezpieczeństwem.Czasami końcowy jest również pomocny w zapobieganiu prostym błędom, takim jak ponowne użycie tej samej zmiennej dla dwóch rzeczy w metodzie itp. W Scali zachęca się do używania tylko tych,
val
które w przybliżeniu odpowiadają zmiennym końcowym w Javie, a właściwie dowolnego użyciavar
lub zmienna nieokreślona jest podejrzana.Wreszcie, kompilatory mogą, przynajmniej teoretycznie, przeprowadzić dodatkowe optymalizacje, gdy wiedzą, że klasa lub metoda jest ostateczna, ponieważ kiedy wywołujesz metodę w klasie końcowej, wiesz dokładnie, która metoda zostanie wywołana i nie musisz iść za pomocą wirtualnej tabeli metod, aby sprawdzić dziedziczenie.
źródło
SecurityManager
. Większość programów tego nie używa, ale nie uruchamia również żadnego niezaufanego kodu. +++ Musisz założyć, że ciągi są niezmienne, ponieważ w przeciwnym razie otrzymasz zero bezpieczeństwa i kilka dowolnych błędów jako bonus. Każdy programista, który zakłada, że ciągi Java mogą się zmieniać w produktywnym kodzie, jest z pewnością szalony.Drugim powodem jest wydajność . Pierwszym powodem jest to, że niektóre klasy mają ważne zachowania lub stany, których nie należy zmieniać w celu umożliwienia działania systemu. Na przykład, jeśli mam klasę „PasswordCheck” i aby zbudować tę klasę, zatrudniłem zespół ekspertów ds. Bezpieczeństwa i ta klasa komunikuje się z setkami bankomatów z dobrze zbadanymi i zdefiniowanymi procedurami. Pozwól nowemu zatrudnionemu facetowi świeżo po studiach stworzyć klasę „TrustMePasswordCheck”, która rozszerza powyższą klasę, może być bardzo szkodliwa dla mojego systemu; metody te nie powinny zostać zastąpione, to wszystko.
źródło
Kiedy potrzebuję zajęć, napiszę zajęcia. Jeśli nie potrzebuję podklas, nie dbam o podklasy. Upewniam się, że moja klasa zachowuje się zgodnie z przeznaczeniem, a miejsca, w których korzystam z klasy, zakładają, że klasa zachowuje się zgodnie z przeznaczeniem.
Jeśli ktoś chce podklasować moją klasę, chcę całkowicie odmówić jakiejkolwiek odpowiedzialności za to, co się stanie. Osiągam to, czyniąc klasę „ostateczną”. Jeśli chcesz to podklasować, pamiętaj, że nie brałem pod uwagę podklas podczas pisania zajęć. Musisz więc wziąć kod źródłowy klasy, usunąć „końcowy”, a odtąd wszystko, co się wydarzy, będzie w pełni odpowiedzialne .
Myślisz, że to „nie zorientowane obiektowo”? Zapłacono mi za stworzenie klasy, która robi to, co powinna. Nikt mi nie zapłacił za zrobienie klasy, którą można by podzielić. Jeśli dostaniesz zapłatę za ponowne wykorzystanie mojej klasy, możesz to zrobić. Zacznij od usunięcia „końcowego” słowa kluczowego.
(Poza tym „końcowy” często pozwala na znaczną optymalizację. Na przykład w Swift „końcowy” w klasie publicznej lub w metodzie klasy publicznej oznacza, że kompilator może w pełni wiedzieć, jaki kod wykona wywołanie metody, i może zastąpić wysyłkę dynamiczną wysyłką statyczną (niewielka korzyść) i często zastępować wysyłkę statyczną wstawką (prawdopodobnie ogromna korzyść)).
adelphus: Co jest tak trudnego do zrozumienia w przypadku „jeśli chcesz go podklasować, wziąć kod źródłowy, usunąć„ końcowy ”, a to twoja odpowiedzialność”? „końcowe” oznacza „uczciwe ostrzeżenie”.
I nie otrzymuję zapłaty za kod wielokrotnego użytku. Zarabia mi się pisanie kodu, który robi to, co powinien. Jeśli otrzymam wynagrodzenie za wykonanie dwóch podobnych fragmentów kodu, wyodrębnię części wspólne, ponieważ jest to tańsze i nie płacę za marnowanie czasu. Ponowne użycie kodu, który nie jest ponownie używany, to strata mojego czasu.
M4ks: Zawsze czynisz wszystko prywatnym, do czego dostęp nie powinien być uzyskiwany z zewnątrz. Ponownie, jeśli chcesz podklasować, bierzesz kod źródłowy, w razie potrzeby zmieniasz rzeczy na „chronione” i bierzesz odpowiedzialność za to, co robisz. Jeśli uważasz, że potrzebujesz dostępu do rzeczy, które oznaczyłem jako prywatne, lepiej wiedz, co robisz.
Oba: Subclassing to niewielka porcja kodu używanego ponownie. Tworzenie bloków konstrukcyjnych, które można dostosowywać bez tworzenia podklas, jest znacznie bardziej wydajne i zapewnia ogromne korzyści z „ostatecznego”, ponieważ użytkownicy bloków mogą polegać na tym, co otrzymują.
źródło
Wyobraźmy sobie, że SDK dla platformy dostarcza następującą klasę:
Aplikacja podklasuje tę klasę:
Wszystko jest w porządku i dobrze, ale ktoś pracujący nad SDK decyduje, że przekazanie metody
get
jest głupie, i sprawia, że interfejs lepiej upewnia się, że wymusza zgodność wsteczną.Wszystko wydaje się w porządku, dopóki powyższa aplikacja nie zostanie ponownie skompilowana z nowym SDK. Nagle nadpisana metoda get nie jest już wywoływana, a żądanie nie jest liczone.
Nazywa się to problemem delikatnej klasy bazowej, ponieważ z pozoru niewinna zmiana powoduje rozbicie podklasy. Każda zmiana metody wywoływanej w klasie może spowodować uszkodzenie podklasy. Oznacza to, że prawie każda zmiana może spowodować uszkodzenie podklasy.
Final uniemożliwia podklasę twojej klasy. W ten sposób metody wewnątrz klasy można zmienić bez obawy, że gdzieś ktoś zależy od tego, które wywołania metod zostaną wykonane.
źródło
Ostatecznie oznacza, że twoja klasa może bezpiecznie zmienić się w przyszłości bez wpływu na jakiekolwiek klasy oparte na dziedziczeniu niższego rzędu (ponieważ nie ma żadnych) lub jakiekolwiek problemy związane z bezpieczeństwem wątków klasy (myślę, że są przypadki, w których ostatnie słowo kluczowe na polu zapobiega niektóre oparte na wątku high-jinx).
Ostateczny oznacza, że możesz swobodnie zmieniać sposób działania swojej klasy bez żadnych niezamierzonych zmian w zachowaniu wkradających się w kod innej osoby, który opiera się na twoim jako bazie.
Jako przykład piszę klasę o nazwie HobbitKiller, co jest świetne, ponieważ wszyscy hobbici są trikami i prawdopodobnie powinni umrzeć. Zdrap je, wszyscy zdecydowanie muszą umrzeć.
Używasz tego jako klasy podstawowej i dodajesz niesamowitą nową metodę korzystania z miotacza ognia, ale używaj mojej klasy jako podstawy, ponieważ mam świetną metodę atakowania hobbitów (poza tym, że są trikami, są szybcy), co pomagasz namierzyć miotacz ognia.
Trzy miesiące później zmieniam implementację mojej metody kierowania. Teraz, w pewnym momencie przyszłym, kiedy uaktualniacie bibliotekę, bez wiedzy, faktyczna implementacja środowiska wykonawczego klasy uległa zasadniczej zmianie ze względu na zmianę metody superklasy, na której polegacie (i na ogół nie kontrolujecie).
Tak więc, aby być sumiennym programistą i zapewnić bezproblemową śmierć hobbitów w przyszłości za pomocą mojej klasy, muszę bardzo, bardzo ostrożnie podchodzić do wszelkich zmian, które wprowadzam do dowolnych klas, które można rozszerzyć.
Usuwając możliwość przedłużania, z wyjątkiem przypadków, w których konkretnie zamierzam rozszerzyć klasę, oszczędzam sobie (i mam nadzieję, że innym) wiele bólów głowy.
źródło
final
Użycie
final
nie jest w żaden sposób naruszeniem zasad SOLID. Niestety, niezwykle powszechne jest interpretowanie zasady otwartej / zamkniętej („jednostki oprogramowania powinny być otwarte dla rozszerzenia, ale zamknięte dla modyfikacji”) w znaczeniu „zamiast modyfikować klasę, podklasować ją i dodawać nowe funkcje”. Nie to pierwotnie miało na myśli i ogólnie uważa się, że nie jest to najlepsze podejście do osiągnięcia swoich celów.Najlepszym sposobem zachowania zgodności z OCP jest zaprojektowanie punktów rozszerzenia w klasie, poprzez zapewnienie w szczególności abstrakcyjnych zachowań, które są parametryzowane przez wstrzyknięcie zależności do obiektu (np. Przy użyciu wzorca projektowania strategii). Te zachowania powinny być zaprojektowane tak, aby korzystały z interfejsu, aby nowe implementacje nie opierały się na dziedziczeniu.
Innym podejściem jest zaimplementowanie swojej klasy za pomocą publicznego interfejsu API jako klasy abstrakcyjnej (lub interfejsu). Następnie możesz stworzyć zupełnie nową implementację, która może być podłączona do tych samych klientów. Jeśli nowy interfejs wymaga zasadniczo podobnego zachowania jak oryginalny, możesz:
źródło
Dla mnie to kwestia projektu.
Załóżmy, że mam program, który oblicza wynagrodzenia dla pracowników. Jeśli mam klasę, która zwraca liczbę dni roboczych między dwiema datami w zależności od kraju (jedna klasa dla każdego kraju), ustawię tę datę końcową i przedstawię metodę dla każdego przedsiębiorstwa, która zapewni dzień wolny tylko dla swoich kalendarzy.
Dlaczego? Prosty. Powiedzmy, że programista chce odziedziczyć klasę podstawową WorkingDaysUSA w klasie WorkingDaysUSAmyCompany i zmodyfikować ją, aby odzwierciedlić, że jego przedsięwzięcie zostanie zamknięte z powodu strajku / konserwacji / z dowolnego powodu 2. marca.
Obliczenia dotyczące zamówień i dostaw klientów odzwierciedlają opóźnienie i działają odpowiednio, gdy w środowisku wykonawczym wywołują WorkingDaysUSAmyCompany.getWorkingDays (), ale co się stanie, gdy obliczę czas wakacji? Czy powinienem dodać 2 miejsce Marsa jako wakacje dla wszystkich? Nie. Ale ponieważ programista zastosował dziedziczenie, a ja nie chroniłem klasy, może to prowadzić do zamieszania.
Albo powiedzmy, że dziedziczą i modyfikują klasę, aby odzwierciedlić, że ta firma nie pracuje w soboty, podczas gdy w kraju pracują w niepełnym wymiarze godzin w sobotę. Następnie trzęsienie ziemi, kryzys energetyczny lub inne okoliczności powodują, że prezydent ogłasza 3 dni wolne od pracy, tak jak ostatnio miało to miejsce w Wenezueli. Jeśli metoda odziedziczonej klasy odejmowała już w każdą sobotę, moje modyfikacje oryginalnej klasy mogłyby prowadzić do odejmowania tego samego dnia dwa razy. Musiałbym przejść do każdej podklasy na każdym kliencie i sprawdzić, czy wszystkie zmiany są zgodne.
Rozwiązanie? Uczyń klasę ostateczną i podaj metodę addFreeDay (mojafirma ID firmy, Data za darmo). W ten sposób masz pewność, że gdy wywołasz klasę WorkingDaysCountry, będzie to Twoja klasa główna, a nie podklasa
źródło