Evita el Problema de clase base frágil . Cada clase viene con un conjunto de garantías e invariantes implícitas o explícitas. El principio de sustitución de Liskov exige que todos los subtipos de esa clase también deben proporcionar todas estas garantías. Sin embargo, es realmente fácil violar esto si no usamos final
. Por ejemplo, tengamos un verificador de contraseñas:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Si permitimos que esa clase se anule, una implementación podría bloquear a todos, otra podría dar acceso a todos:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Esto generalmente no está bien, ya que las subclases ahora tienen un comportamiento que es muy incompatible con el original. Si realmente pretendemos que la clase se extienda con otro comportamiento, una Cadena de Responsabilidad sería mejor:
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;
}
}
El problema se vuelve más evidente cuando una clase más complicada llama a sus propios métodos, y esos métodos pueden ser anulados. A veces encuentro esto cuando imprimo bastante una estructura de datos o escribe HTML. Cada método es responsable de algún 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>");
}
...
}
Ahora creo una subclase que agrega un poco más de estilo:
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>");
}
}
Ahora, ignorando por un momento que esta no es una buena forma de generar páginas HTML, ¿qué sucede si deseo cambiar el diseño una vez más? Tendría que crear una subclase SpiffyPage
que de alguna manera envuelva ese contenido. Lo que podemos ver aquí es una aplicación accidental del patrón de método de plantilla. Los métodos de plantilla son puntos de extensión bien definidos en una clase base que están destinados a ser anulados.
¿Y qué pasa si la clase base cambia? Si los contenidos HTML cambian demasiado, esto podría romper el diseño provisto por las subclases. Por lo tanto, no es realmente seguro cambiar la clase base después. Esto no es evidente si todas sus clases están en el mismo proyecto, pero es muy notable si la clase base es parte de algún software publicado que otras personas construyen.
Si se pretendía esta estrategia de extensión, podríamos haber permitido al usuario cambiar la forma en que se genera cada parte. O bien, podría haber una Estrategia para cada bloque que se pueda proporcionar externamente. O podríamos anidar a los decoradores. Esto sería equivalente al código anterior, pero mucho más explícito y mucho más 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);
...
}
}
(El parámetro top
adicional es necesario para asegurarse de que las llamadas a writeMainContent
pasan por la parte superior de la cadena de decoradores. Esto emula una característica de subclasificación llamada recursión abierta .)
Si tenemos varios decoradores, ahora podemos mezclarlos más libremente.
Mucho más frecuente que el deseo de adaptar ligeramente la funcionalidad existente es el deseo de reutilizar alguna parte de una clase existente. He visto un caso en el que alguien quería una clase en la que pudiera agregar elementos e iterarlos todos. La solución correcta habría sido:
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
}
En su lugar, crearon una subclase:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
De repente, esto significa que toda la interfaz de ArrayList
se ha convertido en parte de nuestra . Los usuarios pueden remove()
things, o get()
things en índices específicos. Esto fue pensado de esa manera? DE ACUERDO. Pero a menudo, no pensamos cuidadosamente en todas las consecuencias.
Por lo tanto, es aconsejable
- nunca
extend
una clase sin pensarlo detenidamente.
- siempre marque sus clases como
final
, excepto si tiene la intención de anular cualquier método.
- cree interfaces donde desee intercambiar una implementación, por ejemplo, para pruebas unitarias.
Hay muchos ejemplos en los que se debe romper esta "regla", pero por lo general lo guía a un diseño bueno y flexible, y evita errores debido a cambios no intencionados en las clases base (o usos no deseados de la subclase como una instancia de la clase base).
Algunos idiomas tienen mecanismos de cumplimiento más estrictos:
- Todos los métodos son finales de forma predeterminada y deben marcarse explícitamente como
virtual
- Proporcionan herencia privada que no hereda la interfaz, sino solo la implementación.
- Requieren que los métodos de clase base estén marcados como virtuales, y requieren que todas las anulaciones también estén marcadas. Esto evita problemas cuando una subclase definió un nuevo método, pero luego se agregó un método con la misma firma a la clase base, pero no fue pensado como virtual.