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.