Perché la parola chiave "finale" dovrebbe mai essere utile?

54

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

    
posta Vorac 12.05.2016 - 10:33
fonte

10 risposte

134

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

Questo è importante perché l'architettura del programma sarebbe davvero molto difficile se si dovesse anticipare che ogni classe e ogni metodo che si scrive potrebbe essere cambiato per fare qualcosa di completamente diverso da una sottoclasse. È molto meglio decidere in anticipo quali elementi dovrebbero essere modificabili e quali no, e per far rispettare l'immutabilità attraverso final .

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

    
risposta data 12.05.2016 - 10:48
fonte
58

Evita il Problema di classe base fragile . Ogni classe ha un insieme di garanzie implicite o esplicite e invarianti. Il Principio di sostituzione di Liskov richiede che tutti i sottotipi di quella classe debbano anche fornire tutte queste garanzie. Tuttavia, è davvero facile violare questo se non usiamo final . Ad esempio, disponiamo di un controllo password:

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

Se permettiamo che la classe venga sovrascritta, un'implementazione potrebbe bloccare tutti, un'altra potrebbe consentire a tutti l'accesso:

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

Questo di solito non è OK, dal momento che le sottoclassi ora hanno un comportamento che è molto incompatibile con l'originale. Se intendiamo davvero estendere la classe ad altri comportamenti, una catena di responsabilità sarebbe meglio:

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 sovrascritti. A volte mi imbatto in questo quando stampo in modo piuttosto carino una struttura dati o scrivendo 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 ancora il layout? Dovrei creare una sottoclasse SpiffyPage che avvolga in qualche modo quel contenuto. Quello che possiamo vedere qui è un'applicazione accidentale del modello di metodo del modello. I metodi template 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. Non è quindi molto sicuro cambiare la classe base in seguito. Questo non è evidente se tutte le tue classi sono nello stesso progetto, ma molto evidenti se la classe base fa parte di un software pubblicato su cui altre persone si basano.

Se questa strategia di estensione era intesa, avremmo potuto consentire all'utente di scambiare il modo in cui ogni parte è generata. O, potrebbe esserci una strategia per ogni blocco che può essere fornita esternamente. Oppure potremmo nidificare Decoratori. Questo sarebbe equivalente al codice precedente, 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 parametro aggiuntivo top è necessario per assicurarsi che le chiamate a writeMainContent passino attraverso la parte superiore della catena di decoratori. Questo emula una funzione di sottoclasse chiamata ricorsione aperta .)

Se disponiamo di più decoratori, ora possiamo mescolarli più liberamente.

Molto più spesso del desiderio di adattare leggermente le funzionalità esistenti è il desiderio di riutilizzare parte di una classe esistente. Ho visto un caso in cui qualcuno voleva un corso in cui si potessero aggiungere articoli e iterare su tutti loro. 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
}

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

È quindi consigliabile

  • mai extend una classe senza un'attenta riflessione.
  • contrassegna sempre le tue classi come final eccetto se intendi che un metodo venga sovrascritto.
  • crea interfacce in cui desideri scambiare un'implementazione, ad es. per il test dell'unità.

Ci sono molti esempi in cui questa "regola" deve essere infranta, ma di solito ti guida ad un design buono e flessibile, ed evita bug dovuti a modifiche involontarie nelle classi di base (o usi non voluti della sottoclasse come un'istanza di la classe base).

Alcune lingue hanno meccanismi di applicazione più severi:

  • Tutti i metodi sono definitivi per impostazione predefinita e devono essere contrassegnati esplicitamente come virtual
  • Forniscono ereditarietà privata che non eredita l'interfaccia ma solo l'implementazione.
  • Richiedono che i metodi della classe base vengano contrassegnati come virtuali e richiedono anche la marcatura di tutti gli override. 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.
risposta data 12.05.2016 - 12:30
fonte
32

Sono sorpreso che nessuno abbia ancora menzionato Java efficace, 2a edizione di Joshua Bloch (che dovrebbe essere richiesto per la lettura almeno per ogni sviluppatore Java). L'articolo 17 del libro discute questo in dettaglio ed è intitolato: " Progetta e documenta l'ereditarietà o vietalo ".

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

But what about ordinary concrete classes? Traditionally, they are neither final nor designed and documented for subclassing, but this state of affairs is dangerous. Each time a change is made in such a class, there is a chance that client classes that extend the class will break. This is not just a theoretical problem. It is not uncommon to receive subclassing-related bug reports after modifying the internals of a nonfinal concrete class that was not designed and documented for inheritance.

The best solution to this problem is to prohibit subclassing in classes that are not designed and documented to be safely subclassed. There are two ways to prohibit subclassing. The easier of the two is to declare the class final. The alternative is to make all the constructors private or package-private and to add public static factories in place of the constructors. This alternative, which provides the flexibility to use subclasses internally, is discussed in Item 15. Either approach is acceptable.

    
risposta data 12.05.2016 - 20:57
fonte
21

Uno dei motivi per cui final è utile è che ci si assicura che non è possibile sottoclasse una classe in modo tale da violare il contratto della classe genitore. Tale sottoclasse sarebbe una violazione di SOLID (soprattutto di "L") e fare in modo che una classe final lo impedisca.

Un tipico esempio sta rendendo impossibile la sottoclasse di una classe immutabile in un modo che renderebbe la sottoclasse mutabile. In certi casi un tale cambiamento di comportamento potrebbe portare a effetti molto sorprendenti, ad esempio quando si usa qualcosa come chiave in una mappa pensando che la chiave sia immutabile mentre in realtà si sta utilizzando una sottoclasse che è mutabile.

In Java, potrebbero essere introdotti molti problemi di sicurezza interessanti se si fosse in grado di suddividere String e renderlo mutabile (o di farlo chiamare a casa quando qualcuno chiama i suoi metodi, quindi estrarre dati sensibili dal sistema ) poiché questi oggetti vengono passati attorno a un codice interno relativo al caricamento e alla sicurezza della classe.

Finalmente è utile anche a prevenire errori semplici come riutilizzare la stessa variabile per due cose all'interno di un metodo, ecc. In Scala, sei incoraggiato a usare solo val che corrisponde approssimativamente alle variabili finali in Java, e in realtà qualsiasi uso di una variabile var o non finale viene guardato con sospetto.

Infine, i compilatori possono, almeno in teoria, eseguire alcune ottimizzazioni extra quando sanno che una classe o un metodo è definitivo, poiché quando si chiama un metodo su una classe finale si conosce esattamente quale metodo verrà chiamato e non si passare attraverso la tabella dei metodi virtuali per controllare l'ereditarietà.

    
risposta data 12.05.2016 - 11:43
fonte
7

Il secondo motivo è performance . La prima ragione è 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 bancomat con procols ben studiati e definiti. Permettere a un nuovo assunto fuori dall'università di fare una classe "TrustMePasswordCheck" che estenda la classe sopra potrebbe essere molto dannosa per il mio sistema; quei metodi non dovrebbero essere sovrascritti, è così.

    
risposta data 12.05.2016 - 11:06
fonte
7

Quando ho bisogno di un corso, scriverò un corso. Se non ho bisogno di sottoclassi, non mi importa delle sottoclassi. Mi assicuro che la mia classe si comporti come previsto, e i luoghi in cui utilizzo la classe presumono che la classe si comporti come previsto.

Se qualcuno vuole sottoclasse la mia classe, voglio negare completamente ogni responsabilità per ciò che accade. Lo realizzo rendendo la classe "finale". Se vuoi creare una sottoclasse, 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 è pienamente la tua responsabilità .

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

(Oltre a questo, "final" spesso consente sostanziali ottimizzazioni. Ad esempio, in Swift "final" su una public class, o su un metodo di una public class, significa che il compilatore può sapere completamente quale codice chiama un metodo eseguirà, e può sostituire il dispatch dinamico con dispatch statico (piccolo vantaggio) e spesso sostituirà il dispatch statico con l'inlining (possibilmente un enorme vantaggio)).

adelphus: Che cosa è così difficile da capire su "se vuoi sottoclassi, prendi il codice sorgente, rimuovi il 'finale', ed è una tua responsabilità"? "final" equivale a "fair warning".

E io sono non pagato per rendere il codice riusabile. Sono pagato per scrivere codice che fa quello che dovrebbe fare. Se sono pagato per creare due bit di codice simili, estrao le parti comuni perché è più economico e non sono pagato per perdere tempo. Rendere il codice riutilizzabile che non è riutilizzato è uno spreco del mio tempo.

M4ks: sempre rende tutto ciò che è privato a cui non si dovrebbe accedere dall'esterno. Di nuovo, se vuoi sottoclasse, prendi il codice sorgente, cambia le cose in "protetto" se ne hai bisogno, e prenditi la responsabilità di ciò che fai. Se pensi di aver bisogno di accedere a cose che ho contrassegnato come private, è meglio sapere cosa stai facendo.

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

    
risposta data 13.05.2016 - 10:49
fonte
4

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

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

Un'applicazione sottoclassa questa classe:

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

Tutto va bene e bene, ma qualcuno che lavora su SDK decide che passare un metodo a get è sciocco, e rende l'interfaccia migliore assicurandosi di applicare la compatibilità con le versioni precedenti.

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, fino a quando l'applicazione da sopra non viene ricompilata con il nuovo SDK. All'improvviso, il metodo get overriden non viene più chiamato e la richiesta non viene contata.

Questo è chiamato il fragile problema della classe base, perché un cambiamento apparentemente innocuo si traduce in una rottura della sottoclasse. Ogni volta che si cambia in quali metodi vengono chiamati all'interno della classe si può causare un'interruzione della sottoclasse. Questo tende a significare che quasi ogni cambiamento potrebbe causare la rottura di una sottoclasse.

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

    
risposta data 13.05.2016 - 00:58
fonte
1

Final in modo efficace significa che la tua classe è sicura di cambiare in futuro senza influire sulle classi di ereditarietà downstream (perché non ce ne sono) o su problemi relativi alla sicurezza dei thread della classe (penso che ci siano casi in cui la parola chiave finale su un campo impedisce l'innesco di alcuni jinx basati su thread).

Final significa che sei libero di cambiare il funzionamento della tua classe senza modifiche involontarie del comportamento che si insinuano nel codice di altre persone che si basa sul tuo come base.

Ad esempio, scrivo una classe chiamata HobbitKiller, che è grandiosa, perché tutti gli hobbit sono trucchi e probabilmente dovrebbero morire. Gratta che, tutti hanno sicuramente bisogno di morire.

Usi questo come una classe base e aggiungi un nuovo metodo fantastico per usare un lanciafiamme, ma usa la mia classe come base perché ho un ottimo metodo per bersagliare gli hobbit (oltre ad essere trucchetti, sono veloci ), che usi per mirare al tuo lanciafiamme.

Tre mesi dopo cambio l'implementazione del mio metodo di targeting. Ora, in futuro, quando esegui l'upgrade della tua libreria, a tua insaputa, l'effettiva implementazione del runtime della tua classe è fondamentalmente cambiata a causa di una modifica del metodo di super classe da cui dipendi (e generalmente non controlla).

Quindi, per me, essere uno sviluppatore coscienzioso e assicurare una morte omogenea verso il futuro usando la mia classe, devo essere molto, molto attento a qualsiasi modifica che io possa apportare a qualsiasi classe che possa essere estesa.

Rimuovendo la possibilità di estendere, tranne nei casi in cui intendo specificamente estendere la classe, salvo me stesso (e, si spera, gli altri) un sacco di mal di testa.

    
risposta data 12.05.2016 - 17:37
fonte
0

L'uso di final non è in alcun modo una violazione dei principi SOLID. Sfortunatamente, è estremamente comune interpretare il Principio Aperto / Chiuso ("le entità software dovrebbero essere aperte per estensione ma chiuse per la modifica") nel senso di "piuttosto che modificare una classe, sottoclassi e aggiungere nuove funzionalità". Questo non è ciò che originariamente intendeva per esso, e in genere non è ritenuto l'approccio migliore per raggiungere i suoi obiettivi.

Il modo migliore per ottemperare a OCP è progettare i punti di estensione in una classe, specificando specificamente comportamenti astratti che sono parametrizzati iniettando una dipendenza all'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 consiste nell'implementare la classe con la sua API pubblica come classe astratta (o interfaccia). È 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:

  • usa il pattern di progettazione Decorator per riutilizzare il comportamento esistente dell'originale, o
  • ridefinisce le parti del comportamento che si desidera conservare in un oggetto helper e utilizza lo stesso helper nella nuova implementazione (il refactoring non è una modifica).
risposta data 16.05.2016 - 10:19
fonte
0

Per me è una questione di design.

Supponiamo di avere 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 ogni paese), inserirò quella finale e fornirò un metodo per ogni azienda per fornire un giorno libero solo per i loro calendari. / p>

Perché? Semplice. Supponiamo che uno sviluppatore voglia ereditare la classe base WorkingDaysUSA in una classe WorkingDaysUSAmyCompany e modificarla per riflettere che la sua impresa sarà chiusa per strike / manutenzione / qualunque sia il motivo del secondo di mars.

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 si calcola l'orario delle vacanze? Dovrei aggiungere il 2 di Marte come una vacanza per tutti? No. Ma dal momento che il programmatore ha usato l'ereditarietà e io non ho protetto la classe, questo può portare a una confusione.

Oppure diciamo che ereditano e modificano la classe per riflettere che questa azienda non lavora il sabato dove nel paese lavorano a metà tempo sabato. Poi un terremoto, una crisi dell'elettricità o qualche circostanza fa dichiarare al presidente 3 giorni non lavorativi come è successo recentemente in Venezuela. Se il metodo della classe ereditata è già stato sottratto ogni sabato, le mie modifiche alla classe originale potrebbero portare a sottrarre lo stesso giorno due volte. Dovrei andare in ciascuna sottoclasse di ciascun client e verificare che tutte le modifiche siano compatibili.

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

    
risposta data 12.05.2016 - 17:43
fonte

Leggi altre domande sui tag