| FAQ Lite | ||||||||||||||||||
| Herança Correta e Substituibilidade | ||||||||||||||||||
|
||||||||||||||||||
| [ 21.1 ] Devo ocultar, em minha classe derivada, as funções membro que são públicas em minha classe base? |
| Nunca, nuca, nunca faça isso. Nunca. Nunca! A tentativa de ocultar (eliminar, cancelar, privatizar) funções membro públicas herdadas é um erro de projeto muito freqüente, e decorre quase sempre de uma confusão mental a respeito do mecanismo de herança. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
| [ 21.2 ] Derived* -> Base* funciona bem. Porque Derived** -> Base** não funciona? |
| C++ permite que um Derived* seja convertido para
um Base*, desde que o objeto
Derived seja um tipo-de objeto Base.
Entretanto, a tentativa de converter um Derived** para um Base** é
interpretado pelo C++ como um erro. Embora a causa desse erro possa não ser óbvia, é bom que uma conversão deste tipo não seja permitida. Por exemplo, se você pudesse converter um Car** para um Vehicle**, e se você pudesse, da mesma forma, converter um NuclearSubmarine** para um Vehicle**, você poderia fazer operações de atribuição para esses dois pointers e, finalmente, acabar fazendo um Car* apontar para um NuclearSubmarine. |
class Vehicle { /*...*/ };
class Car : public Vehicle { /*...*/ };
class NuclearSubmarine : public Vehicle { /*...*/ };
main()
{
Car car;
Car* carPtr = &car;
Car** carPtrPtr = &carPtr;
Vehicle** vehiclePtrPtr = carPtrPtr; // This is an error in C++
NuclearSubmarine sub;
NuclearSubmarine* subPtr = ⊂
*vehiclePtrPtr = subPtr;
// This last line would have caused carPtr to point to sub !
}
| Em outras palavras, se fosse admissível converter um Derived** para
um Base**, o Base** poderia ser desreferenciado, permitindo a alteração de Base*, e o Base* poderia
ser levado a apontar um objeto de uma outra classe derivada, o que poderia causar sérios
problemas. Quem pode saber o que aconteceria se você invocasse a função membro openGasCap() do que você pensa ser um
Car, mas na realidade fosse um NuclearSubmarine ? Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
| [ 21.3 ] Uma vaga-de-estacionamento-de-Car é um tipo-de vaga-de-estacionamento-de-Vehicle ? |
| Não. Parece estranho mas é verdade. Você pode analisar esse caso como uma conseqüência direta do FAQ anterior, ou raciocinar da seguinte maneira: Se o relacionamento tipo-de fosse válido, então alguém poderia fazer o pointer vaga-de-estacionamento-de-Vehicle apontar para vaga-de-estacionamento-de-Car. Mas vaga-de-estacionamento-de-Vehicle tem um função membro AddNewVehicleToParkingLot(Vehicle&) que pode adicionar qualquer objeto Vehicle à vaga de estacionamento. Isso permitiria que você estacionasse um NuclearSubmarine em uma vaga-de-estacionamento-de-Car. Seria suprendente dirigir-se a uma vaga de estacionamento para retirar o que se presume ser um Car, e encontrar ali um NuclearSubmarine.Uma outra maneira de explicar esse mesmo fato: o container de uma Coisa não é um tipo-de container de QualquerCoisa, mesmo que uma Coisa seja um tipo-de QualquerCoisa. Difícil de engolir, é verdade. Você não precisa gostar dessa regra. Só precisa aceitá-la. Um último exemplo utilizado nos cursos de OO/C++ de Mr. Marshall Cline : Um Saco-de-Maçãs não é um tipo-de Saco-de-Frutas. Se um Saco-de-Maçãs pudesse ser passado como Saco-de-Frutas, alguém poderia por uma Banana no Saco mas todos continuariam supondo que o Saco contém exclusivamente Maçãs.Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
| [ 21.4 ] Uma matriz de Derived é um tipo-de matriz de Base ? |
| Não. Esse é um corolário da FAQ anterior. Considere o seguinte: |
class Base {
public:
virtual void f(); // 1
};
class Derived : public Base {
public:
// ...
private:
int i_; // 2
};
void userCode(Base* arrayOfBase)
{
arrayOfBase[1].f(); // 3
}
main()
{
Derived arrayOfDerived[10]; // 4
userCode(arrayOfDerived); // 5
}
| O compilador trata isso como sendo perfeitamente seguro. A
linha //5 converte um Derived*
para um Base*. Na realidade isso é um desastre: como Derived é
maior que Base, a aritmética de pointer realizada na linha //3 está
incorreta: o compilador usa
sizeof(Base) quando calcula o endereço de arrayOfBase[1], ainda que a matriz seja uma matriz de Derived, o que significa que
o endereço calculado na linha
//3 - e a subsequente invocação da função membro f() - não
aponta para o início de nenhum objeto, endereça o meio do objeto Derived. Assumindo que o seu compilador usa a abordagem comum para funções virtual, isso vai levar a uma reinterpretação do int i; da primeira Derived como se apontasse para uma virtual-table. O compilador vai seguir esse pointer, o que a essa altura significa que estamos indo para uma localização de memória qualquer, tomar uma palavra de memória naquela localização e interpretá-la como se fosse o endereço de uma função membro. Vai então carregar o que se presume ser o endereço de uma função no apontador de instruções e começar a processar instruções de máquina daquele endereço de memória.As chances de queda do sistema nesse caso são altíssimas. A raiz desse problema está no fato que o C++ não consegue distinguir entre um pointer-para-uma-coisa e um pointer-para-uma-matriz-de-coisas. Naturalmente C++ "herdou" essa característica do C. Nota: Se tivéssemos usado uma classe array-like (como vector<Derived> da STL) ao invés de uma matriz simples, esse problema teria sido pego apropriadamente como um erro de compilação, e não como um desastre em tempo de execução. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
| [ 21.5 ] Se matriz-de-Derived não é um tipo-de matriz-de-Base, significa que matrizes são ruins? |
| Sim. Matrizes são demoníacas. Falando seriamente, matrizes são altamente vinculadas a pointers, e pointers são notoriamente difíceis de se usar. Mas se você tem uma compreensão plena de porque os FAQs acima representam problemas do ponto de vista de projetos, ou seja, se você realmente sabe porque um container de Coisa não é um tipo-de container de QualquerCoisa, e se você julga que todos os que vão estar envolvidos na manutenção de seu código tem esse mesmo nível de compreensão, então sinta-se a vontade para usar matrizes.Mas se você é como a maioria das pessoas, você deve usar uma template container class como vector<T> da STL, ao invés de matrizes simples.Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
| [ 21.6 ] Um Circle é um tipo-de Ellipse ? |
| Não, se Ellipse permite que se altere o seu
tamanho assimetricamente. Por exemplo, suponha que Ellipse tem uma função membro Set(Size(x,y), e suponha que essa função membro esteja definida para fazer a largura da elipse igual a x, e a altura igual a y. Nesse caso, Circle não pode ser um tipo-de Ellipse.Colocando de forma simples: Ellipse pode ter qualquer relação entre largura e altura, Circle não pode.Isso deixa dois relacionamentos potenciais (válidos) entre Circle e Ellipse:
No primeiro caso, Ellipse poderia ser derivada da classe AsymmetricShape, e SetSize(x,y) poderia ser introduzido em AsymmetricShape. Circle, por sua vez, poderia ser derivado da classe SymmetricShape que tem uma função membro setSize(size).No segundo caso, a classe Oval poderia ter apenas setSize(size) que determina tanto width() quanto height() para size.Ellipse e Circle poderiam ambas derivar de Oval. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) Nota: setSize(x,y) não é uma forma sagrada. Dependendo de seus objetivos, é admissível não permitir aos usuários da classe que alterem as dimensões de uma Ellipse, e nesse caso é uma opção de projeto plenamente válida não ter um método setSize(x,y) em Ellipse. Entretanto o que se discute neste FAQ é o que fazer quando se quer derivar uma classe a partir de uma classe base pré-existente que possui um método inaceitável pela classe derivada. É claro que o ideal seria descobrir esse conflito quando a classe base ainda não tivese sido implementada. Mas a vida nem sempre é o ideal... |
| [ 21.7 ] Há outras opções para o dilema "Circle é/não-é um tipo-de Ellipse"? |
| Se você reivindica que todas as Ellipses possam
ser assimétricas, e você reinvindica que Circle seja um tipo-de Ellipse, e você ainda
reivindica que Circle não
possa ser assimétrico, é claro que você terá rever e ajustar algumas de suas
reivindicações. Ou você abre mão de Ellipse::setSize(x,y), ou abre mão do
relacionamento de herança entre
Circle e Ellipse, ou aceita que seu Circle não
seja necessariamente circular. Aqui estão duas das
armadilhas que freqüentemente pegam programadores iniciantes em OO/C++. Eles tentam
enxertar o código com salvaguardas para cobrir erros de projeto (eles redefinem Circle::setSize(x,y) para ativar uma exceção, chamar abort(), escolher a média
entre os dois parâmetros, ou para ser uma operação nula), Infelizmente todos esse
enxertos vão suprender os usuários da classe, já que os usuários esperam width() == x e Se for absolutamente importante manter o relacionamento por herança "Circle é um tipo-de Ellipse", você pode modificar o compromisso - a definição da função é um compromisso entre você e os usuários da sua classe - de setSize(x,y), função membro de Ellipse. Por exemplo, você poderia estabelecer o seguinte compromisso: Essa função membro pode fazer width() igual a x, e/ou fazer height() igual a y, ou ainda não fazer nada. Infelizmente isso diluiria o compromisso e os usuários da classe não poderiam saber que comportamento esperar da função membro. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) Nota: setSize(x,y) não é uma forma sagrada. Dependendo de seus objetivos, é admissível não permitir aos usuários da classe que alterem as dimensões de uma Ellipse, e nesse caso é uma opção de projeto plenamente válida não ter um método setSize(x,y) em Ellipse. Entretanto o que se discute neste FAQ é o que fazer quando se quer derivar uma classe a partir de uma classe base pré-existente que possui um método inaceitável pela classe derivada. É claro que o ideal seria descobrir esse conflito quando a classe base ainda não tivesse sido implementada. Mas a vida nem sempre é o ideal... |
| [ 21.8 ] Mas eu tenho Ph.D. em Matemática, e eu tenho certeza que um círculo é um tipo-de elipse! Isso significa que Mr Maschall Cline é um estúpido? Ou que o C++ é um estúpido? Ou que OO é uma estupidez? |
| Realmente não significa nada disso. A triste realidade é
que a sua intuição está errada. Veja, eu tenho recebido e respondido dúzias de e-mails veementes sobre esse assunto. Tenho falado sobre esse tema centenas de vezes para milhares de desenvolvedores de software profissionais em todos os lugares. Eu sei que isso vai contra a sua intuição. Mas, acredite-me, sua intuição está errada. O problema real é que sua noção intuitiva de tipo-de não coincide com a noção de OO do que seja herança correta, apropriada, tecnicamente denominada subtipificação. O ponto básico é que objetos da classe derivada devem ser substituíveis por objetos da classe base. No caso Circle/Ellipse, a função membro setSize(x,y) viola essa substituibilidade. Você tem três opções:
Sinto muito, mas não há outras opções. Note que algumas pessoas mencionam a opção de derivar Circle e Ellipse de uma terceira classe base. comum às duas, mas isso é de fato apenas uma variação da opção (3) mencionada acima, porque assim deixa de haver um relacionamento de herança entre Circle e Ellipse, embora ambas sejam derivadas de uma mesma classe base. Uma outra maneira de encarar esse dilema é você enfraquecer a classe base (nesse caso descaracterizando Ellipse ao ponto de que não se possa determinar altura diferente de largura), ou fortalecer a classe derivada (nesse caso incrementando Circle com a habilidade de ser tanto simétrico quando assimétrico (sic)). Quando nada disso é satisfatório (como no caso Circle/Elipse), o que se faz e simplesmente eliminar o relacionamento por herança. Se o relacionamento por herança simplesmente precisa existir, você então tem que remover as funções membro modificadoras - setHeight(y), setWidth(x) e setSize(x,y) - da classe base. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) Nota: setSize(x,y) não é uma forma sagrada. Dependendo de seus objetivos, é admissível não permitir aos usuários da classe que alterem as dimensões de uma Ellipse, e nesse caso é uma opção de projeto plenamente válida não ter um método setSize(x,y) em Ellipse. Entretanto o que se discute neste FAQ é o que fazer quando se quer derivar uma classe a partir de uma classe base pré-existente que possui um método inaceitável pela classe derivada. É claro que o ideal seria descobrir esse conflito quando a classe base ainda não tivesse sido implementada. Mas a vida nem sempre é o ideal... |
| [ 21.9 ] Mas meu problema não tem nada a ver com círculos e elipses, então de que me serve esse exemplo tolo? |
| Aí é que você se engana. Você pensa
que o exemplo de Circle/Ellipse é apenas um exemplo tolo. Mas na realidade, seu problema é um
isomorfismo desse exemplo. Não importa qual seja especificamente seu problema com herança. O fato é que todas (sim, todas) as más heranças recaem no caso exemplificado Circle-não-é-um-tipo-de-Ellipse. Aqui está o por quê; Más heranças sempre tem uma classe base com uma capacitação extra - geralmente uma ou duas funções membro, algumas vezes uma promessa (definição) extra feita por uma função ou por uma combinação de funções membro - que não satisfaz as necessidades da classe derivada. Você tanto pode enfraquecer a classe base, quanto fortalecer a classe derivada, ou ainda eliminar o relacionamento por herança entre as classes em questão. Eu tenho visto muitos e muitos e muitos desses casos de má herança, e acredite-me, todos recaem no caso exemplificado por Circle/Ellipse. Portanto, se você verdadeiramente compreender o exemplo Circle/Ellipse, você estará apto a reconhecer má herança em qualquer outro caso. Se você não compreende o que acontece no problema Circle/Ellipse, então são muito altas as chances de que você cometa erros de herança, que ao final de seu projeto poderão demandar muito tempo para serem corrigidos. É triste mas é verdade. Nota: Este FAQ trata de herança de membros public. Herança de membros private ou protected tem outras especificidades) |
|
| | Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site | | |