Perché la parola chiave "finale" sarebbe mai stata utile?


54

Sembra che Java abbia avuto il potere di dichiarare le classi non derivabili per secoli, e ora anche C ++. Tuttavia, alla luce del principio Open / Close in SOLID, perché sarebbe utile? Per me, la finalparola chiave suona proprio come friend- è legale, ma se la stai usando, molto probabilmente il design è sbagliato. Fornisci alcuni esempi in cui una classe non derivabile sarebbe parte di una grande architettura o modello di progettazione.


43
Perché pensi che una classe sia erroneamente progettata se è annotata con final? Molte persone (incluso me) ritengono che sia un buon design realizzare ogni classe non astratta final.
Scoperto

20
Favorisci la composizione rispetto all'eredità e puoi avere ogni classe non astratta final.
Andy,

24
Il principio di apertura / chiusura è in un certo senso un anacronismo del 20 ° secolo, quando il mantra doveva creare una gerarchia di classi ereditate da classi che a loro volta ereditavano da altre classi. Questo è stato utile per insegnare la programmazione orientata agli oggetti, ma si è scoperto che creava problemi ingarbugliati e irraggiungibili quando applicato a problemi del mondo reale. Progettare una classe per essere estensibile è difficile.
David Hammen,

34
@DavidArno Non essere ridicolo. L'ereditarietà è la condizione essenziale della programmazione orientata agli oggetti, ed è tutt'altro che complicata o disordinata come alcuni individui eccessivamente dogmatici amano predicare. È uno strumento, come tutti gli altri, e un buon programmatore sa come utilizzare lo strumento giusto per il lavoro.
Mason Wheeler,

48
Come sviluppatore in fase di recupero, mi sembra di ricordare che Final è uno strumento geniale per impedire a comportamenti dannosi di accedere a sezioni critiche. Mi sembra anche di ricordare che l'eredità sia uno strumento potente in vari modi. È come se sussultassero diversi strumenti che presentano vantaggi e svantaggi e noi ingegneri dobbiamo bilanciare questi fattori mentre produciamo il nostro software!
corsiKa,

Risposte:


136

final esprime intento . Indica all'utente una classe, un metodo o una variabile "Questo elemento non dovrebbe cambiare e se vuoi cambiarlo, non hai capito il progetto esistente."

Questo è importante perché l'architettura del programma sarebbe molto, molto difficile se dovessi anticipare che ogni classe e ogni metodo che scrivi potrebbero essere cambiati per fare qualcosa di completamente diverso da una sottoclasse. È molto meglio decidere in anticipo quali elementi dovrebbero essere modificabili e quali non lo sono, e far valere l'immancabilità final.

Puoi anche farlo tramite commenti e documenti di architettura, ma è sempre meglio lasciare che il compilatore imponga le cose che può piuttosto che sperare che i futuri utenti leggano e obbediscano alla documentazione.


14
Anch'io. Ma chiunque abbia mai scritto una classe base ampiamente riutilizzata (come un framework o una libreria multimediale) sa meglio di aspettarsi che i programmatori di applicazioni si comportino in modo sano. Sovvertiranno, abuseranno e distorceranno la tua invenzione in modi che non avresti nemmeno pensato fosse possibile a meno che non la bloccassi con una presa di ferro.
Kilian Foth,

8
@KilianFoth Ok, ma onestamente, che problema hai con i programmatori di applicazioni?
coredump,

20
@coredump Le persone che usano la mia biblioteca creano male i sistemi. I sistemi difettosi generano cattiva reputazione. Gli utenti non saranno in grado di distinguere l'ottimo codice di Kilian dall'app mostruosamente instabile di Fred Random. Risultato: perdo nella programmazione di crediti e clienti. Rendere difficile l'utilizzo improprio del codice è una questione di dollari e centesimi.
Kilian Foth,

21
L'affermazione "Questo elemento non dovrebbe cambiare, e se vuoi cambiarlo, non hai capito il design esistente." è incredibilmente arrogante e non l'atteggiamento che vorrei come parte di qualsiasi biblioteca con cui ho lavorato. Se solo avessi avuto un centesimo per ogni volta che una biblioteca ridicolmente incapsulata non mi lasciava senza alcun meccanismo per cambiare un pezzo di stato interiore che deve essere cambiato perché l'autore non è riuscito ad anticipare un caso d'uso importante ...
Mason Wheeler,

31
@JimmyB Regola empirica: se pensi di sapere a cosa servirà la tua creazione, ti sbagli già. Bell concepì il telefono essenzialmente come un sistema Muzak. Kleenex è stato inventato allo scopo di rimuovere il trucco in modo più conveniente. Thomas Watson, presidente di IBM, una volta disse "Penso che esista un mercato mondiale per forse cinque computer". Il rappresentante Jim Sensenbrenner, che ha introdotto il PATRIOT Act nel 2001, afferma che era stato specificamente progettato per impedire all'NSA di fare ciò che sta facendo "con l'autorità dell'atto PATRIOT". E così via ...
Mason Wheeler,

59

Evita il problema della classe di base fragile . Ogni classe viene fornita con una serie di garanzie e invarianti impliciti o espliciti. Il principio di sostituzione di Liskov impone che anche tutti i sottotipi di quella classe debbano fornire tutte queste garanzie. Tuttavia, è davvero facile violarlo se non lo utilizziamo final. Ad esempio, disponiamo di un controllo password:

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

Se permettiamo a quella classe di essere sovrascritta, un'implementazione potrebbe bloccare tutti, un'altra potrebbe dare a tutti l'accesso:

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

Questo di solito non è OK, poiché le sottoclassi ora hanno un comportamento molto incompatibile con l'originale. Se intendiamo davvero ampliare la classe con altri comportamenti, una catena di responsabilità sarebbe migliore:

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;
  }
}

Il problema diventa più evidente quando più una classe complicata chiama i propri metodi e questi metodi possono essere ignorati. A volte mi capita di trovarlo quando stampo una struttura di dati o scrivo HTML. Ogni metodo è responsabile di alcuni widget.

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>");
  }

  ...
}

Ora creo una sottoclasse che aggiunge un po 'più di stile:

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>");
  }
}

Ora ignorando per un momento che questo non è un ottimo modo per generare pagine HTML, cosa succede se voglio cambiare di nuovo il layout? Dovrei creare una SpiffyPagesottoclasse che avvolge in qualche modo quel contenuto. Quello che possiamo vedere qui è un'applicazione accidentale del modello di metodo del modello. I metodi modello sono punti di estensione ben definiti in una classe base che devono essere sovrascritti.

E cosa succede se la classe base cambia? Se i contenuti HTML cambiano troppo, ciò potrebbe interrompere il layout fornito dalle sottoclassi. Quindi non è davvero sicuro cambiare la classe base in seguito. Questo non è evidente se tutte le tue classi fanno parte dello stesso progetto, ma è molto evidente se la classe di base fa parte di alcuni software pubblicati su cui altre persone si basano.

Se si intendesse questa strategia di estensione, avremmo potuto consentire all'utente di scambiare il modo in cui ogni parte viene generata. In entrambi i casi, potrebbe esistere una strategia per ciascun blocco che può essere fornita esternamente. Oppure potremmo nidificare i decoratori. Questo sarebbe equivalente al codice sopra, ma molto più esplicito e molto più flessibile:

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);
    ...
  }
}

(Il topparametro aggiuntivo è necessario per assicurarsi che le chiamate writeMainContentattraversino la parte superiore della catena del decoratore. Questo emula una funzione di sottoclasse chiamata ricorsione aperta .)

Se abbiamo più decoratori, ora possiamo mescolarli più liberamente.

Molto più spesso del desiderio di adattare leggermente la funzionalità esistente è il desiderio di riutilizzare una parte di una classe esistente. Ho visto un caso in cui qualcuno voleva una classe in cui si potevano aggiungere elementi e scorrere su tutti. La soluzione corretta sarebbe stata:

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
}

Invece, hanno creato una sottoclasse:

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

Ciò significa improvvisamente che l'intera interfaccia di ArrayListè diventata parte della nostra interfaccia. Gli utenti possono remove()cose o get()cose su indici specifici. Questo era inteso in questo modo? OK. Ma spesso non pensiamo attentamente a tutte le conseguenze.

È quindi consigliabile

  • mai extenduna lezione senza un attento pensiero.
  • contrassegnare sempre le classi come finaltranne se si intende sostituire qualsiasi metodo.
  • creare interfacce in cui si desidera scambiare un'implementazione, ad esempio per test unitari.

Ci sono molti esempi in cui questa "regola" deve essere infranta, ma di solito ti guida verso una progettazione buona e flessibile ed evita i bug dovuti a cambiamenti involontari nelle classi base (o usi involontari della sottoclasse come istanza della classe base ).

Alcune lingue hanno meccanismi di applicazione più severi:

  • Tutti i metodi sono definitivi per impostazione predefinita e devono essere contrassegnati in modo esplicito come virtual
  • Forniscono eredità privata che non eredita l'interfaccia ma solo l'implementazione.
  • Richiedono che i metodi della classe base siano contrassegnati come virtuali e che anche tutti gli override siano contrassegnati. Questo evita problemi in cui una sottoclasse ha definito un nuovo metodo, ma un metodo con la stessa firma è stato successivamente aggiunto alla classe base ma non inteso come virtuale.

3
Ti meriti almeno +100 per menzionare il "fragile problema della classe base". :)
David Arno,

6
Non sono convinto dai punti sollevati qui. Sì, la fragile classe base è un problema, ma final non risolve tutti i problemi con la modifica di un'implementazione. Il tuo primo esempio è negativo perché stai presupponendo di conoscere tutti i possibili casi d'uso per PasswordChecker ("bloccare tutti o consentire l'accesso a tutti ... non va bene" - dice chi?). La tua ultima lista "quindi consigliabile ..." è davvero pessima - fondamentalmente stai sostenendo di non estendere nulla e di contrassegnare tutto come definitivo - che annulla completamente l'utilità di OOP, eredità e riutilizzo del codice.
adelphus,

4
Il tuo primo esempio non è un esempio del fragile problema della classe base. Nel fragile problema della classe base, le modifiche a una classe base interrompono una sottoclasse. Ma in questo esempio, la tua sottoclasse non segue il contratto di una sottoclasse. Questi sono due problemi diversi. (Inoltre, in realtà è ragionevole che in determinate circostanze, si potrebbe disabilitare un controllo password (diciamo per lo sviluppo))
Winston Ewert,

5
"Evita il fragile problema della classe di base" - nello stesso modo in cui uccidere te stesso evita di avere fame.
user253751,

9
@immibis, è più come evitare di mangiare per evitare di avvelenare il cibo. Certo, mai mangiare sarebbe un problema. Ma mangiare solo in quei luoghi di cui ti fidi ha molto senso.
Winston Ewert,

33

Sono sorpreso che nessuno abbia ancora menzionato Effective Java, 2nd Edition di Joshua Bloch (che almeno dovrebbe essere richiesto per ogni sviluppatore Java). L'articolo 17 del libro ne discute dettagliatamente ed è intitolato: " Progettare e documentare per ereditarietà o altrimenti vietarlo ".

Non ripeterò tutti i buoni consigli nel libro, ma questi particolari paragrafi sembrano rilevanti:

Ma che dire delle normali lezioni concrete? Tradizionalmente, non sono né definitivi né progettati e documentati per la sottoclasse, ma questo stato di cose è pericoloso. Ogni volta che viene apportata una modifica in tale classe, c'è la possibilità che le classi client che estendono la classe si interrompano. Questo non è solo un problema teorico. Non è raro ricevere segnalazioni di bug relative alla sottoclasse dopo aver modificato gli interni di una classe concreta non definitiva che non è stata progettata e documentata per l'ereditarietà.

La migliore soluzione a questo problema è proibire la sottoclasse in classi che non sono progettate e documentate per essere sottoclassate in sicurezza. Esistono due modi per vietare la sottoclasse. Il più semplice dei due è dichiarare la finale di classe. L'alternativa è rendere tutti i costruttori privati ​​o pacchetti-privati ​​e aggiungere fabbriche statiche pubbliche al posto dei costruttori. Questa alternativa, che offre la flessibilità di utilizzare le sottoclassi internamente, è discussa al punto 15. Entrambi gli approcci sono accettabili.


21

Uno dei motivi finalè utile perché si assicura che non si possa sottoclassare una classe in modo tale da violare il contratto della classe genitore. Tale sottoclasse sarebbe una violazione di SOLID (soprattutto "L") e la creazione di una classe lo finalimpedisce.

Un esempio tipico è rendere impossibile la sottoclasse di una classe immutabile in un modo tale da rendere mutevole la sottoclasse. In alcuni casi, un tale cambiamento di comportamento potrebbe portare a effetti molto sorprendenti, ad esempio quando si utilizza qualcosa come chiavi in ​​una mappa pensando che la chiave sia immutabile mentre in realtà si sta utilizzando una sottoclasse che è mutabile.

In Java, molti problemi di sicurezza interessanti potrebbero essere introdotti se si fosse in grado di sottoclassare Stringe renderlo mutabile (o farlo richiamare a casa quando qualcuno chiama i suoi metodi, quindi estraendo i dati sensibili dal sistema) quando questi oggetti vengono passati intorno ad alcuni codici interni relativi al caricamento della classe e alla sicurezza.

Final è anche utile a volte per prevenire semplici errori come il riutilizzo della stessa variabile per due cose all'interno di un metodo, ecc. In Scala, sei incoraggiato a usare solo ciò valche corrisponde approssimativamente alle variabili finali in Java, e in realtà qualsiasi uso di un varo la variabile non finale viene esaminata con sospetto.

Infine, i compilatori possono, almeno in teoria, eseguire alcune ottimizzazioni extra quando sanno che una classe o un metodo è finale, poiché quando chiami un metodo su una classe finale sai esattamente quale metodo verrà chiamato e non devi andare tramite la tabella dei metodi virtuali per verificare l'ereditarietà.


6
Infine, i compilatori possono, almeno in teoria => ho rivisto personalmente il passaggio di devirtualizzazione di Clang e confermo che è utilizzato nella pratica.
Matthieu M.,

Ma il compilatore non può dire in anticipo che nessuno sta scavalcando una classe o un metodo indipendentemente dal fatto che sia segnato o meno il finale?
JesseTG,

3
@JesseTG Se ha accesso a tutto il codice in una volta, probabilmente. Che dire della compilazione di file separati, però?
Ripristina Monica

3
La devirtualizzazione di @JesseTG o la memorizzazione nella cache (monomorfa / polimorfica) in linea è una tecnica comune nei compilatori JIT, poiché il sistema sa quali classi sono attualmente caricate e può deoptimizzare il codice se l'assunzione di metodi non prevalenti risulta falsa. Tuttavia, un compilatore in anticipo non può. Quando compilo una classe Java e un codice che utilizza quella classe, posso successivamente compilare una sottoclasse e passare un'istanza al codice che consuma. Il modo più semplice è aggiungere un altro vaso nella parte anteriore del percorso di classe. Il compilatore non può sapere tutto questo, poiché ciò accade in fase di esecuzione.
amon,

5
@MTilsted Puoi supporre che le stringhe siano immutabili poiché le stringhe mutanti tramite la riflessione possono essere vietate tramite a SecurityManager. La maggior parte dei programmi non lo utilizza, ma non esegue alcun codice non attendibile. +++ Devi supporre che le stringhe siano immutabili poiché altrimenti otterrai zero sicurezza e un sacco di bug arbitrari come bonus. Qualsiasi programmatore che presupponga che le stringhe Java possano cambiare nel codice produttivo è sicuramente folle.
maaartinus,

7

Il secondo motivo è la prestazione . Il primo motivo è perché alcune classi hanno comportamenti o stati importanti che non dovrebbero essere modificati per consentire al sistema di funzionare. Ad esempio, se ho una classe "PasswordCheck" e per costruire quella classe ho assunto un team di esperti di sicurezza e questa classe comunica con centinaia di sportelli automatici con procedure ben studiate e definite. Consenti a un nuovo assunto appena uscito dall'università di creare una classe "TrustMePasswordCheck" che estenda la classe di cui sopra potrebbe essere molto dannosa per il mio sistema; quei metodi non dovrebbero essere sovrascritti, tutto qui.



La JVM è abbastanza intelligente da considerare le classi come finali se non hanno sottoclassi, anche se non sono dichiarate finali.
user253751,

Sottovalutato perché la richiesta di "prestazione" è stata smentita così tante volte.
Paul Smith,

7

Quando avrò bisogno di una lezione, scriverò una lezione. Se non ho bisogno di sottoclassi, non mi interessano le sottoclassi. Mi assicuro che la mia classe si comporti come previsto, e i luoghi in cui uso la classe presumono che la classe si comporti come previsto.

Se qualcuno vuole sottoclassare la mia classe, voglio negare completamente qualsiasi responsabilità per ciò che accade. Lo raggiungo rendendo la classe "finale". Se vuoi sottoclassarlo, ricorda che non ho preso in considerazione la sottoclasse mentre scrivevo la lezione. Quindi devi prendere il codice sorgente della classe, rimuovere il "finale", e da quel momento in poi tutto ciò che accade sarà completamente sotto la tua responsabilità .

Pensi che "non sia orientato agli oggetti"? Sono stato pagato per fare una lezione che fa quello che dovrebbe fare. Nessuno mi ha pagato per aver fatto una classe che poteva essere sottoclassata. Se vieni pagato per rendere riutilizzabile la mia classe, sei invitato a farlo. Inizia rimuovendo la parola chiave "finale".

(A parte questo, "final" spesso consente sostanziali ottimizzazioni. Ad esempio, in Swift "final" su una classe pubblica o su un metodo di una classe pubblica, significa che il compilatore può conoscere appieno quale codice verrà eseguito da una chiamata al metodo, e può sostituire la spedizione dinamica con spedizione statica (piccolo vantaggio) e spesso sostituire la spedizione statica con inline (possibilmente enorme vantaggio)).

adelphus: cosa c'è di così difficile da capire su "se vuoi sottoclassarlo, prendi il codice sorgente, rimuovi il 'finale' ed è tua responsabilità"? "finale" equivale a "giusto avvertimento".

E sto non pagato per rendere il codice riutilizzabile. Sono pagato per scrivere codice che fa quello che dovrebbe fare. Se sono pagato per creare due parti di codice simili, estraggo le parti comuni perché è più economico e non sono pagato per perdere tempo. Rendere il codice riutilizzabile che non viene riutilizzato è una perdita del mio tempo.

M4ks: Rendi sempre tutto privato a cui non è necessario accedere dall'esterno. Ancora una volta, se vuoi sottoclassare, prendi il codice sorgente, cambi le cose in "protetto" se ne hai bisogno e ti prendi la responsabilità di ciò che fai. Se ritieni di dover accedere a cose che ho contrassegnato come private, è meglio sapere cosa stai facendo.

Entrambi: la sottoclasse è una porzione minuscola di riutilizzo del codice. La creazione di blocchi predefiniti che possono essere adattati senza la sottoclasse è molto più potente e beneficia enormemente del "finale" perché gli utenti dei blocchi possono fare affidamento su ciò che ottengono.


4
-1 Questa risposta descrive tutto ciò che è sbagliato con gli sviluppatori di software. Se qualcuno vuole riutilizzare la tua classe tramite la sottoclasse, lascialo. Perché sarebbe tua responsabilità il modo in cui lo usano (o abusano)? Fondamentalmente, stai usando final come af , non stai usando la mia classe. * "Nessuno mi ha pagato per aver creato una classe che poteva essere sottoclassata" . Sei serio? Questo è esattamente il motivo per cui vengono impiegati ingegneri del software - per creare codice solido e riutilizzabile.
adelphus,

4
-1
Rendi

3
@adelphus Mentre il testo di questa risposta è schietto, al limite del duro, non è un punto di vista "sbagliato". In effetti è lo stesso punto di vista della maggior parte delle risposte a questa domanda finora solo con un tono meno clinico.
NemesisX00

+1 per menzionare che è possibile rimuovere "final". È arrogante rivendicare la conoscenza preliminare di tutti i possibili usi del codice. Tuttavia è umile chiarire che non è possibile mantenere alcuni possibili usi e che tali usi richiederebbero il mantenimento di una forcella.
Gmatht

4

Immaginiamo che l'SDK per una piattaforma spedisca la seguente classe:

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

Un'applicazione subclasse questa classe:

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

Tutto va bene e va bene, ma qualcuno che lavora sull'SDK decide che passare un metodo a getè stupido e rende l'interfaccia migliore assicurandosi di far rispettare la compatibilità all'indietro.

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);
}

Tutto sembra a posto, finché l'applicazione dall'alto non viene ricompilata con il nuovo SDK. Improvvisamente, il metodo get overriden non viene più chiamato e la richiesta non viene conteggiata.

Questo è chiamato il fragile problema della classe base, perché un cambiamento apparentemente innoco provoca una rottura della sottoclasse. Ogni volta che una modifica a quali metodi vengono chiamati all'interno della classe potrebbe causare l'interruzione di una sottoclasse. Ciò tende a significare che quasi ogni modifica potrebbe causare la rottura di una sottoclasse.

Final impedisce a chiunque di sottoclassare la tua classe. In questo modo, quali metodi all'interno della classe possono essere modificati senza preoccuparsi che da qualche parte qualcuno dipenda esattamente da quali chiamate di metodo vengono effettuate.


1

Final significa effettivamente che la tua classe è sicura da cambiare in futuro senza influire su alcuna classe basata sull'ereditarietà a valle (perché non ce ne sono) o su eventuali problemi relativi alla sicurezza dei thread della classe (penso che ci siano casi in cui la parola chiave finale su un campo impedisce alcuni jinx basati su thread).

Final significa che sei libero di cambiare il modo in cui funziona la tua classe senza cambiamenti involontari nel comportamento che si insinuano nel codice di altre persone che si basa sul tuo come base.

Ad esempio, scrivo una classe chiamata HobbitKiller, il che è fantastico, perché tutti gli hobbit sono trucchi e probabilmente dovrebbero morire. Grattalo, devono assolutamente morire.

Lo usi come una classe base e aggiungi un nuovo fantastico metodo per usare un lanciafiamme, ma usi la mia classe come base perché ho un ottimo metodo per prendere di mira gli hobbit (oltre ad essere trucchi, sono veloci), che usi per aiutare a mirare il tuo lanciafiamme.

Tre mesi dopo cambio l'implementazione del mio metodo di targeting. Ora, ad un certo punto futuro quando aggiorni la tua libreria, a tua insaputa, l'implementazione di runtime effettiva della tua classe è sostanzialmente cambiata a causa di una modifica del metodo della super classe da cui dipendi (e generalmente non controlli).

Quindi, per essere uno sviluppatore coscienzioso e garantire la morte senza problemi dello hobbit nel futuro usando la mia classe, devo stare molto, molto attento con tutte le modifiche che faccio a tutte le classi che possono essere estese.

Rimuovendo la possibilità di estendere, tranne nei casi in cui ho intenzione di estendere la classe, mi salva un sacco di mal di testa (e spero altri).


2
Se il tuo metodo di targeting cambierà perché lo hai mai reso pubblico? E se cambi il comportamento di una classe in modo significativo hai bisogno di un'altra versione piuttosto che iperprotettivafinal
M4ks,

Non ho idea del perché questo cambierebbe. Gli hobbit sono trucchi e il cambiamento era necessario. Il punto è che se lo costruisco come finale impedisco l'eredità che protegge le altre persone dal fatto che le mie modifiche infettino il loro codice.
Scott Taylor,

0

L'uso di finalnon è in alcun modo una violazione dei principi SOLIDI. Sfortunatamente, è estremamente comune interpretare il principio aperto / chiuso ("le entità software dovrebbero essere aperte per estensione ma chiuse per modifica") come significato "piuttosto che modificare una classe, sottoclassarla e aggiungere nuove funzionalità". Questo non è ciò che originariamente intendeva con esso, ed è generalmente ritenuto non essere l'approccio migliore per raggiungere i suoi obiettivi.

Il modo migliore per conformarsi all'OCP è progettare punti di estensione in una classe, fornendo in modo specifico comportamenti astratti che sono parametrizzati iniettando una dipendenza per l'oggetto (ad esempio utilizzando il modello di progettazione della strategia). Questi comportamenti dovrebbero essere progettati per utilizzare un'interfaccia in modo che le nuove implementazioni non si basino sull'ereditarietà.

Un altro approccio è implementare la tua classe con la sua API pubblica come classe (o interfaccia) astratta. È quindi possibile produrre un'implementazione completamente nuova che può essere collegata agli stessi client. Se la tua nuova interfaccia richiede un comportamento sostanzialmente simile all'originale, puoi:

  • utilizzare il modello di progettazione di Decorator per riutilizzare il comportamento esistente dell'originale, oppure
  • riformattare le parti del comportamento che si desidera mantenere in un oggetto helper e utilizzare lo stesso helper nella nuova implementazione (il refactoring non è una modifica).

0

Per me è una questione di design.

Supponiamo che io abbia un programma che calcola gli stipendi per i dipendenti. Se ho una classe che restituisce il numero di giorni lavorativi tra 2 date in base al paese (una classe per ciascun paese), inserirò quella finale e fornirò un metodo per ogni azienda per fornire un giorno gratuito solo per i loro calendari.

Perché? Semplice. Diciamo che uno sviluppatore vuole ereditare la classe base WorkingDaysUSA in una classe WorkingDaysUSAmyCompany e modificarla per riflettere che la sua impresa sarà chiusa per sciopero / manutenzione / qualunque sia il motivo per il 2 ° di Marte.

I calcoli per gli ordini e la consegna dei clienti rifletteranno il ritardo e funzioneranno di conseguenza quando in fase di esecuzione chiamano WorkingDaysUSAmyCompany.getWorkingDays (), ma cosa succede quando calcolo il tempo delle vacanze? Dovrei aggiungere il 2 ° di Marte come vacanza per tutti? No. Ma dal momento che il programmatore ha usato l'ereditarietà e non ho protetto la classe, questo può creare confusione.

Oppure diciamo che ereditano e modificano la classe per riflettere che questa azienda non lavora il sabato, mentre nel paese lavora a metà tempo il sabato. Quindi un terremoto, una crisi dell'elettricità o alcune circostanze fanno sì che il presidente dichiari 3 giorni non lavorativi come è successo di recente in Venezuela. Se il metodo della classe ereditata viene già sottratto ogni sabato, le mie modifiche sulla classe originale potrebbero portare a sottrarre lo stesso giorno due volte. Dovrei andare a ciascuna sottoclasse su ciascun client e verificare che tutte le modifiche siano compatibili.

Soluzione? Rendi la classe finale e fornisci un metodo addFreeDay (companyID mycompany, Date freeDay). In questo modo sei sicuro che quando chiami una classe WorkingDaysCountry è la tua classe principale e non una sottoclasse

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.