Cela évite le problème de la classe de base fragile . Chaque classe est livrée avec un ensemble de garanties et d'invariants implicites ou explicites. Le principe de substitution de Liskov stipule que tous les sous-types de cette classe doivent également fournir toutes ces garanties. Cependant, il est vraiment facile de violer cela si nous n'utilisons pas final
. Par exemple, établissons un vérificateur de mot de passe:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Si nous permettons à cette classe d'être remplacée, une implémentation pourrait verrouiller tout le monde, une autre pourrait donner à tout le monde l'accès:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Cela n’est généralement pas acceptable, car les sous-classes ont maintenant un comportement très incompatible avec le comportement initial. Si nous souhaitons réellement que la classe soit étendue à un autre comportement, une chaîne de responsabilité serait préférable:
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;
}
}
Le problème devient plus évident quand plus d'une classe compliquée appelle ses propres méthodes, et ces méthodes peuvent être remplacées. Je rencontre parfois cela lorsque joliment imprimer une structure de données ou écrire du HTML. Chaque méthode est responsable d'un 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>");
}
...
}
Je crée maintenant une sous-classe qui ajoute un peu plus de style:
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>");
}
}
Ignorant pour l'instant que ce n'est pas un très bon moyen de générer des pages HTML, que se passe-t-il si je souhaite modifier à nouveau la mise en page? Je devrais créer une SpiffyPage
sous-classe qui en quelque sorte encapsule ce contenu. Ce que nous pouvons voir ici est une application accidentelle du modèle de méthode template. Les méthodes de modèle sont des points d'extension bien définis dans une classe de base destinés à être remplacés.
Et que se passe-t-il si la classe de base change? Si le contenu HTML change trop, cela pourrait endommager la mise en page fournie par les sous-classes. Il n’est donc pas très prudent de changer de classe de base par la suite. Cela n'est pas évident si toutes vos classes sont dans le même projet, mais très visible si la classe de base fait partie d'un logiciel publié sur lequel d'autres personnes s'appuient.
Si cette stratégie d’extension était prévue, nous aurions pu permettre à l’utilisateur d’échanger la manière dont chaque partie est générée. Soit une stratégie pour chaque bloc pouvant être fournie à l’extérieur. Ou, nous pourrions nidifier les décorateurs. Cela serait équivalent au code ci-dessus, mais beaucoup plus explicite et beaucoup plus flexible:
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);
...
}
}
(Le top
paramètre supplémentaire est nécessaire pour garantir que les appels writeMainContent
passent par le haut de la chaîne de décorateurs. Cela émule une fonctionnalité de sous-classement appelée récursion ouverte .)
Si nous avons plusieurs décorateurs, nous pouvons maintenant les mélanger plus librement.
Bien plus souvent que le désir d'adapter légèrement les fonctionnalités existantes est le désir de réutiliser une partie d'une classe existante. J'ai vu un cas où quelqu'un voulait une classe où vous pouvez ajouter des éléments et les parcourir tous. La bonne solution aurait été de:
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
}
Au lieu de cela, ils ont créé une sous-classe:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
Cela signifie soudainement que toute l'interface de ArrayList
est devenue une partie de notre interface. Les utilisateurs peuvent des remove()
choses, ou des get()
choses à des index spécifiques. C'était prévu comme ça? D'ACCORD. Mais souvent, nous ne réfléchissons pas soigneusement à toutes les conséquences.
Il est donc conseillé de
- jamais
extend
une classe sans réflexion.
- marquez toujours vos classes comme
final
sauf si vous souhaitez que l'une des méthodes soit remplacée.
- créez des interfaces sur lesquelles vous souhaitez échanger une implémentation, par exemple pour les tests unitaires.
Il existe de nombreux exemples dans lesquels cette «règle» doit être enfreinte, mais elle vous guide généralement vers une conception flexible et de bonne qualité, et évite les bogues dus à des modifications inattendues dans les classes de base (ou à des utilisations non intentionnelles de la sous-classe en tant qu'instance de la classe de base. ).
Certaines langues ont des mécanismes d'application plus stricts:
- Toutes les méthodes sont finales par défaut et doivent être marquées explicitement comme
virtual
- Ils fournissent un héritage privé qui n'hérite pas de l'interface mais seulement de la mise en œuvre.
- Ils exigent que les méthodes de la classe de base soient marquées comme virtuelles et que tous les remplacements soient également marqués. Cela évite les problèmes dans lesquels une sous-classe définit une nouvelle méthode, mais une méthode avec la même signature est ensuite ajoutée à la classe de base, mais n'est pas conçue comme virtuelle.
final
? Beaucoup de gens (y compris moi-même) trouvent que c'est un bon design pour faire chaque classe non abstraitefinal
.