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.
object-oriented
programming-practices
Nicolas Repiquet
fonte
fonte
Open/Closed principle
, não oClosed Principle.
Respostas:
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.
fonte
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.
fonte
new List<Foo>()
por algo comoList<Foo>.CreateMutable()
.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.
fonte
Dois casos comuns em que você precisará de baunilha, classes não seladas:
Técnico: se você possui uma hierarquia com mais de dois níveis de profundidade e deseja instanciar algo no meio.
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 qualquer
Control
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:
virtual
funcionalidade, com exceção das implementações explícitas da interface.internal
vez de selar a classe, o que permite que ele seja herdado dentro da sua base de código, mas não fora dela.fonte
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 :
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:
Também vale a pena mencionar a seguinte pergunta "filosófica": É se uma classe é
sealed
/final
parte 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.fonte
Uma aula não deve ser final / selada nem abstrata se:
Por exemplo, faça a
ObservableCollection<T>
aula em C # . Ele só precisa adicionar o aumento de eventos às operações normais de aCollection<T>
, e é por isso que as subclassesCollection<T>
.Collection<T>
é uma classe viável por si só, e por issoObservableCollection<T>
.fonte
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.Collection<T>
uma "classe abstrata meia-boca"?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.ObservableCollection
herdaCollection
e não é selado, para que você possa herdar novamente. E você pode acessar aItems
propriedade protegida, permitindo adicionar itens na coleção sem gerar eventos ... Agradável.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.
fonte
"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!"
fonte
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.
fonte
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:
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
Table
classe comum que implementa consultas de tabela de banco de dados enquanto subclasses deTable
implementam os detalhes específicos de cada tabela usando umenum
para definir os campos. Deixar oenum
implementar uma interface definida naTable
classe faz todo tipo de sentido.fonte
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.
fonte
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.
fonte