Omitindo herança em linguagens de programação

10

Estou desenvolvendo minha própria linguagem de programação. É uma linguagem de uso geral (pense em Python estaticamente digitado para a área de trabalho, por exemplo int x = 1;) não destinada à nuvem.

Você acha que não há problema em não permitir herança ou Mixins? (considerando que o usuário teria pelo menos interfaces)

Por exemplo: Google Go, uma linguagem de sistemas que chocou a comunidade de programação por não permitir herança.

Christopher
fonte

Respostas:

4

De fato, descobrimos cada vez mais que o uso da herança é subótimo porque (entre outros) leva a um acoplamento rígido.

A herança de implementação sempre pode ser substituída por herança de interface mais composição, e os projetos de software mais modernos tendem a ir na direção de usar cada vez menos a herança, em favor da composição.

Então , sim , não fornecer herança em particular é uma decisão de design completamente válida e muito em voga .

Mixins, por outro lado, não são mesmo (ainda) um recurso de linguagem mainstream, e idiomas que não oferecem um recurso chamado “mixin” muitas vezes entender as coisas muito diferentes por ele. Sinta-se livre para fornecer, ou não. Pessoalmente, acho muito útil, mas implementá-lo corretamente pode ser muito difícil.

Konrad Rudolph
fonte
13

Raciocinar sobre se a herança (ou qualquer recurso único, na verdade) é necessário ou não, sem considerar também o restante da semântica da linguagem; você está discutindo no vácuo.

O que você precisa é de uma filosofia consistente de design de linguagem; o idioma precisa ser capaz de resolver com elegância os problemas para os quais foi projetado. O modelo para conseguir isso pode ou não exigir herança, mas é difícil julgá-lo sem uma visão geral.

Se, por exemplo, sua linguagem possui funções de primeira classe, aplicação de função parcial, tipos de dados polimórficos, variáveis ​​de tipo e tipos genéricos, você cobriu praticamente as mesmas bases que faria com a herança clássica de POO, mas usando um paradigma diferente.

Se você possui ligação tardia, tipagem dinâmica, métodos como propriedades, argumentos de função flexíveis e funções de primeira classe, também cobre os mesmos motivos, mas novamente, usando um paradigma diferente.

(Encontrar exemplos para os dois paradigmas descritos é deixado como um exercício para o leitor.)

Então, pense no tipo de semântica que você deseja, brinque com eles e veja se eles são suficientes sem herança. Se não estiverem, você pode decidir incluir a herança na mistura ou pode decidir que algo está faltando.

tdammers
fonte
9

Sim.

Eu acho que não permitir a herança é bom, principalmente se o seu idioma for digitado dinamicamente. Você pode obter uma reutilização de código semelhante, por exemplo, delegação ou composição. Por exemplo, eu escrevi alguns programas moderadamente complicados - ok, não tão complicados assim :) - em JavaScript sem herança. Basicamente, usei objetos como estruturas de dados algébricas com algumas funções anexadas como métodos. Eu também tinha várias funções que não eram métodos que operavam nesses objetos.

Se você tem digitação dinâmica - e presumo que sim - você também pode ter polimorfismo sem herança. Além disso, se você permitir adicionar métodos arbitrários a objetos em tempo de execução, não precisará realmente de coisas como mixins.

Outra opção - que eu acho que é boa - é emular o JavaScript e usar protótipos. Estes são mais simples que as classes e também muito flexíveis. Apenas algo a considerar.

Então, ao todo, eu aceitaria.

Tikhon Jelvis
fonte
2
Desde que exista uma maneira razoável de realizar os tipos de tarefas normalmente tratadas com herança, vá em frente. Você pode experimentar alguns casos de uso, para certificar-se (possivelmente até mesmo indo tão longe como para problemas de exemplo solicitar de outros, para evitar a auto-polarização ...)
comingstorm
Não É estaticamente e fortemente tipado, mencionarei isso no topo.
Christopher
Como é digitado estaticamente, você não pode simplesmente adicionar métodos aleatórios a objetos em tempo de execução. No entanto, você ainda pode digitar o pato: consulte os protocolos Gosu para obter um exemplo. Eu usei protocolos um pouco durante o verão e eles foram realmente úteis. O Gosu também possui "aprimoramentos", que são uma maneira de adicionar métodos às classes após o fato. Você também pode adicionar algo parecido.
Tikhon Jelvis
2
Por favor, por favor, pare de relacionar herança e reutilização de código. Há muito se sabe que a herança é realmente uma ferramenta ruim para a reutilização de código.
Jan Hudec
3
A herança é apenas uma ferramenta para reutilização de código. Muitas vezes, é uma ferramenta muito boa e outras vezes é horrível. Preferir composição sobre herança não significa nunca usar herança.
Michael K
8

Sim, é uma decisão de projeto perfeitamente razoável omitir herança.

De fato, existem boas razões para remover a herança de implementação, pois ela pode produzir código extremamente complexo e difícil de manter. Eu chegaria ao ponto de considerar a herança (como normalmente é implementada na maioria dos idiomas OOP) como uma característica incorreta.

O Clojure, por exemplo, não fornece herança de implementação, preferindo fornecer um conjunto de recursos ortogonais (protocolos, dados, funções, macros) que podem ser usados ​​para obter os mesmos resultados, mas com muito mais clareza.

Aqui está um vídeo que eu achei muito esclarecedor sobre esse tópico geral, onde Rich Hickey identifica fontes fundamentais de complexidade em linguagens de programação (incluindo herança) e apresenta alternativas para cada uma: Simples facilitado

Mikera
fonte
Em muitos casos, a herança da interface é mais apropriada. Porém, se usada com moderação, a herança de implação pode permitir a remoção de muitos códigos clichê, e é a solução mais elegante. Requer apenas programadores que sejam mais inteligentes e mais cuidadosos que o macaco Python / JavaScript comum.
Erik Alapää
4

Quando me deparei com o fato de que as classes VB6 não suportam herança (apenas interfaces), isso realmente me incomodou (e ainda o faz).

No entanto, a razão de ser tão ruim é que ele também não tinha parâmetros de construtor; portanto, não era possível fazer injeção de dependência normal (DI). Se você tem DI, isso é mais importante que a herança, porque você pode seguir o princípio de favorecer a composição sobre a herança. Essa é a melhor maneira de reutilizar o código de qualquer maneira.

Não tendo Mixins embora? Se você deseja implementar uma interface e delegar todo o trabalho dessa interface em um conjunto de objetos por meio de injeção de dependência, então os Mixins são ideais. Caso contrário, você precisará escrever todo o código padrão para delegar todos os métodos e / ou propriedades ao objeto filho. Eu faço muito (graças ao C #) e é uma coisa que eu gostaria de não ter que fazer.

Scott Whitlock
fonte
Sim, o que levou a essa pergunta é a implementação do SVG DOM, onde a reutilização de código é muito benéfica.
Christopher
2

Para discordar de outra resposta: não, você lança recursos quando incompatíveis com algo que deseja mais. Java (e outras linguagens do GC) descartou o gerenciamento de memória explícita porque queria mais a segurança do tipo. Haskell eliminou a mutação porque queria mais raciocínio equacional e tipos extravagantes. Mesmo a C descartou (ou declarou ilegal) certos tipos de alias e outros comportamentos porque queria mais otimizações do compilador.

Portanto, a pergunta é: o que você quer mais do que herança?

Ryan Culpepper
fonte
11
Somente esse gerenciamento explícito de memória e segurança de tipo são completamente independentes e definitivamente não são incompatíveis. O mesmo se aplica à mutação e aos "tipos extravagantes". Eu concordo com o raciocínio geral, mas seus exemplos são mal escolhidos.
Konrad Rudolph
@KonradRudolph, gerenciamento de memória e segurança de tipo estão intimamente relacionados. Uma operação free/ deletedá a você a capacidade de invalidar referências; a menos que seu sistema de tipos seja capaz de rastrear todas as referências afetadas, isso torna o idioma inseguro. Em particular, nem C nem C ++ são do tipo seguro. É verdade que você pode fazer compromissos em um ou outro (por exemplo, tipos lineares ou restrições de alocação) para fazê-los concordar. Para ser mais preciso, eu deveria ter dito que o Java queria segurança de tipo com um sistema de tipo simples e particular e alocação irrestrita mais.
Ryan Culpepper
Bem, isso depende de como você define o tipo de segurança. Por exemplo, se você adota a definição (totalmente razoável) de segurança de tipo em tempo de compilação e deseja que a integridade de referência faça parte do seu sistema de tipos, Java também não é um tipo seguro (já que permite nullreferências). Proibir freeé uma restrição adicional bastante arbitrária. Em resumo, se um idioma é do tipo seguro depende mais da sua definição de segurança do tipo do que do idioma.
Konrad Rudolph
11
@ Ryan: Os ponteiros são perfeitamente seguros para o tipo. Eles sempre se comportam de acordo com sua definição. Pode não ser como você gosta, mas é sempre de acordo com a forma como são definidos. Você está tentando esticá-los para serem algo que não são. Ponteiros inteligentes podem garantir a segurança da memória de maneira bastante trivial em C ++.
precisa saber é
@KonradRudolph: Em Java, toda Treferência se refere a nullum objeto ou a uma classe que se estende T; nullé feio, mas as operações nullgeram uma exceção bem definida, em vez de corromper o tipo invariante acima. Contraste com C ++: após uma chamada para delete, um T*ponteiro pode apontar para a memória que não contém mais um Tobjeto. Pior ainda, ao fazer uma atribuição de campo usando esse ponteiro em uma atribuição, você pode atualizar inteiramente um campo de um objeto de uma classe diferente se ele tiver sido colocado em um endereço próximo. Isso não é segurança de tipo por qualquer definição útil do termo.
Ryan Culpepper
1

Não.

Se você deseja remover um recurso de idioma base, está declarando universalmente que nunca é necessário (ou é injustificadamente difícil de implementar, o que não se aplica aqui). E "nunca" é uma palavra forte em engenharia de software. Você precisaria de uma justificativa muito poderosa para fazer essa afirmação.

DeadMG
fonte
8
Isso dificilmente é verdade: todo o projeto do Java era remover recursos como sobrecarga de operador, herança múltipla e gerenciamento manual de memória. Além disso, acho que tratar qualquer recurso de linguagem como "sagrado" está errado; em vez de justificar a remoção de um recurso, você deve justificar acrescentando que - eu não consigo pensar em nenhum apresenta cada linguagem deve ter.
Tikhon Jelvis
6
@TikhonJelvis: É por isso que Java é uma linguagem terrível , e é a falta de qualquer coisa, exceto "USE A HERANÇA COLECIONADA POR LIXO" é a razão número um . Os recursos de linguagem devem ser justificados, eu concordo, mas este é um recurso de idioma básico - possui muitos aplicativos úteis e não pode ser replicado pelo programador sem violar o DRY.
DeadMG
11
@DeadMG wow! Linguagem bastante forte.
Christopher
@DeadMG: Comecei a escrever uma refutação como um comentário, mas depois a transformei em uma resposta (qv).
Ryan Culpepper
11
"A perfeição é alcançada não quando não há mais nada a acrescentar, mas quando não há mais nada a remover" (q)
9000
1

Falando de uma perspectiva estritamente C ++, a herança é útil para dois propósitos principais:

  1. Permite a resusibilidade do código
  2. Em combinação com a substituição em uma classe filho, ele permite que você use um ponteiro de classe base para manipular um objeto de classe filho sem conhecer seu tipo.

Para o ponto 1, desde que você tenha uma maneira de compartilhar código sem ter que fazer acrobacias demais, você pode acabar com a herança. Para o ponto 2. Você pode seguir o caminho do java e insistir em uma interface para implementar esse recurso.

Os benefícios de remover a herança são

  1. impedir longas hierarquias e seus problemas associados. Seu mecanismo de compartilhamento de código deve ser bom o suficiente para isso.
  2. Evite que as mudanças nas classes pai quebrem as classes filho.

A desvantagem é em grande parte entre "Não se repita" e Flexibilidade, por um lado, e prevenção de problemas, por outro. Pessoalmente, eu odiaria ver a herança passar do C ++ simplesmente porque algum outro desenvolvedor pode não ser inteligente o suficiente para prever os problemas.

DPD
fonte
1

Você acha que não há problema em não permitir herança ou Mixins? (considerando que o usuário teria pelo menos interfaces)

Você pode fazer um trabalho muito útil sem herança de implementação ou mixins, mas gostaria de saber se você deve ter algum tipo de herança de interface, ou seja, uma declaração que diz que se um objeto implementa a interface A, ele também precisa da interface B (ou seja, A é uma especialização de B e, portanto, existe um relacionamento de tipo). Por outro lado, seu objeto resultante precisa apenas registrar que implementa as duas interfaces, portanto, não há muita complexidade lá. Tudo perfeitamente factível.

A falta de herança de implementação tem uma desvantagem clara: você não poderá construir vtables indexadas numéricas para suas classes e, portanto, terá que fazer pesquisas de hash para cada chamada de método (ou descobrir uma maneira inteligente de evitá-las). Isso pode ser doloroso se você estiver direcionando até valores fundamentais (por exemplo, números) através desse mecanismo. Mesmo uma implementação de hash muito boa pode ser cara quando você a atinge várias vezes em cada loop interno!

Donal Fellows
fonte