Evita o Problema de Classe Base Frágil . Cada turma vem com um conjunto de garantias implícitas ou explícitas e invariantes. O Princípio de Substituição de Liskov determina que todos os subtipos dessa classe também devem fornecer todas essas garantias. No entanto, é realmente fácil violar isso se não usarmos final
. Por exemplo, vamos ter um verificador de senha:
public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
Se permitirmos que essa classe seja substituída, uma implementação poderá bloquear todos, outra poderá dar acesso a todos:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Geralmente, isso não está correto, pois as subclasses agora têm um comportamento muito incompatível com o original. Se realmente pretendemos que a classe seja estendida com outro comportamento, uma Cadeia de Responsabilidade seria melhor:
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;
}
}
O problema se torna mais aparente quando mais uma classe complicada chama seus próprios métodos, e esses métodos podem ser substituídos. Eu às vezes encontro isso quando imprimindo uma estrutura de dados ou escrevendo HTML. Cada método é responsável por algum 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>");
}
...
}
Eu agora criei uma subclasse que adiciona um pouco mais 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>");
}
}
Agora, ignorando por um momento que essa não é uma boa maneira de gerar páginas HTML, o que acontece se eu quiser alterar o layout novamente? Eu teria que criar uma subclasse SpiffyPage
que de alguma forma envolve esse conteúdo. O que podemos ver aqui é uma aplicação acidental do padrão de método de modelo. Os métodos de modelo são pontos de extensão bem definidos em uma classe base que devem ser substituídos.
E o que acontece se a classe base mudar? Se o conteúdo HTML mudar muito, isso pode quebrar o layout fornecido pelas subclasses. Portanto, não é realmente seguro alterar a classe base depois. Isso não é aparente se todas as suas classes estiverem no mesmo projeto, mas muito perceptível se a classe base fizer parte de algum software publicado que outras pessoas constroem.
Se essa estratégia de extensão fosse pretendida, poderíamos permitir que o usuário trocasse a maneira como cada parte é gerada. Ou pode haver uma estratégia para cada bloco que pode ser fornecido externamente. Ou podemos aninhar Decoradores. Isso seria equivalente ao código acima, mas muito mais explícito e muito mais flexível:
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);
...
}
}
(O parâmetro adicional top
é necessário para garantir que as chamadas para writeMainContent
passem pelo topo da cadeia do decorador. Isso emula um recurso de subclasse chamado recursão aberta .)
Se tivermos vários decoradores, podemos agora misturá-los mais livremente.
Muito mais frequentemente do que o desejo de adaptar ligeiramente a funcionalidade existente é o desejo de reutilizar parte de uma classe existente. Eu vi um caso em que alguém queria uma aula onde você poderia adicionar itens e interagir sobre todos eles. A solução correta seria:
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
}
Em vez disso, eles criaram uma subclasse:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
Isso significa que toda a interface de ArrayList
se tornou parte da nossa interface. Os usuários podem remove()
things ou get()
things em índices específicos. Isso foi planejado dessa maneira? ESTÁ BEM. Mas, muitas vezes, não pensamos com cuidado em todas as consequências.
Portanto, é aconselhável
- nunca
extend
de uma turma sem pensar cuidadosamente.
- sempre marque suas classes como
final
, exceto se você pretende que algum método seja substituído.
- crie interfaces onde você deseja trocar uma implementação, por exemplo para testes unitários.
Existem muitos exemplos em que essa "regra" precisa ser quebrada, mas geralmente leva a um design bom e flexível, e evita erros devido a mudanças não intencionais nas classes base (ou usos não intencionais da subclasse como uma instância de a classe base).
Alguns idiomas têm mecanismos de aplicação mais rigorosos:
- Todos os métodos são finais por padrão e precisam ser marcados explicitamente como
virtual
- Eles fornecem herança privada que não herda a interface, mas apenas a implementação.
- Eles exigem que os métodos da classe base sejam marcados como virtuais e exigem que todas as substituições também sejam marcadas. Isso evita problemas em que uma subclasse definiu um novo método, mas um método com a mesma assinatura foi adicionado posteriormente à classe base, mas não como virtual.