Herança (programação orientada a objetos) - Inheritance (object-oriented programming)

Na programação orientada a objetos , a herança é o mecanismo de basear um objeto ou classe em outro objeto ( herança baseada em protótipo ) ou classe ( herança baseada em classe ), mantendo uma implementação semelhante . Também definido como derivar novas classes ( subclasses ) das existentes, como superclasse ou classe base e, em seguida, formá-las em uma hierarquia de classes. Na maioria das linguagens orientadas a objetos baseadas em classes, um objeto criado por herança, um "objeto filho", adquire todas as propriedades e comportamentos do "objeto pai", com exceção de: construtores , destruidor, operadores sobrecarregados e funções de amigo de a classe base. A herança permite que os programadores criem classes que são construídas sobre as classes existentes, para especificar uma nova implementação enquanto mantém os mesmos comportamentos ( realizando uma interface ), para reutilizar o código e estender independentemente o software original por meio de classes públicas e interfaces . Os relacionamentos de objetos ou classes por meio de herança dão origem a um grafo direcionado .

A herança foi inventada em 1969 para Simula e agora é usada em muitas linguagens de programação orientadas a objetos, como Java , C ++ , PHP e Python .

Uma classe herdada é chamada de subclasse de sua classe pai ou superclasse. O termo "herança" é usado livremente para programação baseada em classe e baseada em protótipo, mas em uso restrito o termo é reservado para programação baseada em classe (uma classe herda de outra), com a técnica correspondente na programação baseada em protótipo sendo em vez disso, é chamado de delegação (um objeto delega para outro).

Herança não deve ser confundida com subtipagem . Em algumas línguas, a herança e a subtipagem concordam, enquanto em outras elas diferem; em geral, a subtipagem estabelece uma relação é-um , enquanto a herança apenas reutiliza a implementação e estabelece uma relação sintática, não necessariamente uma relação semântica (a herança não garante a subtipagem comportamental). Para distinguir esses conceitos, a subtipagem às vezes é chamada de herança de interface (sem reconhecer que a especialização de variáveis ​​de tipo também induz uma relação de subtipagem), enquanto a herança, conforme definida aqui, é conhecida como herança de implementação ou herança de código . Ainda assim, a herança é um mecanismo comumente usado para estabelecer relacionamentos de subtipos.

Herança é contrastada com composição de objeto , onde um objeto contém outro objeto (ou objetos de uma classe contêm objetos de outra classe); veja composição sobre herança . A composição implementa um relacionamento tem-um , em contraste com o relacionamento é-um de subtipagem.

Tipos

Herança única
Herança múltipla

Existem vários tipos de herança, com base em paradigma e linguagem específica.

Herança única
onde as subclasses herdam os recursos de uma superclasse. Uma classe adquire as propriedades de outra classe.
Herança múltipla
onde uma classe pode ter mais de uma superclasse e herdar recursos de todas as classes pai.

"A herança múltipla  ... era amplamente considerada muito difícil de implementar com eficiência. Por exemplo, em um resumo de C ++ em seu livro sobre Objective C , Brad Cox na verdade afirmou que adicionar herança múltipla ao C ++ era impossível. Portanto, a herança múltipla parecia mais um desafio. Como eu havia considerado a herança múltipla já em 1982 e encontrado uma técnica de implementação simples e eficiente em 1984, não pude resistir ao desafio. Suspeito que este seja o único caso em que a moda afetou a sequência de eventos . "

Herança multinível
onde uma subclasse é herdada de outra subclasse. Não é incomum que uma classe seja derivada de outra classe derivada, conforme mostrado na figura "Herança multinível".
Herança multinível
A classe A serve como uma classe base para a classe de derivados B , que por sua vez serve como uma classe base para a classe derivada C . A classe B é conhecida como intermediário classe base, porque fornece uma ligação para a herança entre A e C . A cadeia ABC é conhecida como caminho de herança .
Uma classe derivada com herança multinível é declarada da seguinte maneira:
Class A(...);      // Base class
Class B : public A(...);   // B derived from A
Class C : public B(...);   // C derived from B
Este processo pode ser estendido a qualquer número de níveis.
Herança hierárquica
É aqui que uma classe serve como superclasse (classe base) para mais de uma subclasse. Por exemplo, uma classe pai, A, pode ter duas subclasses B e C. As classes pai de B e C são A, mas B e C são duas subclasses separadas.
Herança híbrida
Herança híbrida é quando ocorre uma combinação de dois ou mais dos tipos de herança acima. Um exemplo disso é quando a classe A tem uma subclasse B que possui duas subclasses, C e D. Esta é uma mistura de herança multinível e herança hierárquica.

Subclasses e superclasses

Subclasses , as classes derivadas , aulas de herdeiro , ou classes filhas são modulares aulas derivativos que herda um ou mais língua entidades de uma ou mais outras classes (chamados superclasse , classes base ou classes pai ). A semântica da herança de classes varia de idioma para idioma, mas normalmente a subclasse herda automaticamente as variáveis ​​de instância e funções de membro de suas superclasses.

A forma geral de definir uma classe derivada é:

class SubClass: visibility SuperClass
{
    // subclass members
};
  • Os dois pontos indicam que a subclasse herda da superclasse. A visibilidade é opcional e, se houver, pode ser privada ou pública . A visibilidade padrão é privada . Visibilidade especifica se os recursos da classe base são derivados de forma privada ou derivada publicamente .

Algumas linguagens também suportam a herança de outras construções. Por exemplo, em Eiffel , os contratos que definem a especificação de uma classe também são herdados pelos herdeiros. A superclasse estabelece uma interface comum e funcionalidade básica, que subclasses especializadas podem herdar, modificar e complementar. O software herdado por uma subclasse é considerado reutilizado na subclasse. Uma referência a uma instância de uma classe pode, na verdade, estar se referindo a uma de suas subclasses. A classe real do objeto que está sendo referenciado é impossível de prever em tempo de compilação . Uma interface uniforme é usada para invocar as funções-membro de objetos de várias classes diferentes. As subclasses podem substituir funções de superclasse por funções inteiramente novas que devem compartilhar a mesma assinatura de método .

Classes não subclassíveis

Em algumas linguagens, uma classe pode ser declarada como não subclassível adicionando certos modificadores de classe à declaração da classe. Os exemplos incluem a finalpalavra - chave em Java e C ++ 11 em diante ou a sealedpalavra - chave em C #. Esses modificadores são adicionados à declaração da classe antes da classpalavra - chave e da declaração do identificador da classe. Essas classes não subclassíveis restringem a reutilização , particularmente quando os desenvolvedores só têm acesso a binários pré-compilados e não ao código-fonte .

Uma classe não subclassível não tem subclasses, então pode ser facilmente deduzido em tempo de compilação que referências ou ponteiros para objetos dessa classe estão na verdade referenciando instâncias dessa classe e não instâncias de subclasses (elas não existem) ou instâncias de superclasses ( atualizar um tipo de referência viola o sistema de tipos). Como o tipo exato do objeto que está sendo referenciado é conhecido antes da execução, a vinculação antecipada (também chamada de despacho estático ) pode ser usada em vez da vinculação tardia (também chamada de despacho dinâmico ), que requer uma ou mais pesquisas de tabela de método virtual dependendo se herança múltipla ou apenas herança única é suportada na linguagem de programação que está sendo usada.

Métodos não substituíveis

Assim como as classes podem não ser subclassíveis, as declarações de método podem conter modificadores de método que evitam que o método seja sobrescrito (isto é, substituído por uma nova função com o mesmo nome e assinatura de tipo em uma subclasse). Um método privado não pode ser substituído simplesmente porque não é acessível por classes diferentes da classe da qual ele é uma função de membro (embora isso não seja verdade para C ++). Um finalmétodo em Java, um sealedmétodo em C # ou um frozenrecurso em Eiffel não pode ser substituído.

Métodos virtuais

Se o método da superclasse for um método virtual , as invocações do método da superclasse serão despachadas dinamicamente . Algumas linguagens exigem que os métodos sejam especificamente declarados como virtuais (por exemplo, C ++) e, em outras, todos os métodos são virtuais (por exemplo, Java). Uma invocação de um método não virtual sempre será despachada estaticamente (isto é, o endereço da chamada de função é determinado em tempo de compilação). O despacho estático é mais rápido do que o despacho dinâmico e permite otimizações como a expansão em linha .

Visibilidade de membros herdados

A tabela a seguir mostra quais variáveis ​​e funções são herdadas dependendo da visibilidade fornecida ao derivar a classe.

Visibilidade da classe base Visibilidade de classe derivada
Derivação pública Derivação privada Derivação protegida
  • Privado →
  • Protegido →
  • Público →
  • Não herdado
  • Protegido
  • Público
  • Não herdado
  • Privado
  • Privado
  • Não herdado
  • Protegido
  • Protegido

Formulários

A herança é usada para co-relacionar duas ou mais classes entre si.

Substituindo

Ilustração de substituição de método

Muitas linguagens de programação orientadas a objetos permitem que uma classe ou objeto substitua a implementação de um aspecto - normalmente um comportamento - que ele herdou. Esse processo é chamado de substituição . A substituição introduz uma complicação: qual versão do comportamento uma instância da classe herdada usa - aquela que faz parte de sua própria classe ou aquela da classe pai (base)? A resposta varia entre as linguagens de programação e algumas linguagens fornecem a capacidade de indicar que um determinado comportamento não deve ser substituído e deve se comportar conforme definido pela classe base. Por exemplo, em C #, o método ou propriedade base só pode ser substituído em uma subclasse se estiver marcado com o modificador virtual, abstrato ou de substituição, enquanto em linguagens de programação como Java, diferentes métodos podem ser chamados para substituir outros métodos. Uma alternativa para substituir é ocultar o código herdado.

Reutilização de código

Herança de implementação é o mecanismo pelo qual uma subclasse reutiliza o código em uma classe base. Por padrão, a subclasse retém todas as operações da classe base, mas a subclasse pode sobrescrever algumas ou todas as operações, substituindo a implementação da classe base pela sua própria.

No exemplo Python a seguir, as subclasses SquareSumComputer e CubeSumComputer substituem o método transform () da classe base SumComputer . A classe base compreende operações para calcular a soma dos quadrados entre dois inteiros. A subclasse reutiliza todas as funcionalidades da classe base, com exceção da operação que transforma um número em seu quadrado, substituindo-o por uma operação que transforma um número em seu quadrado e cubo, respectivamente. As subclasses, portanto, calculam a soma dos quadrados / cubos entre dois inteiros.

Abaixo está um exemplo de Python.

class SumComputer:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def transform(self, x):
        raise NotImplementedError

    def inputs(self):
        return range(self.a, self.b)

    def compute(self):
        return sum(self.transform(value) for value in self.inputs())

class SquareSumComputer(SumComputer):
    def transform(self, x):
        return x * x

class CubeSumComputer(SumComputer):
    def transform(self, x):
        return x * x * x

Na maioria dos trimestres, a herança de classes com o único propósito de reutilização de código caiu em desuso. A principal preocupação é que a herança de implementação não oferece nenhuma garantia de substituibilidade polimórfica - uma instância da classe de reutilização não pode necessariamente ser substituída por uma instância da classe herdada. Uma técnica alternativa, delegação explícita , requer mais esforço de programação, mas evita o problema de substituibilidade. Em C ++, a herança privada pode ser usada como uma forma de herança de implementação sem possibilidade de substituição. Enquanto a herança pública representa um relacionamento "é um" e a delegação representa um relacionamento "tem um", a herança privada (e protegida) pode ser considerada como um relacionamento "é implementado em termos de".

Outro uso frequente de herança é garantir que as classes mantenham uma determinada interface comum; ou seja, eles implementam os mesmos métodos. A classe pai pode ser uma combinação de operações implementadas e operações que devem ser implementadas nas classes filhas. Freqüentemente, não há mudança de interface entre o supertipo e o subtipo - o filho implementa o comportamento descrito em vez de sua classe pai.

Herança vs subtipagem

A herança é semelhante, mas distinta da subtipagem. A subtipagem permite que um determinado tipo seja substituído por outro tipo ou abstração, e é dito que estabelece uma relação é-um entre o subtipo e alguma abstração existente, implícita ou explicitamente, dependendo do suporte de linguagem. O relacionamento pode ser expresso explicitamente por meio de herança em linguagens que suportam herança como um mecanismo de subtipagem. Por exemplo, o código C ++ a seguir estabelece uma relação de herança explícita entre as classes B e A , onde B é uma subclasse e um subtipo de A , e pode ser usado como A sempre que um B for especificado (por meio de uma referência, um ponteiro ou o próprio objeto).

class A {
 public:
  void DoSomethingALike() const {}
};

class B : public A {
 public:
  void DoSomethingBLike() const {}
};

void UseAnA(const A& a) {
  a.DoSomethingALike();
}

void SomeFunc() {
  B b;
  UseAnA(b);  // b can be substituted for an A.
}

Em linguagens de programação que não suportam herança como mecanismo de subtipagem , o relacionamento entre uma classe base e uma classe derivada é apenas um relacionamento entre implementações (um mecanismo para reutilização de código), em comparação com um relacionamento entre tipos . A herança, mesmo em linguagens de programação que suportam herança como um mecanismo de subtipagem, não acarreta necessariamente a subtipagem comportamental . É inteiramente possível derivar uma classe cujo objeto se comportará incorretamente quando usado em um contexto onde a classe pai é esperada; veja o princípio de substituição de Liskov . (Compare conotação / denotação .) Em algumas linguagens OOP, as noções de reutilização e subtipagem de código coincidem porque a única maneira de declarar um subtipo é definir uma nova classe que herda a implementação de outra.

Restrições de design

O uso extensivo da herança no projeto de um programa impõe certas restrições.

Por exemplo, considere uma classe Person que contém o nome de uma pessoa, data de nascimento, endereço e número de telefone. Podemos definir uma subclasse de Pessoa chamada Aluno, que contém a média de notas da pessoa e as aulas realizadas, e outra subclasse de Pessoa, chamada Funcionário, que contém o cargo, o empregador e o salário da pessoa.

Ao definir essa hierarquia de herança, já definimos certas restrições, nem todas desejáveis:

Solteiro
Usando herança única, uma subclasse pode herdar de apenas uma superclasse. Continuando o exemplo dado acima, Pessoa pode ser um Estudante ou um Funcionário , mas não ambos. O uso de herança múltipla resolve parcialmente esse problema, pois é possível definir uma classe StudentEmployee que herda tanto de Student quanto de Employee . No entanto, na maioria das implementações, ele ainda pode herdar de cada superclasse apenas uma vez e, portanto, não oferece suporte a casos em que um aluno tem dois empregos ou frequenta duas instituições. O modelo de herança disponível em Eiffel torna isso possível por meio do suporte para herança repetida .
Estático
A hierarquia de herança de um objeto é fixada na instanciação quando o tipo do objeto é selecionado e não muda com o tempo. Por exemplo, o gráfico de herança não permite que um objeto Student se torne um objeto Employee enquanto retém o estado de sua superclasse Person . (Esse tipo de comportamento, no entanto, pode ser obtido com o padrão decorator .) Alguns criticaram a herança, argumentando que ela prende os desenvolvedores aos seus padrões de design originais.
Visibilidade
Sempre que o código do cliente tem acesso a um objeto, geralmente ele tem acesso a todos os dados da superclasse do objeto. Mesmo que a superclasse não tenha sido declarada pública, o cliente ainda pode converter o objeto para seu tipo de superclasse. Por exemplo, não há como fornecer a uma função um indicador para a média e transcrição de notas de um Aluno sem também dar a essa função acesso a todos os dados pessoais armazenados na superclasse Person do aluno . Muitas linguagens modernas, incluindo C ++ e Java, fornecem um modificador de acesso "protegido" que permite que subclasses acessem os dados, sem permitir que qualquer código fora da cadeia de herança os acesse.

O princípio da reutilização composta é uma alternativa à herança. Essa técnica oferece suporte ao polimorfismo e à reutilização de código, separando os comportamentos da hierarquia de classes primárias e incluindo classes de comportamento específicas, conforme exigido em qualquer classe de domínio de negócios. Essa abordagem evita a natureza estática de uma hierarquia de classes, permitindo modificações de comportamento em tempo de execução e permite que uma classe implemente comportamentos no estilo buffet, em vez de ficar restrita aos comportamentos de suas classes ancestrais.

Problemas e alternativas

A herança de implementação é controversa entre os programadores e teóricos da programação orientada a objetos desde pelo menos os anos 1990. Entre eles estão os autores de Design Patterns , que defendem a herança da interface e favorecem a composição em vez da herança. Por exemplo, o padrão decorator (como mencionado acima ) foi proposto para superar a natureza estática da herança entre as classes. Como uma solução mais fundamental para o mesmo problema, a programação orientada a papéis apresenta um relacionamento distinto, representado por , combinando propriedades de herança e composição em um novo conceito.

De acordo com Allen Holub , o principal problema com a herança de implementação é que ela introduz um acoplamento desnecessário na forma do "problema da classe base frágil" : modificações na implementação da classe base podem causar mudanças comportamentais inadvertidas nas subclasses. O uso de interfaces evita esse problema porque nenhuma implementação é compartilhada, apenas a API. Outra maneira de afirmar isso é que "a herança quebra o encapsulamento ". O problema surge claramente em sistemas orientados a objetos abertos, como frameworks , onde se espera que o código do cliente herde de classes fornecidas pelo sistema e, em seguida, substitua as classes do sistema em seus algoritmos.

Alegadamente, o inventor de Java James Gosling falou contra a herança de implementação, afirmando que ele não a incluiria se redesenhasse o Java. Projetos de linguagem que desacoplam herança de subtipagem (herança de interface) apareceram já em 1990; um exemplo moderno disso é a linguagem de programação Go .

Herança complexa, ou herança usada em um design insuficientemente maduro, pode levar ao problema de ioiô . Quando a herança foi usada como uma abordagem primária para estruturar o código em um sistema no final dos anos 1990, os desenvolvedores naturalmente começaram a quebrar o código em várias camadas de herança conforme a funcionalidade do sistema crescia. Se uma equipe de desenvolvimento combinasse várias camadas de herança com o princípio de responsabilidade única, ela criaria muitas camadas superfinas de código, muitas das quais teriam apenas 1 ou 2 linhas de código em cada camada. Muitas camadas tornam a depuração um desafio significativo, pois se torna difícil determinar qual camada precisa ser depurada.

Outro problema com a herança é que as subclasses devem ser definidas no código, o que significa que os usuários do programa não podem adicionar novas subclasses em tempo de execução. Outros padrões de design (como Entidade-componente-sistema ) permitem que os usuários do programa definam variações de uma entidade em tempo de execução.

Veja também

Notas

Referências

Leitura adicional