Por que uma classe deveria ser outra coisa que não "abstrata" ou "final / selada"?

29

Após mais de 10 anos de programação em java / c #, me pego criando:

  • classes abstratas : contrato não destinado a ser instanciado como está.
  • classes finais / seladas : a implementação não pretende servir como classe base para outra coisa.

Não consigo pensar em nenhuma situação em que uma "classe" simples (ou seja, nem abstrata nem final / selada) seria "programação inteligente".

Por que uma classe deveria ser outra coisa senão "abstrata" ou "final / selada"?

EDITAR

Este ótimo artigo explica minhas preocupações muito melhor do que posso.

Nicolas Repiquet
fonte
21
Porque é chamado de Open/Closed principle, não oClosed Principle.
StuperUser
2
Que tipo de coisas você está escrevendo profissionalmente? Pode muito bem influenciar sua opinião sobre o assunto.
Bem, eu conheço alguns desenvolvedores de plataformas que selam tudo, porque querem minimizar a interface de herança.
K ..
4
@StuperUser: Isso não é uma razão, é uma banalidade. O OP não está perguntando qual é a banalidade, ele está perguntando o porquê . Se não há razão por trás de um princípio, não há razão para prestar atenção a ele.
Michael Shaw
1
Gostaria de saber quantas estruturas de interface do usuário quebrariam alterando a classe Window para selada.
Reactgular

Respostas:

46

Ironicamente, acho o oposto: o uso de classes abstratas é a exceção e não a regra, e costumo desaprovar as aulas finais / seladas.

As interfaces são um mecanismo mais típico de design por contrato, porque você não especifica nenhum elemento interno - não está preocupado com eles. Ele permite que toda implementação desse contrato seja independente. Isso é fundamental em muitos domínios. Por exemplo, se você estivesse criando um ORM, seria muito importante transmitir uma consulta ao banco de dados de maneira uniforme, mas as implementações podem ser bem diferentes. Se você usar classes abstratas para esse fim, você acaba conectando os componentes que podem ou não se aplicar a todas as implementações.

Para as aulas finais / seladas, a única desculpa que posso usar é quando é realmente perigoso permitir a substituição - talvez um algoritmo de criptografia ou algo assim. Fora isso, você nunca sabe quando poderá estender a funcionalidade por razões locais. O selamento de uma classe restringe suas opções para ganhos inexistentes na maioria dos cenários. É muito mais flexível escrever suas aulas de uma maneira que elas possam ser estendidas mais tarde.

Essa última visão foi consolidada para mim ao trabalhar com componentes de terceiros que selavam classes, impedindo assim alguma integração que tornaria a vida muito mais fácil.

Michael
fonte
17
+1 no último parágrafo. Eu frequentemente descobri que o encapsulamento em excesso nas bibliotecas de terceiros (ou mesmo nas classes de bibliotecas padrão!) É uma causa maior de dor do que o encapsulamento em excesso.
Mason Wheeler
5
O argumento usual para selar classes é que você não pode prever as muitas maneiras diferentes pelas quais um cliente poderia substituir sua classe e, portanto, não pode garantir o seu comportamento. Veja blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey
12
@RobertHarvey Estou familiarizado com o argumento e parece ótimo em teoria. Você, como designer de objetos, não pode prever como as pessoas podem estender suas classes - é exatamente por isso que elas não devem ser seladas. Você não pode apoiar tudo - tudo bem. Não. Mas também não tire opções.
Michael
6
@RobertHarvey não são apenas pessoas que quebram o LSP recebendo o que merecem?
StuperUser
2
Também não sou um grande fã de aulas seladas, mas pude ver por que algumas empresas como a Microsoft (que frequentemente precisam apoiar coisas que não quebraram a si mesmas) as acham atraentes. Os usuários de uma estrutura não devem necessariamente ter conhecimento dos internos de uma classe.
Robert Harvey
11

Esse é um ótimo artigo de Eric Lippert, mas não acho que apóie seu ponto de vista.

Ele argumenta que todas as classes expostas para uso por terceiros devem ser seladas ou tornadas não extensíveis.

Sua premissa é que todas as classes devem ser abstratas ou seladas.

Grande diferença.

O artigo de EL não diz nada sobre as (presumivelmente muitas) classes produzidas por sua equipe sobre as quais você e eu não sabemos nada. Em geral, as classes expostas publicamente em uma estrutura são apenas um subconjunto de todas as classes envolvidas na implementação dessa estrutura.

Martin
fonte
Mas você pode aplicar o mesmo argumento a classes que não fazem parte de uma interface pública. Uma das principais razões para finalizar uma aula é que ela atua como uma medida de segurança para evitar códigos desleixados. Esse argumento é igualmente válido para qualquer base de código compartilhada por diferentes desenvolvedores ao longo do tempo ou mesmo se você for o único desenvolvedor. Apenas protege a semântica do código.
DPM
Penso que a ideia de que as classes devam ser abstratas ou seladas faz parte de um princípio mais amplo, que é o de evitar o uso de variáveis ​​de tipos de classe instáveis. Tal evitação permitirá criar um tipo que possa ser usado pelos consumidores de um determinado tipo, sem que seja necessário herdar todos os seus membros privados. Infelizmente, esses projetos realmente não funcionam com sintaxe de construtor público. Em vez disso, o código teria que substituir new List<Foo>()por algo como List<Foo>.CreateMutable().
precisa
5

De uma perspectiva java, acho que as aulas finais não são tão inteligentes quanto parece.

Muitas ferramentas (especialmente AOP, JPA etc) funcionam com o tempo de carregamento suspenso, portanto elas precisam estender suas classes. A outra maneira seria criar delegados (não os .NET) e delegar tudo à classe original, o que seria muito mais confuso do que estender as classes de usuários.

Uwe Plonus
fonte
5

Dois casos comuns em que você precisará de baunilha, classes não seladas:

  1. Técnico: se você possui uma hierarquia com mais de dois níveis de profundidade e deseja instanciar algo no meio.

  2. Princípio: Às vezes é desejável escrever classes que sejam explicitamente projetadas para serem estendidas com segurança . (Isso acontece muito quando você está escrevendo uma API como Eric Lippert ou quando está trabalhando em uma equipe em um projeto grande). Às vezes, você deseja escrever uma classe que funcione bem por si só, mas foi projetada com a extensibilidade em mente.

Os pensamentos de Eric Lippert sobre selagem faz sentido, mas ele também admite que fazer design para extensibilidade, deixando a classe "aberta".

Sim, muitas classes são seladas na BCL, mas um grande número de classes não é, e pode ser estendido de todos os tipos e formas maravilhosas. Um exemplo que vem à mente é o Windows Forms, no qual você pode adicionar dados ou comportamento a praticamente qualquerControl via herança. Certamente, isso poderia ter sido feito de outras maneiras (padrão decorador, vários tipos de composição etc.), mas a herança também funciona muito bem.

Duas notas específicas do .NET:

  1. Na maioria das circunstâncias, as classes de vedação geralmente não são críticas para a segurança, porque os herdeiros não podem mexer com sua não virtualfuncionalidade, com exceção das implementações explícitas da interface.
  2. Às vezes, uma alternativa adequada é criar o Construtor em internalvez de selar a classe, o que permite que ele seja herdado dentro da sua base de código, mas não fora dela.
Kevin McCormick
fonte
4

Acredito que a verdadeira razão pela qual muitas pessoas acham que as aulas devam ser final/ sealedé que a maioria das classes extensíveis não abstratas não são documentadas adequadamente .

Deixe-me elaborar. A partir de longe, existe a visão entre alguns programadores de que a herança como ferramenta no OOP é amplamente usada e abusada. Todos nós lemos o princípio da substituição de Liskov, embora isso não nos impedisse de violá-lo centenas (talvez até milhares) de vezes.

O problema é que os programadores adoram reutilizar código. Mesmo quando não é uma boa ideia. E a herança é uma ferramenta essencial nesse "reabusing" do código. Voltar à pergunta final / lacrada.

A documentação adequada para uma classe final / selada é relativamente pequena: você descreve o que cada método faz, quais são os argumentos, o valor de retorno, etc. Todas as coisas usuais.

No entanto, ao documentar adequadamente uma classe extensível, você deve incluir pelo menos o seguinte :

  • Dependências entre métodos (qual método chama quais, etc.)
  • Dependências em variáveis ​​locais
  • Contratos internos que a classe estendida deve honrar
  • Convenção de chamada para cada método (por exemplo, quando você a substitui, chama o super implementação? Você chama no início do método ou no final? Pense em construtor ou destruidor)
  • ...

Estes estão no topo da minha cabeça. E posso fornecer um exemplo de por que cada uma delas é importante e ignorá-la irá estragar uma classe estendida.

Agora, considere quanto esforço de documentação deve ser necessário para documentar adequadamente cada uma dessas coisas. Acredito que uma classe com métodos 7-8 (que pode ser muito em um mundo idealizado, mas é muito pouco no real) também pode ter uma documentação de 5 páginas, somente texto. Em vez disso, evitamos a metade do caminho e não selamos a classe, para que outras pessoas possam usá-la, mas também não a documentam adequadamente, pois isso levará uma quantidade enorme de tempo (e, você sabe, pode nunca seja estendido, então por que se preocupar?).

Se você estiver criando uma classe, poderá sentir a tentação de selá-la para que as pessoas não possam usá-la de uma maneira que você não previu (e preparou). Por outro lado, quando você está usando o código de outra pessoa, às vezes a partir da API pública, não há motivo visível para a aula ser final, e você pode pensar "Droga, isso me custou 30 minutos em busca de uma solução alternativa".

Eu acho que alguns dos elementos de uma solução são:

  • Primeiro, verifique se a extensão é uma boa idéia quando você é cliente do código e realmente favorece a composição sobre a herança.
  • Segundo, leia o manual na íntegra (novamente como cliente) para garantir que você não esteja negligenciando algo mencionado.
  • Terceiro, quando você estiver escrevendo um pedaço de código que o cliente usará, escreva a documentação adequada para o código (sim, o longo caminho). Como exemplo positivo, posso fornecer os documentos para iOS da Apple. Eles não são suficientes para que o usuário sempre estenda adequadamente suas classes, mas pelo menos incluem algumas informações sobre herança. O que é mais do que posso dizer para a maioria das APIs.
  • Quarto, tente estender sua própria classe, para garantir que ela funcione. Sou um grande defensor da inclusão de muitas amostras e testes nas APIs e, quando você está fazendo um teste, pode testar também as cadeias de herança: afinal, elas fazem parte do seu contrato!
  • Quinto, em situações em que você está em dúvida, indique que a classe não deve ser estendida e que fazê-lo é uma má ideia (tm). Indique que você não deve ser responsabilizado por esse uso não intencional, mas ainda não selará a classe. Obviamente, isso não cobre casos em que a classe deve ser 100% selada.
  • Por fim, ao selar uma classe, forneça uma interface como um gancho intermediário, para que o cliente possa "reescrever" sua própria versão modificada da classe e contornar a classe 'selada'. Dessa forma, ele pode substituir a classe selada por sua implementação. Agora, isso deve ser óbvio, já que é um acoplamento solto em sua forma mais simples, mas ainda vale a pena mencionar.

Também vale a pena mencionar a seguinte pergunta "filosófica": É se uma classe é sealed/ finalparte do contrato da classe ou um detalhe de implementação? Agora eu não quero ir para lá, mas uma resposta para isso também deve influenciar sua decisão de selar ou não uma classe.

K.Steff
fonte
3

Uma aula não deve ser final / selada nem abstrata se:

  • É útil por si só, ou seja, é benéfico ter instâncias dessa classe.
  • É benéfico para essa classe ser a subclasse / classe base de outras classes.

Por exemplo, faça a ObservableCollection<T>aula em C # . Ele só precisa adicionar o aumento de eventos às operações normais de a Collection<T>, e é por isso que as subclasses Collection<T>. Collection<T>é uma classe viável por si só, e por isso ObservableCollection<T>.

FishBasketGordo
fonte
Se eu estava encarregado disso, provavelmente o faria CollectionBase(abstrato), Collection : CollectionBase(selado), ObservableCollection : CollectionBase(selado). Se você olhar atentamente para a coleção <T> , verá que é uma classe abstrata de meia-boca.
Nicolas Repiquet
2
Que vantagem você ganha por ter três classes em vez de apenas duas? Além disso, como é Collection<T>uma "classe abstrata meia-boca"?
FishBasketGordo
Collection<T>expõe muitas entranhas através de métodos e propriedades protegidas e claramente pretende ser uma classe base para coleta especializada. Mas não está claro onde você deve colocar seu código, pois não há métodos abstratos para implementar. ObservableCollectionherda Collectione não é selado, para que você possa herdar novamente. E você pode acessar a Itemspropriedade protegida, permitindo adicionar itens na coleção sem gerar eventos ... Agradável.
Nicolas Repiquet
@NicolasRepiquet: em muitas linguagens e estruturas, o meio idiomático normal de criação de um objeto requer que o tipo da variável que irá reter o objeto seja o mesmo que o tipo da instância criada. Em muitos casos, o uso ideal seria passar referências a um tipo abstrato, mas isso forçaria muito código a usar um tipo para variáveis ​​e parâmetros e um tipo concreto diferente ao chamar construtores. Dificilmente impossível, mas um pouco estranho.
Supercat 21/12
3

O problema com as aulas finais / seladas é que eles estão tentando resolver um problema que ainda não aconteceu. É útil apenas quando o problema existe, mas é frustrante porque terceiros impuseram uma restrição. Raramente a classe de vedação resolve um problema atual, o que torna difícil argumentar que é útil.

Há casos em que uma classe deve ser selada. Como exemplo; A classe gerencia recursos / memória alocados de uma maneira que não pode prever como mudanças futuras podem alterar esse gerenciamento.

Ao longo dos anos, achei o encapsulamento, retornos de chamada e eventos muito mais flexíveis / úteis do que as classes abstraídas. Vejo muito código com uma grande hierarquia de classes em que o encapsulamento e os eventos tornariam a vida mais simples para o desenvolvedor.

Reactgular
fonte
1
O material selado final no software resolve o problema de "Sinto a necessidade de impor minha vontade aos futuros mantenedores deste código".
Kaz
Não foi final / selar algo adicionado ao OOP, porque não me lembro de estar por perto quando era mais jovem. Parece um tipo de recurso após o pensamento.
Reactgular
A finalização existia como um procedimento de cadeia de ferramentas nos sistemas OOP antes que o OOP se tornasse emburrecido com linguagens como C ++ e Java. Os programadores trabalham em Smalltalk, Lisp com flexibilidade máxima: tudo pode ser estendido, novos métodos adicionados o tempo todo. Então, a imagem compilada do sistema está sujeita a uma otimização: é feita uma suposição de que o sistema não será estendido pelos usuários finais e, portanto, isso significa que o envio do método pode ser otimizado com base no balanço de quais métodos e classes existem agora .
Kaz
Eu não acho que é a mesma coisa, porque este é apenas um recurso de otimização. Não me lembro de estar selado em todas as versões do Java, mas posso estar errado porque não o uso muito.
Reactgular
Só porque se manifesta como algumas declarações que você precisa colocar no código não significa que não é a mesma coisa.
Kaz
2

"Selar" ou "finalizar" em sistemas de objetos permite certas otimizações, porque o gráfico completo de expedição é conhecido.

Ou seja, dificultamos que o sistema seja transformado em outra coisa, como uma compensação pelo desempenho. (Essa é a essência da maior otimização.)

Em todos os outros aspectos, é uma perda. Os sistemas devem ser abertos e extensíveis por padrão. Deve ser fácil adicionar novos métodos a todas as classes e estender arbitrariamente.

Atualmente, não ganhamos nenhuma nova funcionalidade ao tomar medidas para impedir futuras extensões.

Portanto, se fizermos isso em prol da própria prevenção, o que estamos fazendo é tentar controlar a vida dos futuros mantenedores. É sobre ego. "Mesmo quando eu não trabalhar mais aqui, esse código será mantido do meu jeito, caramba!"

Kaz
fonte
1

As aulas de teste parecem vir à mente. Essas são as classes chamadas de maneira automatizada ou "à vontade", com base no que o programador / testador está tentando realizar. Não sei se já vi ou ouvi falar de uma classe de testes concluída que é privada.

joshin4colours
fonte
Do que você está falando? De que maneira as classes de teste não devem ser finais / seladas / abstratas?
1

O momento em que você precisa considerar uma aula que deve ser estendida é quando você está fazendo um planejamento real para o futuro. Deixe-me dar um exemplo da vida real do meu trabalho.

Gasto muito tempo escrevendo ferramentas de interface entre nosso principal produto e sistemas externos. Quando fazemos uma nova venda, um grande componente é um conjunto de exportadores projetados para serem executados em intervalos regulares que geram arquivos de dados detalhando os eventos que ocorreram naquele dia. Esses arquivos de dados são consumidos pelo sistema do cliente.

Esta é uma excelente oportunidade para estender as aulas.

Eu tenho uma classe Export, que é a classe base de todos os exportadores. Ele sabe como se conectar ao banco de dados, descobrir onde ele chegou da última vez em que foi executado e criar arquivos dos arquivos de dados que ele gera. Ele também fornece gerenciamento de arquivos de propriedades, registro e manipulação simples de exceções.

Além disso, tenho um exportador diferente para trabalhar com cada tipo de dados, talvez haja atividade do usuário, dados transacionais, dados de gerenciamento de caixa etc.

No topo dessa pilha, coloco uma camada específica do cliente que implementa a estrutura do arquivo de dados que o cliente precisa.

Dessa maneira, o exportador de base muda muito raramente. Às vezes, os principais exportadores de tipos de dados mudam, mas raramente, e geralmente apenas para lidar com alterações no esquema do banco de dados que devem ser propagadas para todos os clientes de qualquer maneira. O único trabalho que tenho que fazer para cada cliente é a parte do código específica para esse cliente. Um mundo perfeito!

Portanto, a estrutura se parece com:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Meu ponto principal é que, ao arquitetar o código dessa maneira, eu posso usar a herança principalmente para reutilizar o código.

Devo dizer que não consigo pensar em nenhuma razão para passar por três camadas.

Eu usei duas camadas várias vezes, por exemplo, para ter uma Tableclasse comum que implementa consultas de tabela de banco de dados enquanto subclasses de Tableimplementam os detalhes específicos de cada tabela usando um enumpara definir os campos. Deixar o enumimplementar uma interface definida na Tableclasse faz todo tipo de sentido.

OldCurmudgeon
fonte
1

Eu achei as classes abstratas úteis, mas nem sempre necessárias, e as classes seladas se tornam um problema ao aplicar testes de unidade. Você não pode zombar ou stub de uma aula selada, a menos que use algo como o Teleriks justmock.

Dan H
fonte
1
Este é um bom comentário, mas não responde diretamente à pergunta. Por favor, considere expandir seus pensamentos aqui.
1

Eu concordo com a sua opinião. Eu acho que em Java, por padrão, as classes devem ser declaradas "finais". Se você não finalizar, prepare-o especificamente e documente-o para extensão.

A principal razão para isso é garantir que quaisquer instâncias de suas classes cumpram a interface que você originalmente projetou e documentou. Caso contrário, um desenvolvedor que use suas classes poderá criar código quebradiço e inconsistente e, por sua vez, transmiti-lo a outros desenvolvedores / projetos, tornando os objetos de suas classes não confiáveis.

De uma perspectiva mais prática, de fato, há uma desvantagem nisso, já que os clientes da interface da sua biblioteca não poderão fazer nenhum ajuste e usar suas classes de maneiras mais flexíveis que você pensava originalmente.

Pessoalmente, por todo o código de má qualidade que existe (e, como estamos discutindo isso em um nível mais prático, eu diria que o desenvolvimento Java é mais propenso a isso), acho que essa rigidez é um pequeno preço a pagar em nossa busca por soluções mais fáceis. manter código.

DPM
fonte