Dlaczego słowo „końcowe” miałoby być przydatne?

54

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 finalsł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.

Vorac
źródło
43
Jak myślisz, dlaczego klasa jest źle zaprojektowana, jeśli jest opatrzona adnotacjami final? Wiele osób (w tym ja) uważa, że ​​dobrym pomysłem jest stworzenie każdej klasy nieabstrakcyjnej final.
Zauważył
20
Preferuj kompozycję zamiast dziedziczenia i możesz mieć każdą nieabstrakcyjną klasę final.
Andy
24
Zasada otwarcia / zamknięcia jest w pewnym sensie anachronizmem z XX wieku, kiedy mantra miała stworzyć hierarchię klas, które odziedziczyły po klasach, które z kolei odziedziczyły po innych klasach. Było to przydatne do nauczania programowania obiektowego, ale okazało się, że tworzy splątane, niemożliwe do utrzymania bałagany, gdy stosuje się je do rzeczywistych problemów. Projektowanie klasy, która ma być rozszerzalna, jest trudne.
David Hammen
34
@DavidArno Nie bądź śmieszny. Dziedziczenie jest warunkiem koniecznym programowania obiektowego i nie jest tak skomplikowane lub bałagan, jak niektórzy ludzie zbyt dogmatyczni lubią głosić. Jest to narzędzie, jak każde inne, a dobry programista wie, jak używać odpowiedniego narzędzia do pracy.
Mason Wheeler,
48
Jako odzyskujący programista wydaje mi się, że finał jest świetnym narzędziem do zapobiegania przedostawaniu się szkodliwych zachowań do krytycznych sekcji. Wydaje mi się również, że dziedziczenie jest potężnym narzędziem na wiele sposobów. To prawie tak, jakby gasp różne narzędzia mają wady i zalety, a my jako inżynierowie muszą pogodzić te czynniki jak produkujemy nasze oprogramowanie!
corsiKa

Odpowiedzi:

136

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.

Kilian Foth
źródło
14
Ja też. Ale każdy, kto kiedykolwiek napisał powszechnie używaną klasę podstawową (taką jak framework lub biblioteka multimediów), wie lepiej niż oczekiwać, że programiści będą zachowywać się w rozsądny sposób. Obetną, niewłaściwie użyją i zniekształcą twój wynalazek w sposób, o którym nawet nie myślałeś, że jest możliwy, chyba że zablokujesz go żelaznym uchwytem.
Kilian Foth,
8
@KilianFoth Ok, ale szczerze mówiąc, jak to jest twój problem, co robią programiści aplikacji?
rdzeń rdzeniowy
20
@coredump Ludzie używający mojej biblioteki źle tworzą złe systemy. Złe systemy rodzą złą reputację. Użytkownicy nie będą w stanie odróżnić świetnego kodu Kiliana od potwornie niestabilnej aplikacji Freda Randoma. Wynik: tracę kredyty programistyczne i klientów. Trudne do niewłaściwego użycia kodu to kwestia dolarów i centów.
Kilian Foth,
21
Stwierdzenie „Ten element nie powinien się zmieniać, a jeśli chcesz go zmienić, nie zrozumiałeś istniejącego projektu”. jest niesamowicie arogancki, a nie postawy, jakiej chciałbym w ramach każdej biblioteki, z którą pracowałem. Gdybym tylko miał dziesięciocentówkę za każdym razem, gdy jakaś śmiesznie przepełniona biblioteka nie pozostawiała mnie bez mechanizmu zmiany jakiegoś stanu wewnętrznego, który należy zmienić, ponieważ autor nie przewidział ważnego przypadku użycia ...
Mason Wheeler
31
@JimmyB Ogólna zasada: jeśli uważasz, że wiesz, do czego zostanie wykorzystane twoje dzieło, już się mylisz. Bell uważał telefon za zasadniczo system Muzak. Kleenex został wynaleziony w celu wygodniejszego usuwania makijażu. Thomas Watson, prezes IBM, powiedział kiedyś: „Myślę, że istnieje światowy rynek na może pięć komputerów”. Przedstawiciel Jim Sensenbrenner, który wprowadził ustawę PATRIOT w 2001 r., Mówi, że jej celem było uniemożliwienie NSA robienia rzeczy, które robią „z autorytetem PATRIOT”. I tak dalej ...
Mason Wheeler,
59

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:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Jeśli pozwolimy na zastąpienie tej klasy, jedna implementacja może zablokować wszystkich, inna może dać każdemu dostęp:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Teraz tworzę podklasę, która dodaje nieco więcej stylizacji:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

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ć SpiffyPagepodklasę, 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:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(Dodatkowy topparametr jest konieczny, aby upewnić się, że wywołania writeMainContentprzechodzą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:

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

Zamiast tego stworzyli podklasę:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

To nagle oznacza, że ​​cały interfejs ArrayListstał się częścią naszego interfejsu. Użytkownicy mogą remove()rzeczy lub get()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

  • nigdy extendklasa bez dokładnego przemyślenia.
  • zawsze zaznaczaj swoje klasy jako finalwyjątki, chyba że zamierzasz zastąpić dowolną metodę.
  • twórz interfejsy, w których chcesz zamienić implementację, np. do testowania jednostkowego.

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:

  • Wszystkie metody są domyślnie ostateczne i muszą być wyraźnie oznaczone jako virtual
  • Zapewniają prywatne dziedziczenie, które nie dziedziczy interfejsu, a jedynie implementację.
  • Wymagają, aby metody klasy bazowej były oznaczone jako wirtualne i wymagają również oznaczenia wszystkich przesłonięć. Pozwala to uniknąć problemów, gdy podklasa zdefiniowała nową metodę, ale metoda o tej samej sygnaturze została później dodana do klasy podstawowej, ale nie była przeznaczona jako wirtualna.
amon
źródło
3
Zasługujesz na co najmniej +100 za wspomnienie o „delikatnym problemie z klasą podstawową”. :)
David Arno
6
Nie przekonują mnie przedstawione tutaj punkty. Tak, delikatna klasa podstawowa stanowi problem, ale wersja ostateczna nie rozwiązuje wszystkich problemów związanych ze zmianą implementacji. Twój pierwszy przykład jest zły, ponieważ zakładasz, że znasz wszystkie możliwe przypadki użycia programu PasswordChecker („blokowanie wszystkich lub zezwalanie wszystkim na dostęp ... nie jest w porządku” - mówi kto?). Twoja ostatnia lista „dlatego wskazane ...” jest naprawdę zła - zasadniczo opowiadasz się za nie przedłużaniem niczego i oznaczaniem wszystkiego jako ostatecznym - co całkowicie niweczy użyteczność OOP, dziedziczenie i ponowne użycie kodu.
Adelphus
4
Twój pierwszy przykład nie jest przykładem delikatnego problemu z klasą podstawową. W delikatnym problemie z klasą podstawową zmiany w klasie podstawowej powodują uszkodzenie podklasy. Ale w tym przykładzie podklasa nie jest zgodna z umową podklasy. To są dwa różne problemy. (Dodatkowo uzasadnione jest, że w pewnych okolicznościach możesz wyłączyć sprawdzanie hasła (powiedzmy o rozwoju))
Winston Ewert
5
„Unika delikatnego problemu z klasą podstawową” - w ten sam sposób, w jaki samobójstwo pozwala uniknąć głodu.
user253751
9
@immibis, to bardziej jak unikanie jedzenia, aby uniknąć zatrucia pokarmowego. Jasne, nigdy nie byłoby problemu. Ale jedzenie tylko w zaufanych miejscach ma sens.
Winston Ewert
33

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:

A co ze zwykłymi konkretnymi zajęciami? Tradycyjnie nie są one ani ostateczne, ani zaprojektowane i udokumentowane do podklasy, ale ten stan rzeczy jest niebezpieczny. Za każdym razem, gdy wprowadza się zmianę w takiej klasie, istnieje szansa, że ​​klasy klienta, które ją rozszerzą, ulegną awarii. To nie jest tylko problem teoretyczny. Często zdarza się, że otrzymujemy raporty o błędach związane z podklasą po zmodyfikowaniu wewnętrznych elementów nieskończonej konkretnej klasy, która nie została zaprojektowana i udokumentowana do dziedziczenia.

Najlepszym rozwiązaniem tego problemu jest zakazanie podklasowania w klasach, które nie zostały zaprojektowane i nie zostały udokumentowane, aby można je było bezpiecznie podklasować. Istnieją dwa sposoby na zakazanie podklas. Najłatwiejszym z nich jest ogłoszenie finału klasy. Alternatywą jest uczynienie wszystkich konstruktorów prywatnymi lub pakietowymi prywatnymi oraz dodanie publicznych statycznych fabryk zamiast konstruktorów. Ta alternatywa, która zapewnia elastyczność w wewnętrznym korzystaniu z podklas, została omówiona w punkcie 15. Każde podejście jest dopuszczalne.

Daniel Pryden
źródło
21

Jednym z powodów finaljest 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 klasy finaluniemoż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ć Stringi 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, valktóre w przybliżeniu odpowiadają zmiennym końcowym w Javie, a właściwie dowolnego użycia varlub 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.

Michał Kosmulski
źródło
6
Wreszcie, kompilatory mogą, przynajmniej teoretycznie => osobiście przejrzeć kartę demirtualizacji Clanga i potwierdzam, że jest stosowana w praktyce.
Matthieu M.,
Ale czy kompilator nie może z wyprzedzeniem powiedzieć, że nikt nie zastępuje klasy lub metody, niezależnie od tego, czy jest ona oznaczona jako ostateczna?
JesseTG
3
@JesseTG Prawdopodobnie ma dostęp do całego kodu na raz. A co z osobną kompilacją plików?
Przywróć Monikę
3
Dewirtualizacja @JesseTG lub buforowanie wbudowane (monomorficzne / polimorficzne) jest powszechną techniką w kompilatorach JIT, ponieważ system wie, które klasy są aktualnie ładowane, i może dezoptymalizować kod, jeśli założenie, że żadne metody zastępujące nie okaże się fałszywe. Jednak kompilator z wyprzedzeniem nie może. Kiedy kompiluję jedną klasę Java i kod korzystający z tej klasy, mogę później skompilować podklasę i przekazać instancję do używającego kodu. Najprostszym sposobem jest dodanie kolejnego słoika z przodu ścieżki klasy. Kompilator nie może wiedzieć o tym wszystkim, ponieważ dzieje się tak w czasie wykonywania.
amon
5
@MTilsted Możesz założyć, że ciągi są niezmienne, ponieważ mutowanie łańcuchów poprzez odbicie może być zabronione przez 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.
maaartinus
7

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.

JoulinRouge
źródło
1
bancomat = ATM: en.wikipedia.org/wiki/Cash_machine
tar
JVM jest wystarczająco inteligentny, aby traktować klasy jako ostateczne, jeśli nie mają podklas, nawet jeśli nie zostały uznane za ostateczne.
user253751
Odebrano, ponieważ twierdzenie o „wydajności” zostało odrzucone wiele razy.
Paul Smith
7

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ą.

gnasher729
źródło
4
-1 Ta odpowiedź opisuje wszystko, co jest nie tak z twórcami oprogramowania. Jeśli ktoś chce ponownie wykorzystać twoją klasę przez podklasę, pozwól mu. Dlaczego miałbyś ponosić odpowiedzialność za sposób, w jaki go wykorzystują (lub wykorzystują)? Zasadniczo używasz finału tak jak ty, nie korzystasz z mojej klasy. * „Nikt mi nie zapłacił za stworzenie zajęć, które można by podzielić na klasy” . Mówisz poważnie? Właśnie dlatego zatrudniani są inżynierowie oprogramowania - do tworzenia solidnego kodu wielokrotnego użytku.
adelphus
4
-1 Po prostu
ustaw wszystko w tajemnicy
3
@adelphus Chociaż treść tej odpowiedzi jest tępa, granicząca z ostrym, nie jest to „zły” punkt widzenia. W rzeczywistości jest to ten sam punkt widzenia, jak większość odpowiedzi na to pytanie, tylko z mniejszym tonem klinicznym.
NemesisX00
+1 za wzmiankę, że możesz usunąć „końcowy”. Aroganckie jest twierdzenie, że wiesz o wszystkich możliwych zastosowaniach twojego kodu. Jednak pokornym jest wyjaśnienie, że nie można utrzymać niektórych możliwych zastosowań i że zastosowania te wymagałyby utrzymania widelca.
gmatht
4

Wyobraźmy sobie, że SDK dla platformy dostarcza następującą klasę:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Aplikacja podklasuje tę klasę:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Wszystko jest w porządku i dobrze, ale ktoś pracujący nad SDK decyduje, że przekazanie metody getjest głupie, i sprawia, że ​​interfejs lepiej upewnia się, że wymusza zgodność wsteczną.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

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.

Winston Ewert
źródło
1

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.

Scott Taylor
źródło
2
Jeśli metoda kierowania zmieni się, dlaczego udostępniłeś ją publicznie? A jeśli znacząco zmienisz zachowanie klasy, potrzebujesz innej wersji niż nadopiekuńczejfinal
M4ks
Nie mam pojęcia, dlaczego to się zmieni. Hobbici są trikami i zmiana była wymagana. Chodzi o to, że jeśli zbuduję go jako ostateczny, zapobiegam dziedziczeniu, które chroni inne osoby przed zainfekowaniem mojego kodu przez moje zmiany.
Scott Taylor,
0

Użycie finalnie 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:

  • użyj wzorca projektowego Dekorator, aby ponownie wykorzystać istniejące zachowanie oryginału, lub
  • refaktoryzuj części zachowania, które chcesz zachować w obiekcie pomocnika i użyj tego samego pomocnika w nowej implementacji (refaktoryzacja nie jest modyfikacją).
Jules
źródło
0

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

bns
źródło