Evita el problema de la clase base frágil . Cada clase viene con un conjunto de garantías implícitas o explícitas e invariantes. 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 lo usamos final
. Por ejemplo, tengamos un verificador de contraseña:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Si permitimos que se anule esa clase, 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 hace más evidente cuando una clase más complicada llama a sus propios métodos, y esos métodos pueden ser anulados. A veces me encuentro con esto cuando imprimo una estructura de datos o escribo 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 muy buena manera de generar páginas HTML, ¿qué sucede si quiero cambiar el diseño una vez más? Tendría que crear una SpiffyPage
subclase que de alguna manera envuelva ese contenido. Lo que podemos ver aquí es una aplicación accidental del patrón del 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 el contenido HTML cambia demasiado, esto podría romper el diseño proporcionado 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 sobre el que otras personas se basan.
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 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 top
parámetro adicional es necesario para asegurarse de que las llamadas writeMainContent
pasen por la parte superior de la cadena del decorador. Esto emula una característica de subclasificación llamada recursión abierta ).
Si tenemos múltiples decoradores, ahora podemos mezclarlos más libremente.
Mucho más a menudo 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 pudieras agregar elementos e iterar sobre todos ellos. 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 cambio, crearon una subclase:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
Esto de repente significa que toda la interfaz se ArrayList
ha convertido en parte de nuestra interfaz. Los usuarios pueden remove()
cosas, o get()
cosas en índices específicos. ¿Esto fue pensado de esa manera? OKAY. Pero a menudo, no pensamos cuidadosamente en todas las consecuencias.
Por lo tanto, es aconsejable
- Nunca
extend
una clase sin pensarlo detenidamente.
- marque siempre 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 esta "regla" tiene que romperse, pero generalmente 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 intencionados de la subclase como una instancia de la clase base )
Algunos idiomas tienen mecanismos de aplicación más estrictos:
- Todos los métodos son finales por defecto y deben marcarse explícitamente como
virtual
- Proporcionan una herencia privada que no hereda la interfaz sino solo la implementación.
- Requieren que los métodos de clase base se marquen como virtuales, y también se marcan todas las anulaciones. Esto evita problemas en los que una subclase definió un nuevo método, pero luego se agregó un método con la misma firma a la clase base pero no se pensó como virtual.
final
? Mucha gente (incluyéndome a mí) considera que es un buen diseño hacer que cada clase no abstractafinal
.