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


54

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


43
Por que você acha que uma classe é projetada incorretamente se é anotada final? Muitas pessoas (inclusive eu) acham que é um bom design criar todas as classes não abstratas final.
Visto

20
Favorecer a composição sobre a herança e você pode ter todas as classes não abstratas final.
Andy

24
O princípio de abrir / fechar é, em certo sentido, um anacronismo do século XX, quando o mantra era fazer uma hierarquia de classes que herdou de classes que por sua vez herdaram de outras classes. Isso foi bom para o ensino de programação orientada a objetos, mas acabou criando bagunças emaranhadas e inatingíveis quando aplicadas a problemas do mundo real. Projetar uma classe para ser extensível é difícil.
David Hammen

34
@DavidArno Não seja ridículo. A herança é o sine qua non da programação orientada a objetos, e não é nem de longe tão complicada ou confusa quanto certos indivíduos excessivamente dogmáticos gostam de pregar. É uma ferramenta, como qualquer outra, e um bom programador sabe como usar a ferramenta certa para o trabalho.
Mason Wheeler

48
Como desenvolvedor em recuperação, pareço me lembrar de ser uma ferramenta brilhante para impedir que comportamentos prejudiciais entrem em seções críticas. Também me lembro da herança como uma ferramenta poderosa de várias maneiras. É quase como se ofegar ferramentas diferentes tivessem prós e contras e nós, como engenheiros, tivéssemos que equilibrar esses fatores ao produzir nosso software!
CorsiKa

Respostas:


136

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

Isso é importante porque a arquitetura do programa seria muito, muito difícil se você tivesse que antecipar que todas as classes e métodos que você escreve podem ser alterados para fazer algo completamente diferente por uma subclasse. É muito melhor decidir antecipadamente quais elementos devem ser alteráveis ​​e quais não são, e impor 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 faça o que for possível do que esperar que os usuários futuros leiam e obedeçam à documentação.


14
Eu também. Mas quem já escreveu uma classe base amplamente reutilizada (como uma estrutura ou uma biblioteca de mídia) sabe que não deve esperar que os programadores de aplicativos se comportem de maneira sadia. Eles subvertem, abusam e distorcem sua invenção de maneiras que você nem imaginava serem possíveis, a menos que você a prendesse com um aperto de ferro.
Kilian Foth

8
@KilianFoth Ok, mas honestamente, como é esse o seu problema que os programadores de aplicativos fazem?
Coredump 12/05

20
@coredump As pessoas que usam minha biblioteca mal criam sistemas ruins. Sistemas ruins geram reputações ruins. Os usuários não serão capazes de distinguir o ótimo código de Kilian do aplicativo monstruosamente instável de Fred Random. Resultado: eu perco na programação de creds e clientes. Tornar difícil o uso indevido do seu código é uma questão de dólares e centavos.
Kilian Foth

21
A declaração "Este elemento não deve ser alterado e, se você quiser alterá-lo, não entendeu o design existente". é incrivelmente arrogante e não é a atitude que eu gostaria como parte de qualquer biblioteca com a qual trabalhei. Se ao menos eu tivesse um centavo para cada vez que uma biblioteca ridiculamente superencapsulada me deixasse sem mecanismo para alterar algum estado interno que precisa ser alterado porque o autor falhou em antecipar um caso de uso importante ...
Mason Wheeler

31
@ JimmyB Regra geral: se você acha que sabe para que sua criação será usada, já está enganado. Bell concebeu o telefone como essencialmente um sistema Muzak. O Kleenex foi inventado com o objetivo de remover a maquiagem de maneira mais conveniente. Thomas Watson, presidente da IBM, disse uma vez: "Acho que existe um mercado mundial para talvez cinco computadores". O representante Jim Sensenbrenner, que introduziu a Lei PATRIOT em 2001, está declarando que foi especificamente destinado a impedir a NSA de fazer as coisas que está fazendo "com a autoridade do ato PATRIOT". E assim por diante ...
Mason Wheeler

59

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 SpiffyPagesubclasse 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 topparâmetro adicional é necessário para garantir que as chamadas writeMainContentpassem 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 ArrayListse 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 extenduma aula sem reflexão cuidadosa.
  • sempre marque suas classes como finalexceto, 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.

3
Você merece pelo menos +100 por mencionar o "frágil problema da classe base". :)
David Arno

6
Não estou convencido pelos argumentos apresentados aqui. Sim, a frágil classe base é um problema, mas final não resolve todos os problemas com a alteração de uma implementação. Seu primeiro exemplo é ruim porque você supõe que conhece todos os casos de uso possíveis para o PasswordChecker ("bloquear todos ou permitir que todos acessem ... não é bom" - diz quem?). Sua última lista "portanto aconselhável ..." é realmente ruim - você está basicamente defendendo não estender nada e marcando tudo como final - o que oblitera completamente a utilidade da OOP, herança e reutilização de código.
adelphus

4
Seu primeiro exemplo não é um exemplo do frágil problema da classe base. No frágil problema da classe base, as alterações em uma classe base quebram uma subclasse. Mas nesse exemplo, sua subclasse não segue o contrato de uma subclasse. Estes são dois problemas diferentes. (Além disso, ele realmente razoável que, sob certas circunstâncias, você pode desativar um verificador de senha (dizer para o desenvolvimento))
Winston Ewert

5
"Evita o frágil problema da classe base" - da mesma maneira que se matar evita ficar com fome.
User253751

9
@immibis, é mais como evitar comer para evitar intoxicação alimentar. Claro, nunca comer seria um problema. Mas comer apenas nos lugares em que você confia faz muito sentido.
Winston Ewert

33

Estou surpreso que ninguém ainda tenha mencionado o Effective Java, 2nd Edition, de Joshua Bloch (que deveria ser leitura obrigatória para todos os desenvolvedores Java). O item 17 do livro discute isso em detalhes e tem o título: " Design e documento da herança ou então a proíbe ".

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

Mas e as aulas concretas comuns? Tradicionalmente, eles não são finais, nem projetados e documentados para subclassificação, mas esse estado de coisas é perigoso. Cada vez que uma alteração é feita em uma classe, há uma chance de que as classes clientes que estendem a classe sejam interrompidas. Este não é apenas um problema teórico. Não é incomum receber relatórios de erros relacionados a subclasses depois de modificar os internos de uma classe concreta não final que não foi projetada e documentada para herança.

A melhor solução para esse problema é proibir a subclasse em classes que não foram projetadas e documentadas para serem subclassificadas com segurança. Existem duas maneiras de proibir a subclassificação. O mais fácil dos dois é declarar a aula final. A alternativa é tornar todos os construtores privados ou privados de pacotes e adicionar fábricas estáticas públicas no lugar dos construtores. Essa alternativa, que fornece a flexibilidade de usar subclasses internamente, é discutida no Item 15. Qualquer uma das abordagens é aceitável.


21

Um dos motivos finalé útil: garante que você não possa subclassificar uma classe de maneira a violar o contrato da classe pai. Essa subclasse seria uma violação do SOLID (acima de tudo "L") e criar uma classe finalevita isso.

Um exemplo típico é tornar impossível subclassificar uma classe imutável de uma maneira que tornaria a subclasse mutável. Em certos casos, essa 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 interessantes de segurança podem ser introduzidos se você puder subclassificar Stringe torná-lo mutável (ou fazer com que seja chamada de volta para casa quando alguém chama seus métodos, possivelmente retirando dados confidenciais do sistema) à medida que esses objetos são passados em torno de algum código interno relacionado ao carregamento e segurança da classe.

Às vezes, final também é útil para evitar erros simples, como reutilizar a mesma variável para duas coisas dentro de um método, etc. No Scala, você é incentivado a usar apenas o valque corresponde aproximadamente às variáveis ​​finais em Java e, na verdade, qualquer uso de um método. varou variável não final é encarada com suspeita.

Finalmente, os compiladores podem, pelo menos em teoria, realizar algumas otimizações extras quando sabem que uma classe ou método é final, pois quando você chama um método em uma classe final, sabe exatamente qual método será chamado e não precisa ir. através da tabela de método virtual para verificar a herança.


6
Por fim, os compiladores podem, pelo menos em teoria => Eu revi pessoalmente o passe de desvirtualização de Clang e confirmo que ele é usado na prática.
Matthieu M.

Mas o compilador não pode dizer antecipadamente que ninguém está substituindo uma classe ou método, independentemente de sua final marcada ou não?
JesseTG

3
@JesseTG Se tiver acesso a todo o código de uma vez, provavelmente. Mas e a compilação de arquivos separados?
Reintegrar Monica

3
A destirtualização do @JesseTG ou o cache em linha (monomórfico / polimórfico) é uma técnica comum nos compiladores JIT, já que o sistema sabe quais classes estão carregadas no momento e pode desoptimizar o código se a suposição de que nenhum método de substituição se mostre falso. No entanto, um compilador antecipado não pode. Ao compilar uma classe e código Java que a utilizam, posteriormente posso compilar uma subclasse e transmitir uma instância ao código consumidor. A maneira mais simples é adicionar outro jar à frente do caminho de classe. O compilador não pode saber sobre tudo isso, pois isso acontece em tempo de execução.
amon

5
@MTilsted Você pode presumir que as strings são imutáveis, já que a mutação de strings via reflexão pode ser proibida via a SecurityManager. A maioria dos programas não o utiliza, mas também não executa nenhum código não confiável. +++ Você deve assumir que as strings são imutáveis, caso contrário, você obtém zero de segurança e vários bugs arbitrários como bônus. Qualquer programador que suponha que as seqüências Java possam alterar o código produtivo é certamente insano.
Maaartinus 13/05

7

A segunda razã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 criar essa classe, contratei uma equipe de especialistas em segurança e essa classe se comunica com centenas de caixas eletrônicos com procedimentos 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.



A JVM é inteligente o suficiente para tratar as classes como finais se elas não tiverem subclasses, mesmo que não sejam declaradas finais.
User253751

Voto negativo porque a reivindicação de 'desempenho' foi refutada tantas vezes.
Paul Smith

7

Quando eu precisar de uma aula, eu vou escrever uma aula. Se eu não precisar de subclasses, não me importo com subclasses. Certifico-me de que minha classe se comporte da maneira pretendida, e os locais onde eu uso a classe pressupõem que a classe se comporte da maneira pretendida.

Se alguém quiser subclassificar minha classe, quero negar totalmente qualquer responsabilidade pelo que acontece. Consigo isso tornando a aula "final". Se você deseja subclassificar, lembre-se de que eu não levei em consideração a subclasse enquanto escrevia a classe. Portanto, você deve pegar o código-fonte da classe, remover o "final" e, a partir de então, tudo o que acontecer é de sua inteira responsabilidade .

Você acha que isso "não é orientado a objetos"? Eu fui pago para fazer uma aula que faz o que deveria fazer. Ninguém me pagou por fazer uma aula que poderia ser subclassificada. Se você é pago para tornar minha classe reutilizável, você pode fazê-lo. Comece removendo a palavra-chave "final".

(Além disso, "final" geralmente permite otimizações substanciais. Por exemplo, em Swift "final" em uma classe pública ou em um método de uma classe pública, significa que o compilador pode conhecer completamente o código que uma chamada de método executará, e pode substituir o envio dinâmico pelo estático (pequeno benefício) e, muitas vezes, o estático pelo inlining (possivelmente enorme).

adelphus: O que é tão difícil de entender sobre "se você deseja subclasse, pegue o código-fonte, remova o 'final' e é sua responsabilidade"? "final" é igual a "aviso justo".

E eu estou não pago para tornar o código reutilizável. Sou pago para escrever um código que faça o que deve fazer. Se sou pago para criar dois bits de código semelhantes, extraio as partes comuns porque é mais barato e não sou pago para perder meu tempo. Tornar reutilizável o código que não é reutilizado é uma perda de tempo.

M4ks: Você sempre torna tudo privado que não deve ser acessado de fora. Novamente, se você deseja subclasse, pega o código fonte, muda as coisas para "protegido", se necessário, e assume a responsabilidade pelo que faz. Se você acha que precisa acessar as coisas que marquei como particulares, é melhor saber o que está fazendo.

Ambos: A subclassificação é uma parte minúscula do código de reutilização. Criar blocos de construção que podem ser adaptados sem subclassificação é muito mais poderoso e se beneficia enormemente de "final", porque os usuários dos blocos podem confiar no que recebem.


4
-1 Esta resposta descreve tudo o que há de errado com os desenvolvedores de software. Se alguém quiser reutilizar sua classe por subclasse, deixe-o. Por que seria sua responsabilidade como eles a usam (ou abusam)? Basicamente, você está usando final como af , você não está usando minha classe. * "Ninguém me pagou por fazer uma aula que poderia ser subclassificada" . Você está falando sério? É exatamente por isso que os engenheiros de software são empregados - para criar código sólido e reutilizável.
Adelphus

4
-1 apenas fazer tudo confidencial, assim ninguém vai cada sequer pensar em subclasses ..
M4ks

3
@adelphus Embora o texto desta resposta seja contundente, chegando a ser severo, não é um ponto de vista "errado". De fato, é o mesmo ponto de vista que a maioria das respostas sobre essa questão até agora, apenas com um tom menos clínico.
NemesisX00

+1 por mencionar que você pode remover 'final'. É arrogante reivindicar o conhecimento prévio de todos os usos possíveis do seu código. No entanto, é humilde deixar claro que você não pode manter alguns usos possíveis e que esses usos exigiriam a manutenção de um garfo.
Gmatht 17/05

4

Vamos imaginar que o SDK de uma plataforma envie a seguinte classe:

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

Um aplicativo inclui subclasses dessa classe:

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

Tudo está bem, mas alguém que trabalha no SDK decide que passar um método para ele geté bobo e melhora a interface, garantindo 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 anulado não está mais sendo chamado e a solicitação não está sendo contada.

Isso é chamado de problema frágil da classe base, porque uma mudança aparentemente inócua resulta em uma quebra de subclasse. A qualquer momento, a alteração na qual os métodos são chamados dentro da classe pode causar uma subclasse. Isso tende a significar que quase qualquer alteração pode causar uma subclasse.

Final impede que alguém subclasse sua classe. Dessa forma, quais métodos dentro da classe podem ser alterados sem a preocupação de que alguém dependa exatamente de quais chamadas de método são feitas.


1

Final significa efetivamente que sua classe pode ser alterada com segurança no futuro sem afetar nenhuma classe baseada em herança downstream (porque não há nenhuma) ou qualquer problema relacionado à segurança de thread da classe (acho que há casos em que a palavra-chave final em um campo impede alguns high-jinx baseados em threads).

Final significa que você é livre para mudar a forma como sua classe funciona, sem que mudanças indesejadas de comportamento se arrastem para o código de outras pessoas que se baseia no seu como base.

Como exemplo, escrevo uma classe chamada HobbitKiller, o que é ótimo, porque todos os hobbits são complicados e provavelmente devem morrer. Risque isso, todos eles definitivamente precisam morrer.

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

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

Para que eu seja um desenvolvedor consciente e garanta uma morte suave do hobbit no futuro usando minha classe, tenho que ter muito, muito cuidado com as alterações que fizer em qualquer classe que possa ser estendida.

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


2
Se o método de segmentação vai mudar, por que você o tornou público? E se você mudar o comportamento de uma classe significativamente você precisa de outra versão, em vez de superprotetorafinal
M4ks

Não tenho ideia de por que isso mudaria. Os hobbits são traiçoeiros e a mudança foi necessária. O ponto é que, se eu construí-lo como final, evito que a herança proteja outras pessoas de que minhas alterações infectem seu código.
Scott Taylor

0

O uso de finalnã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, subclassificá-la e adicionar novos recursos". Isso 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 ao injetar 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 confiem na herança.

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

  • use o padrão de design do Decorator para reutilizar o comportamento existente do original ou
  • refatorar as partes do comportamento que você deseja manter em um objeto auxiliar e usar o mesmo auxiliar em sua nova implementação (refatorar não é modificação).

0

Para mim, é uma questão de design.

Vamos supor que eu tenha um programa que calcula salários para os funcionários. Se eu tiver uma turma que retorne o número de dias úteis entre duas datas com base no país (uma turma para cada país), colocarei essa final e fornecerei um método para que toda empresa forneça um dia gratuito apenas para seus calendários.

Por quê? Simples. Digamos que um desenvolvedor queira herdar a classe base WorkingDaysUSA de uma classe WorkingDaysUSAmyCompany e modifique-a para refletir que sua empresa será fechada por greve / manutenção / por qualquer motivo no dia 2 de março.

Os cálculos para pedidos e entrega de clientes refletirão o atraso e funcionarão adequadamente quando em tempo de execução eles chamam WorkingDaysUSAmyCompany.getWorkingDays (), mas o que acontece quando calculo o tempo de férias? Devo adicionar o dia 2 de março como 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 trabalha aos sábados, onde no país trabalha meio período no sábado. Em seguida, um terremoto, crise de eletricidade ou alguma circunstância faz o presidente declarar três dias não úteis, como aconteceu recentemente na Venezuela. Se o método da classe herdada já subtraído todos os sábados, minhas modificações na classe original podem levar à subtração duas vezes no mesmo dia. Eu precisaria ir a 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 minhaempresa, Date freeDay). Dessa forma, você tem certeza de que, quando chama uma classe WorkingDaysCountry, é sua classe principal e não uma subclasse

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.