Evita o problema frágil da classe base . Toda classe vem com um conjunto de garantias e invariáveis implícitas ou explícitas. O Princípio da Substituição de Liskov exige que todos os subtipos dessa classe também forneçam todas essas garantias. No entanto, é realmente fácil violar isso se não 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 pode bloquear todos, outra pode dar acesso a todos:
public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
Isso geralmente não é bom, já que 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 uma classe mais complicada chama seus próprios métodos, e esses métodos podem ser substituídos. Às vezes, encontro isso ao imprimir bastante uma estrutura de dados ou ao escrever 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>");
}
...
}
Agora, crio 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 maneira muito boa de gerar páginas HTML, o que acontece se eu quiser alterar o layout novamente? Eu teria que criar uma SpiffyPage
subclasse que de alguma forma envolvesse esse conteúdo. O que podemos ver aqui é uma aplicação acidental do padrão do 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 interromper o layout fornecido pelas subclasses. Portanto, não é realmente seguro alterar a classe base posteriormente. 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 desenvolvam.
Se essa estratégia de extensão foi planejada, poderíamos ter permitido ao usuário trocar a maneira como cada parte é gerada. Ou, pode haver uma estratégia para cada bloco que possa ser fornecida 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 top
parâmetro adicional é necessário para garantir que as chamadas writeMainContent
passem pela parte superior da cadeia do decorador. Isso simula um recurso de subclasse denominado recursão aberta .)
Se temos vários decoradores, agora podemos misturá-los mais livremente.
Muito mais frequentemente do que o desejo de adaptar levemente a funcionalidade existente, é o desejo de reutilizar parte de uma classe existente. Eu vi um caso em que alguém queria uma aula em que você pudesse adicionar itens e repetir todos eles. A solução correta teria 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
}
Em vez disso, eles criaram uma subclasse:
class Thingies extends ArrayList<Thing> {
... // custom methods
}
De repente, isso significa que toda a interface do ArrayList
se tornou parte da nossa interface. Os usuários podem remove()
coisas, ou get()
coisas em índices específicos. Isso foi planejado dessa maneira? ESTÁ BEM. Mas, muitas vezes, não pensamos cuidadosamente em todas as consequências.
Portanto, é aconselhável
- nunca
extend
uma aula sem reflexão cuidadosa.
- sempre marque suas classes como
final
exceto, se você pretende que qualquer método seja substituído.
- crie interfaces nas quais deseja trocar uma implementação, por exemplo, para teste de unidade.
Existem muitos exemplos em que essa "regra" precisa ser quebrada, mas geralmente o orienta para um design bom e flexível e evita bugs devido a alterações não intencionais nas classes base (ou usos não intencionais da subclasse como uma instância da classe base) )
Alguns idiomas têm mecanismos de aplicação mais rigorosos:
- Todos os métodos são finais por padrão e devem 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 sejam marcadas também. Isso evita problemas em que uma subclasse definiu um novo método, mas um método com a mesma assinatura foi posteriormente adicionado à classe base, mas não pretendido como virtual.
final
? Muitas pessoas (inclusive eu) acham que é um bom design criar todas as classes não abstratasfinal
.