Por que a palavra-chave 'final' seria útil?

54

Parece que o Java teve o poder de declarar classes não deriváveis por muito tempo, e agora o C ++ também o possui. No entanto, à luz do princípio Open / Close no SOLID, por que isso seria útil? Para mim, a palavra-chave final soa como friend - é legal, mas se você estiver usando, provavelmente o design está errado. Forneça alguns exemplos em que uma classe não derivável seria uma parte de uma grande arquitetura ou padrão de design.

    
por Vorac 12.05.2016 / 10:33
fonte

10 respostas

134

final expressa intenção . Ele informa ao usuário de uma classe, método ou variável "Este elemento não deve mudar, e se você quiser alterá-lo, você não entendeu o design existente."

Isto é importante porque a arquitetura do programa seria muito, muito difícil se você tivesse que antecipar que todas as classes e todos os métodos escritos por você poderiam ser alterados para fazer algo completamente diferente por uma subclasse. É muito melhor decidir antecipadamente quais elementos devem ser modificáveis e quais não são, e reforçar a imutabilidade via final .

Você também pode fazer isso por meio de comentários e documentos de arquitetura, mas é sempre melhor permitir que o compilador imponha as coisas do que esperar que os futuros usuários leiam e obedeçam a documentação.

    
por 12.05.2016 / 10:48
fonte
58

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.
por 12.05.2016 / 12:30
fonte
32

Estou surpreso que ninguém ainda tenha mencionado Effective Java, 2nd Edition por Joshua Bloch (que deve ser necessário ler para cada desenvolvedor Java pelo menos). O item 17 do livro discute isso em detalhes, e intitula-se: " Projete e documente a herança, ou então a proíba ".

Não vou repetir todos os bons conselhos do livro, mas esses parágrafos específicos parecem relevantes:

But what about ordinary concrete classes? Traditionally, they are neither final nor designed and documented for subclassing, but this state of affairs is dangerous. Each time a change is made in such a class, there is a chance that client classes that extend the class will break. This is not just a theoretical problem. It is not uncommon to receive subclassing-related bug reports after modifying the internals of a nonfinal concrete class that was not designed and documented for inheritance.

The best solution to this problem is to prohibit subclassing in classes that are not designed and documented to be safely subclassed. There are two ways to prohibit subclassing. The easier of the two is to declare the class final. The alternative is to make all the constructors private or package-private and to add public static factories in place of the constructors. This alternative, which provides the flexibility to use subclasses internally, is discussed in Item 15. Either approach is acceptable.

    
por 12.05.2016 / 20:57
fonte
21

Uma das razões pelas quais final é útil é garantir que você não possa criar subclasses de uma classe de maneira a violar o contrato da classe pai. Tal subclasse seria uma violação do SOLID (acima de tudo "L") e fazendo com que uma classe final o impeça.

Um exemplo típico é tornar impossível subclassificar uma classe imutável de uma maneira que torne a subclasse mutável. Em certos casos, tal mudança de comportamento pode levar a efeitos muito surpreendentes, por exemplo, quando você usa algo como chaves em um mapa pensando que a chave é imutável, enquanto na realidade você está usando uma subclasse que é mutável.

Em Java, muitos problemas de segurança interessantes poderiam ser introduzidos se você pudesse subclassificar String e torná-lo mutável (ou fazê-lo chamar de volta quando alguém chama seus métodos, possivelmente removendo dados confidenciais do sistema ) como esses objetos são passados em torno de algum código interno relacionado ao carregamento de classe e segurança.

Final também é útil às vezes para evitar erros simples, como reutilizar a mesma variável para duas coisas dentro de um método, etc. No Scala, você é encorajado a usar apenas val , que corresponde aproximadamente às variáveis finais em Java, e, na verdade, qualquer uso de uma variável var ou não final é visto com suspeita.

Finalmente, os compiladores podem, pelo menos em teoria, executar algumas otimizações extras quando sabem que uma classe ou método é final, já que quando você chama um método em uma classe final, você sabe exatamente qual método será chamado e não tem que passar por tabela de método virtual para verificar a herança.

    
por 12.05.2016 / 11:43
fonte
7

O segundo motivo é o desempenho . A primeira razão é porque algumas classes têm comportamentos ou estados importantes que não devem ser alterados para permitir que o sistema funcione. Por exemplo, se eu tenho uma classe "PasswordCheck" e para construir essa classe eu contratei uma equipe de especialistas em segurança e essa classe se comunica com centenas de caixas eletrônicos com procols bem estudados e definidos. Permitir que um novo contratado recém-saído da universidade faça uma classe "TrustMePasswordCheck" que estenda a classe acima pode ser muito prejudicial para o meu sistema; esses métodos não devem ser substituídos, é isso.

    
por 12.05.2016 / 11:06
fonte
7

Quando eu precisar de uma aula, vou escrever uma aula. Se eu não preciso de subclasses, não me importo com subclasses. Eu me certifico de que minha turma se comporta como pretendido, e os lugares onde eu uso a turma presumem que a turma se comporta como pretendido.

Se alguém quiser subclassificar minha turma, quero negar totalmente qualquer responsabilidade sobre o que acontece. Eu consegui isso fazendo a aula "final". Se você quiser subclassificá-lo, lembre-se de que não levei em consideração a subclasse enquanto escrevia a turma. Então você tem que pegar o código fonte da classe, remover o "final" e, a partir de então, qualquer coisa que aconteça é totalmente sua responsabilidade .

Você acha que "não é orientada a objetos"? Fui pago para fazer uma aula que faz o que é suposto fazer. Ninguém me pagou por fazer uma aula que pudesse ser subclassificada. Se você é pago para tornar a minha aula reutilizável, você é bem-vindo a fazê-lo. Comece removendo a palavra-chave "final".

(Além disso, "final" geralmente permite otimizações substanciais. Por exemplo, no Swift "final" em uma classe pública, ou em um método de uma classe pública, significa que o compilador pode saber qual código um método chama irá executar, e pode substituir o despacho dinâmico com o despacho estático (pequeno benefício) e, muitas vezes, substituir o despacho estático por inline (possivelmente grande benefício).

adelphus: O que é tão difícil de entender sobre "se você quiser subclassificá-lo, pegar o código-fonte, remover o 'final' e é de sua responsabilidade"? "final" é igual a "aviso justo".

E eu sou não pago para criar código reutilizável. Eu sou pago para escrever código que faz o que é suposto fazer. Se eu for pago para fazer dois pedaços de código similares, extraio as partes comuns porque isso é mais barato e não sou pago para desperdiçar meu tempo. Tornar o código reutilizável que não é reutilizado é um desperdício do meu tempo.

M4ks: Você sempre torna tudo privado que não deve ser acessado de fora. Novamente, se você quiser subclasse, pegue o código-fonte, altere as coisas para "protegido" se precisar e assuma a responsabilidade pelo que faz. Se você acha que precisa acessar as coisas marcadas como particulares, é melhor saber o que está fazendo.

Ambos: Subclassing é uma pequena porção de reutilização de código. Criar blocos de construção que podem ser adaptados sem subclasses é muito mais poderoso e se beneficia enormemente do "final" porque os usuários dos blocos podem confiar no que recebem.

    
por 13.05.2016 / 10:49
fonte
4

Vamos imaginar que o SDK para uma plataforma seja da seguinte classe:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Um aplicativo subclasse essa classe:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Tudo está bem e bem, mas alguém que trabalha no SDK decide que passar um método para get é tolo e torna a interface melhor, certificando-se de reforçar a compatibilidade com versões anteriores.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

Tudo parece bem, até que o aplicativo acima seja recompilado com o novo SDK. De repente, o método get overriden não está mais sendo chamado e a solicitação não está sendo contada.

Isso é chamado de problema de classe base frágil, porque uma mudança aparentemente inofensiva resulta em uma quebra de subclasse. A qualquer momento, a mudança para quais métodos são chamados dentro da classe pode causar a quebra de uma subclasse. Isso significa que quase qualquer alteração pode causar a quebra de uma subclasse.

Final impede que alguém subclassifique sua classe. Dessa forma, quais métodos dentro da classe podem ser alterados sem se preocupar que em algum lugar alguém dependa exatamente de quais chamadas de método são feitas.

    
por 13.05.2016 / 00:58
fonte
1

Final significa efetivamente que sua classe está segura para mudar no futuro sem afetar as classes baseadas em herança downstream (porque não há nenhuma), ou quaisquer problemas relacionados à segurança de thread da classe (acho que há casos em que a palavra-chave final um campo previne alguma alta jinx baseada em threads).

Final significa que você é livre para mudar o modo como sua classe funciona sem nenhuma mudança não intencional no comportamento que se insinua no código de outras pessoas que depende da sua como base.

Como exemplo, eu escrevo uma classe chamada HobbitKiller, o que é ótimo, porque todos os hobbits são tricksie e provavelmente deveriam morrer. Raspe isso, todos eles definitivamente precisam morrer.

Você usa isso como uma classe base e adiciona um novo método para usar um lança-chamas, mas usa minha classe como base porque eu tenho um ótimo método para atacar os hobbits (além de ser um truque, eles são rápidos ), que você usa para ajudar a apontar o seu lança-chamas.

Três meses depois, mudei a implementação do meu método de segmentação. Agora, em algum momento futuro, quando você atualiza sua biblioteca, sem o seu conhecimento, a implementação real do tempo de execução da sua classe foi fundamentalmente alterada por causa de uma mudança no método da superclasse da qual você depende (e geralmente não controla).

Para que eu seja um desenvolvedor consciencioso e garanta uma morte suave de hobbits no futuro usando minha classe, tenho que ser muito, muito cuidadoso com quaisquer mudanças que eu faça em qualquer classe que possa ser estendida.

Ao remover a capacidade de estender, exceto nos casos em que eu pretendo especificamente estender a classe, eu salvo (e espero que outros) muitas dores de cabeça.

    
por 12.05.2016 / 17:37
fonte
0

O uso de final não é de forma alguma uma violação dos princípios do SOLID. Infelizmente, é extremamente comum interpretar o Princípio Aberto / Fechado ("entidades de software devem estar abertas para extensão mas fechadas para modificação") como significando "em vez de modificar uma classe, subclasse-a e adicione novos recursos". Isto não é o que originalmente significava, e geralmente não é a melhor abordagem para alcançar seus objetivos.

A melhor maneira de cumprir o OCP é projetar pontos de extensão em uma classe, fornecendo especificamente comportamentos abstratos que são parametrizados pela injeção de uma dependência no objeto (por exemplo, usando o padrão de design da Estratégia). Esses comportamentos devem ser projetados para usar uma interface para que novas implementações não dependam da herança.

Outra abordagem é implementar sua classe com sua API pública como uma classe abstrata (ou interface). Você pode então produzir uma implementação totalmente nova que pode ser conectada aos mesmos clientes. Se sua nova interface exigir um comportamento semelhante ao do original, você poderá:

  • use o padrão de design do Decorator para reutilizar o comportamento existente do original ou
  • refatorie as partes do comportamento que você deseja manter em um objeto auxiliar e use o mesmo auxiliar em sua nova implementação (a refatoração não é uma modificação).
por 16.05.2016 / 10:19
fonte
0

Para mim, é uma questão de design.

Vamos supor que eu tenha um programa que calcule os salários dos funcionários. Se eu tiver uma classe que retorna o número de dias úteis entre duas datas com base no país (uma classe para cada país), colocarei essa final e forneço um método para cada empresa fornecer um dia livre somente para seus calendários. / p>

Por quê? Simples. Digamos que um desenvolvedor queira herdar a classe base WorkingDaysUSA em uma classe WorkingDaysUSAmyCompany e modificá-la para refletir que sua empresa será fechada para ataque / manutenção / qualquer motivo do segundo dia de março.

Os cálculos para pedidos de clientes e entrega refletirão o atraso e funcionarão de acordo quando em tempo de execução eles chamarem WorkingDaysUSAmyCompany.getWorkingDays (), mas o que acontece quando eu calculo o tempo de férias? Devo adicionar o segundo de Marte como um feriado para todos? Não. Mas como o programador usou herança e eu não protegi a classe, isso pode levar a uma confusão.

Ou digamos que eles herdam e modificam a classe para refletir que esta empresa não funciona aos sábados, onde no país eles trabalham meio período no sábado. Em seguida, um terremoto, crise de eletricidade ou alguma circunstância faz com que o presidente declare 3 dias de folga, como aconteceu recentemente na Venezuela. Se o método da classe herdada já fosse subtraído a cada sábado, minhas modificações na classe original poderiam levar a subtrair o mesmo dia duas vezes. Eu teria que ir para cada subclasse de cada cliente e verificar se todas as alterações são compatíveis.

Solução? Torne a aula final e forneça um método addFreeDay (companyID mycompany, Date freeDay). Dessa forma, você tem certeza de que quando você chama uma classe WorkingDaysCountry é sua classe principal e não uma subclasse

    
por 12.05.2016 / 17:43
fonte