domingo, 25 de setembro de 2011

Aventuras com Heranças

Embora a herança em si seja um tema básico da orientação a objetos, ela possui uma série de artimanhas e regras (principalmente para quem programa em C++) que devem ser seguidas para não cair em futuras armadilhas.
Basicamente, existem três tipos de heranças em C++: a pública, a protegida e a privada. O primeiro tipo é o mais conhecido. Os outros dois não são muito utilizados e raramente são ensinados na faculdade ou nos cursos de C++. Além disso, a herança pode ser virtual ou não.
Vou explicar cada um destes tipos na sequência:

Herança pública:

De longe a mais conhecida e mais utilizada (embora muitas vezes utilizada de forma incorreta). A regra mais importante para esta herança é: quando você diz que B herda de A, você está dizendo que B é um A. Tenha isso em mente. Sempre. Só herde uma classe de outra se você puder dizer, no bom português, que a classe filha é um tipo da classe mãe. Nunca herde com o objetivo de economizar código ou coisa do tipo.
Por exemplo, você pode herdar Carro de Veiculo, Moto de Veiculo, Ferrari de Carro, Marte de Planeta, Carrefour de Supermercado, mas nunca deve herdar Moto de Carro, por exemplo. Mesmo que no seu código seja tentador fazer isso, pois a maioria das funções de Moto estão implementadas em Carro, não faça tal herança. O ideal é criar uma classe acima de Carro e Moto (como Veiculo) e colocar lá as funções em comum.
Vou citar um exemplo de um código que eu trabalhei há algum tempo que fazia o uso incorreto de herança pública.
Tínhamos uma classe chamada Thread, com um método virtual puro chamado Run, um Inicia e um SetDelay. Basicamente, se você queria ter uma thread no sistema, bastava herdar desta classe, implementar o método Run, e chamar o Inicia para que o conteúdo de Run ficasse sendo executado de acordo com o delay definido.
Como o código implementava uma comunicação por Gprs e esta comunicação deveria ficar sendo executada em uma thread separada no sistema, fizemos Gprs herdar de Thread. O problema é que Gprs não é uma Thread! Gprs pode ser considerado uma Rede, um Canal de Comunicação, mas definitivamente não é uma thread!
Apesar de parecer inofensiva, a decisão de herdar Gprs de Thread começou a causar dores de cabeça. O problema é que no código você acaba tendo algo do tipo:

Gprs* objGprs = new Gprs();
objGprs->SetDelay( 1000 );
objGprs->Inicia();

Agora, para uma pessoa que nunca mexeu no código, o que o Inicia faz? A pessoa pode imaginar que vai configurar IP, zerar as variáveis da classe, ligar o hardware do modem, o que for, mas não vai imaginar que este comando está iniciando a Thread.
Além disso, tínhamos a necessidade de ter uma função que iniciasse o modem e configurasse a rede, mas esta função não poderia se chamar Inicia porque o Inicia já está implementado em Thread.
Assim, o código acabou ficando uma beleza do tipo:

Gprs* objGprs = new Gprs();
objGprs->IniciaGprs( IP, PORTA );
objGprs->SetDelay( 1000 );
objGprs->Inicia();

O que beira a estupidez.

O correto, neste caso, seria ter criado duas classes: uma chamada ThreadGprs que herda de Thread, e outra chamada Gprs que não herda de ninguém (ou herda de CanalComunicacao, ou algo do tipo), e fazer com que ThreadGprs agregasse Gprs. Um código do tipo

ThreadGprs* objThreadGprs = new ThreadGprs();
objThreadGprs->SetDelay( 1000 );
objThreadGprs->Inicia();

Faz sentido pra qualquer novato que bata o olho nele.

Bom, dito isso, vamos ao que interessa: o que é uma herança pública?

O funcionamento é simples: a classe filha vai conseguir acessar os atributos e métodos públicos e protegidos da mãe, e as outras classes vão conseguir acessar os atributos e métodos públicos da filha e da mãe através de um objeto da filha.

Veja o exemplo:

class Base
{
public:
    void FuncaoUm()
    {
        printf( "Funcao numero 1\n" );
    }

    virtual void FuncaoDois()
    {
        printf( "Funcao numero 2 da base\n" );
    }
};

class Derivada: public Base
{
public:
    void FuncaoDois()
    {
        printf( "Funcao numero 2 da derivada!\n" );
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoUm();
    obj->FuncaoDois();

    delete obj;
    return 0;
}

O resultado disso vai ser:

Funcao numero 1
Funcao numero 2 da derivada!

(Se você não entende o que a palavra virtual faz, experimente rodar o código novamente sem ela e verá que o comportamento será diferente do esperado. Via de regra, sempre que você pretende reescrever uma função na derivada, declare a função da Base como virtual).

Função virtual pura

Se você quer obrigar a Derivada a reimplementar uma função, você pode declará-la como virtual pura. Esta função pode ou não ser implementada na Base, mas a Derivada deve obrigatoriamente implementá-la.
Uma função virtual pura é declarada da seguinte forma:

virtual void Funcao() = 0;

Veja o exemplo:

class Base
{
public:    
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf( "Funcao Pura Dois na base\n" );
    }
};

class Derivada: public Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );

        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

O resultado vai ser:

Funcao Pura Um implementada na derivada
Funcao Pura Dois implementada na derivada
Funcao Pura Dois na base

Tranquilo, né? Se você seguir corretamente a regra que eu comentei no começo, vai tirar um grande benefício com heranças, e dificilmente vai se meter em problemas.

Vamos para os tipos menos conhecidos:


Herança privada:

Um tipo de herança menos conhecido e que deve ser evitado. Diferente da pública, na herança privada, quando você diz que B herda de A, está dizendo que B tem A. Por exemplo, Carro poderia fazer uma herança privada de Motor.

A principal diferença entre esta herança e a pública, do ponto de vista técnico, é que quem possui um objeto da classe Derivada não irá conseguir chamar, por este objeto, uma função da classe Base.

Voltemos ao primeiro exemplo:

class Base
{
public:
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf("Funcao Pura Dois na base\n");
    }
};

class Derivada: private Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );
        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    //Base* obj = new Derivada();
    Derivada* obj = new Derivada();

    //obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

Note as diferenças:

  1. A herança agora é private, e não mais public;
  2. Fazer um ponteiro do tipo Base apontar para um objeto alocado como o tipo Derivada já não funciona mais. (É possível utilizar um reinterpret_cast aqui para passar esta segurança, porém você já está hackeando o código e vai acabar com um comportamento inesperado mais cedo ou mais tarde);
  3. Não é mais possível chamar as funções que estão na Base através do objeto da Derivada. Assim, a linha obj->FuncaoPuraUm(); teve que ser comentada.

O mesmo vale caso você herde (independente da forma da herança) uma classe de Derivada (uma classe chamada DerivadaDaDerivada, vamos supor). DerivadaDaDerivada não conseguirá acessar nenhum método ou atributo de Base diretamente, independente se este método ou atributo é público, protegido ou privado.

Mas por que usar herança privada, e não composição ou agregação?

Difícil responder, até porque você deve utilizar composição ou agregação em vez de herança privada.
Existem duas razões para você optar por herança privada:

  1. Caso você precise que a sua classe derivada acesse os métodos e atributos protegidos da base. Isso não seria possível caso fosse utilizado composição ou agregação, a menos que você declarasse a classe agregada como friend da classe agregadora.
  2. Caso você queira declarar na base uma função virtual (tanto faz se é pura ou não) e queira que ela seja reimplementada na derivada.
Sinceramente não consigo pensar em nenhum exemplo prático bom para se utilizar este tipo de herança.
Uma situação, talvez, fosse no exemplo do Gprs que eu comentei acima. Fazer Gprs herdar de forma privada de Thread seria melhor do que fazer a herança pública, porque, desta forma, quem chamasse:

objGprs->Inicia();

Estaria, com certeza, chamando a Inicia do Gprs, e não da Thread. A diferença é que daí a responsabilidade de inicializar a thread, definir timeouts e tudo mais ficaria a cargo da classe Gprs.


Herança Protegida

Dos três tipos de herança, este é o pior. Ninguém sabe ao certo o que significa dizer que B faz uma herança protegida de A, mas está perto de ser um relacionamento do tipo B tem A, como na herança privada.

O funcionamento deste tipo de herança é bem semelhante ao da (herança) privada. Os métodos da Base continuam inacessíveis por quem tenta acessá-los por um objeto da Derivada. A diferença está em se você herdar de Derivada (o exemplo da DerivadaDaDerivada). Neste caso, a DerivadaDaDerivada conseguirá acessar os métodos e atributos públicos e protegidos da Base sem problemas.

Herança Virtual

Os três tipos citados acima podem ser virtuais ou não (embora para a herança privada eu não veja sentido).
As heranças virtuais são úteis para o caso das heranças do tipo diamante (quem programa em Java treme ao ouvir isso).
Uma herança do tipo diamante se forma quando você tem uma Base, duas (ou mais) derivadas e depois uma classe que faz herança múltipla dessas duas (ou mais) derivadas. Pense o seguinte:

class Base
{
protected:
    int x;
};

class DerivadaUm: public Base
{
};

class DerivadaDois: public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso vai dar um erro de compilação, dizendo que o acesso a “x” é ambíguo. Isso acontece porque Join enxerga dois x: um que veio de Base por meio da DerivadaUm e outro que veio de Base por DerivadaDois.

Para que este código funcione, é necessário que DerivadaUm e DerivadaDois façam herança virtual de Base.
Assim, o código fica da seguinte forma:

class Base
{
protected:
    int x;
};

class DerivadaUm: virtual public Base
{
};

class DerivadaDois: virtual public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso criará apenas uma instância de x para Join.

Bom, dizem que heranças do tipo virtual devem ser evitadas. O problema não está com as heranças virtuais, mas sim com a herança do tipo diamante. Se você chegar a um modelo que chegue a este tipo de configuração, evite-o. Modele de forma diferente. Mas, inevitavelmente, você vai acabar tendo que dar manutenção em um código que está mal modelado, e talvez esta seja a sua única saída...

Nenhum comentário:

Postar um comentário