Por que a herança e o polimorfismo são tão amplamente usados?

18

Quanto mais aprendo sobre diferentes paradigmas de programação, como programação funcional, mais começo a questionar a sabedoria dos conceitos de POO, como herança e polimorfismo. Eu aprendi sobre herança e polimorfismo na escola e, na época, o polimorfismo parecia uma maneira maravilhosa de escrever código genérico que permitia fácil extensibilidade.

Mas diante da digitação do pato (dinâmica e estática) e dos recursos funcionais, como funções de ordem superior, comecei a considerar a herança e o polimorfismo como impondo uma restrição desnecessária com base em um conjunto frágil de relacionamentos entre objetos. A idéia geral por trás do polimorfismo é que você escreve uma função uma vez e, posteriormente, pode adicionar novas funcionalidades ao seu programa sem alterar a função original - tudo que você precisa fazer é criar outra classe derivada que implemente os métodos necessários.

Mas isso é muito mais simples de se obter através da digitação de pato, seja em uma linguagem dinâmica como Python ou em uma linguagem estática como C ++.

Como exemplo, considere a seguinte função Python, seguida pelo seu equivalente estático em C ++:

def foo(obj):
   obj.doSomething()

template <class Obj>
void foo(Obj& obj)
{
   obj.doSomething();
}

O equivalente a OOP seria algo como o seguinte código Java:

public void foo(DoSomethingable obj)
{
  obj.doSomething();
}

A principal diferença, é claro, é que a versão Java requer a criação de uma interface ou hierarquia de herança antes de funcionar. A versão Java, portanto, envolve mais trabalho e é menos flexível. Além disso, acho que a maioria das hierarquias de herança do mundo real é um pouco instável. Todos nós já vimos exemplos inventados de Formas e Animais, mas, no mundo real, à medida que os requisitos de negócios mudam e novos recursos são adicionados, é difícil realizar qualquer trabalho antes que você realmente precise ampliar o relacionamento "é um" entre subclasses ou remodele / refatorar sua hierarquia para incluir outras classes-base ou interfaces para acomodar novos requisitos. Com a digitação do duck, você não precisa se preocupar em modelar nada - apenas se preocupa com a funcionalidade necessária.

No entanto, herança e polimorfismo são tão populares que duvido que seria exagero chamá-los de estratégia dominante para extensibilidade e reutilização de código. Então, por que a herança e o polimorfismo são tão bem-sucedidos? Estou negligenciando algumas vantagens sérias que a herança / polimorfismo tem sobre a digitação de patos?

Channel72
fonte
Eu não sou um especialista em Python, então tenho que perguntar: o que aconteceria se objnão tivesse um doSomethingmétodo? Uma exceção é levantada? Nada acontece?
FrustratedWithFormsDesigner
6
@ Frustrated: Uma exceção específica muito clara é levantada.
dsimcha
11
A digitação de pato não é apenas polimorfismo, que leva até onze? Meu palpite é que você não está frustrado com o POO, mas com suas encarnações estaticamente tipadas. Eu posso entender isso, realmente, mas isso não faz da OOP uma má idéia.
3
Primeiro leia "amplamente" como "descontroladamente" ...

Respostas:

22

Eu concordo principalmente com você, mas por diversão eu interpreto o advogado do diabo. As interfaces explícitas fornecem um único local para procurar um contrato especificado explicitamente e formalmente , informando o que um tipo deve fazer. Isso pode ser importante quando você não é o único desenvolvedor em um projeto.

Além disso, essas interfaces explícitas podem ser implementadas com mais eficiência do que a digitação com patos. Uma chamada de função virtual tem um pouco mais de sobrecarga do que uma chamada de função normal, exceto que ela não pode ser incorporada. A digitação de pato tem uma sobrecarga substancial. A digitação estrutural no estilo C ++ (usando modelos) pode gerar grandes quantidades de inchaço no arquivo de objeto (já que cada instanciação é independente no nível do arquivo de objeto) e não funciona quando você precisa de polimorfismo no tempo de execução, não no tempo de compilação.

Conclusão: Concordo que a herança e o polimorfismo no estilo Java podem ser uma PITA e que alternativas devem ser usadas com mais frequência, mas ainda tem suas vantagens.

dsimcha
fonte
29

Herança e polimorfismo são amplamente utilizados porque funcionam , para certos tipos de problemas de programação.

Não é que eles sejam amplamente ensinados nas escolas, é o contrário: eles são amplamente ensinados nas escolas porque as pessoas (também conhecidas como mercado) descobriram que funcionavam melhor do que as ferramentas antigas, e então as escolas começaram a ensiná-las. [Anedota: quando eu estava aprendendo OOP pela primeira vez, era extremamente difícil encontrar uma faculdade que ensinasse qualquer idioma OOP. Dez anos depois, foi difícil encontrar uma faculdade que não ensinasse um idioma OOP.]

Você disse:

A idéia geral por trás do polimorfismo é que você escreve uma função uma vez e, posteriormente, pode adicionar novas funcionalidades ao seu programa sem alterar a função original - tudo o que você precisa fazer é criar outra classe derivada que implemente os métodos necessários

Eu digo:

Não, não é

O que você descreve não é polimorfismo, mas herança. Não é de admirar que você esteja tendo problemas de POO! ;-)

Faça o backup de uma etapa: o polimorfismo é um benefício da passagem de mensagens; significa simplesmente que cada objeto é livre para responder a uma mensagem à sua maneira.

Então ... Duck Typing é (ou melhor, permite) polimorfismo

A essência da sua pergunta parece não ser que você não entende OOP ou que não gosta, mas não gosta de definir interfaces . Tudo bem, e desde que você tenha cuidado, as coisas funcionarão bem. A desvantagem é que se você cometeu um erro - omitiu um método, por exemplo - não descobrirá até o tempo de execução.

Isso é estático versus dinâmico, que é uma discussão tão antiga quanto o Lisp, e não se limita ao OOP.

Steven A. Lowe
fonte
2
+1 para "tipagem de pato é polimorfismo". Polimorfismo e herança foram subconscientemente encadeados por muito tempo, e a digitação estática estrita é a única razão real que eles tiveram que ser.
cHao 02/12/19
+1. Eu acho que a distinção estática versus dinâmica deve ser mais do que uma nota lateral - está no centro da distinção entre tipagem de pato e polimorfismo no estilo java.
Sava B.
8

Comecei a considerar a herança e o polimorfismo como impondo uma restrição desnecessária com base em um conjunto frágil de relações entre objetos.

Por quê?

A herança (com ou sem digitação do pato) garante a reutilização de um recurso comum. Se for comum, você pode garantir que seja reutilizado de forma consistente nas subclasses.

Isso é tudo. Não há "restrição desnecessária". É uma simplificação.

Polimorfismo, da mesma forma, é o que significa "digitação de pato". Mesmos métodos. Muitas classes com uma interface idêntica, mas implementações diferentes.

Não é uma restrição. É uma simplificação.

S.Lott
fonte
7

A herança é abusada, mas a digitação com patos também. Ambos podem levar a problemas.

Com a digitação forte, você obtém muitos "testes de unidade" em tempo de compilação. Com a digitação de patos, você geralmente precisa escrevê-los.

ElGringoGrande
fonte
3
Seu comentário sobre o teste se aplica apenas à digitação dinâmica do pato. A digitação estática de patos no estilo C ++ (com modelos) fornece erros no tempo de compilação. (Embora, concedido erros modelo C ++ são muito difíceis de decifrar, mas isso é uma questão completamente diferente.)
Channel72
2

O lado bom de aprender coisas na escola é que você as aprende. O que não é tão bom é que você pode aceitá-los um pouco dogmaticamente, sem entender quando eles são úteis e quando não.

Então, se você aprendeu dogmaticamente, mais tarde poderá "se rebelar" da mesma forma dogmaticamente na outra direção. Isso também não é bom.

Como em qualquer dessas idéias, é melhor adotar uma abordagem pragmática. Desenvolva um entendimento de onde eles se encaixam e onde não se encaixam. E ignore todas as maneiras pelas quais eles foram vendidos em excesso.

Mike Dunlavey
fonte
2

Sim, digitação estática e interfaces são restrições. Mas tudo desde que a programação estruturada foi inventada (isto é, "considerado perigoso") tem sido sobre nos restringir. O tio Bob tem uma excelente opinião sobre isso em seu blog de vídeo .

Agora, pode-se argumentar que a constrição é ruim, mas, por outro lado, traz ordem, controle e familiaridade a um tópico muito complexo.

Restrições relaxantes ao (re) introduzir a digitação dinâmica e até o acesso direto à memória é um conceito muito poderoso, mas também pode dificultar o trabalho de muitos programadores. Especialmente os programadores costumavam confiar no compilador e digitar segurança durante grande parte do seu trabalho.

Martin Wickman
fonte
1

A herança é um relacionamento muito forte entre duas classes. Eu não acho que Java tenha mais força. Portanto, você só deve usá-lo quando quiser. A herança pública é um relacionamento "é-a", não um "geralmente é-a". É muito, muito fácil usar demais a herança e acabar com uma bagunça. Em muitos casos, a herança é usada para representar "has-a" ou "takes-funcionalidade-de-a", e isso geralmente é melhor realizado pela composição.

O polimorfismo é uma conseqüência direta do relacionamento "é-a". Se o Derivado herdar da Base, cada Base Derivada "é-a" e, portanto, você poderá usar um Derivado sempre que usar uma Base. Se isso não faz sentido em uma hierarquia de herança, a hierarquia está errada e provavelmente há muita herança acontecendo.

A digitação com patos é um recurso interessante, mas o compilador não avisa se você for usá-lo mal. Se você não quiser lidar com exceções em tempo de execução, precisará garantir que está obtendo os resultados certos sempre. Pode ser mais fácil definir uma hierarquia de herança estática.

Não sou muito fã de digitação estática (considero que muitas vezes é uma forma de otimização prematura), mas elimina algumas classes de erros e muitas pessoas pensam que vale a pena eliminar essas classes.

Se você gosta de digitação dinâmica e de digitação melhor do que a estática e as hierarquias de herança definidas, tudo bem. No entanto, a maneira Java tem suas vantagens.

David Thornley
fonte
0

Percebi que quanto mais uso os fechamentos de C #, menos estou fazendo OOP tradicional. A herança costumava ser a única maneira de compartilhar facilmente a implementação, por isso acho que foi muitas vezes usada em excesso e os limites da concepção foram levados longe demais.

Embora você possa usar os fechamentos para fazer a maior parte do que você faria com a herança, também pode ser feio.

Basicamente, é uma situação da ferramenta certa para o trabalho: OOP tradicional pode funcionar muito bem quando você tem um modelo adequado e os fechamentos podem funcionar muito bem quando você não tem.

Ben Hughes
fonte
-1

A verdade está em algum lugar no meio. Eu gosto de como a linguagem de tipo estaticamente C # 4.0 suporta "digitação de pato" por palavra-chave "dinâmica".

SiberianGuy
fonte
-1

A herança, mesmo quando a razão do ponto de vista de PF, é um ótimo conceito; ele não apenas economiza muito tempo, mas também dá sentido à relação entre certos objetos em seu programa.

public class Animal {
    public virtual string Sound () {
        return "Some Sound";
    }
}

public class Dog : Animal {
    public override string Sound () {
        return "Woof";
    }
}

public class Cat : Animal {
    public override string Sound () {
        return "Mew";
    }
}

public class GoldenRetriever : Dog {

}

Aqui a classe GoldenRetrievertem o mesmo Soundque Dogde graça, graças à herança.

Vou escrever o mesmo exemplo com o meu nível de Haskell para você ver a diferença

data Animal = Animal | Dog | Cat | GoldenRetriever

sound :: Animal -> String
sound Animal = "Some Sound"
sound Dog = "Woof"
sound Cat = "Mew"
sound GoldenRetriever = "Woof"

Em aqui você não escapam ter que especificar soundpara GoldenRetriever. A coisa mais fácil em geral seria

sound GoldenRetriever = sound Dog

mas imagine se você tiver 20 funções! Se houver um especialista em Haskell, mostre-nos uma maneira mais fácil.

Dito isso, seria ótimo ter correspondência e herança de padrões ao mesmo tempo, onde uma função seria padronizada para a classe base se a instância atual não tiver a implementação.

Cristian Garcia
fonte
5
Eu juro, Animalé o anti-tutorial do OOP.
Telastyn
@Telastyn Aprendi o exemplo de Derek Banas no Youtube. Ótimo professor. Na minha opinião, é muito fácil visualizar e raciocinar. Você pode editar a postagem com um exemplo melhor.
Cristian Garcia
este não parece acrescentar nada substancial sobre anteriores 10 respostas
mosquito
@gnat Enquanto a "substância" já está disponível, uma pessoa nova no assunto pode achar um exemplo mais fácil de entender.
Cristian Garcia
2
Animais e formas são realmente más aplicações de polimorfismo por várias razões que foram discutidas ad nauseum neste site. Basicamente, dizer que um gato "é um" animal não tem sentido, pois não há "animal" concreto. O que você realmente pretende modelar é comportamento, não identidade. Definir uma interface "CanSpeak" que possa ser implementada como um miado ou um latido faz sentido. Definir uma interface "HasArea" que um quadrado e um círculo podem implementar faz sentido, mas não um Shape genérico.