Introdução
Boas vindas! Hoje pretendo escrever um pouco sobre polimorfismo, um assunto que não era pra ser tão complicado, mas que começou a parecer algo nebuloso pela popularidade de linguagens que acabam fazendo algumas coisas "por debaixo dos panos", de modo implícito, as vezes dificultando o entendimento de alguns conceitos.
Aprendizado X Eficiência
É inegável que no processo de construção de software, usar uma linguagem como Python pode facilitar bastante o desenvolvimento, fazendo você ganhar bastante tempo e tirando barreiras desnecessárias na hora de criar um projeto real. Entretanto, existem alguns conceitos fundamentais de entender sobre programação, que acabam sendo "escondidos" por linguagens como Python para ganhar produtividade e conseguir ter um código fonte mais fácil de ler. Por esse motivo, muitas vezes para o aprendizado pode ser um divisor de águas a escolha de uma linguagem que "esconde menos detalhes", uma vez que isso te deixa mais "a par" do que está acontecendo, sem muita abstração.
O que é Polimorfismo
De forma resumida, o Polimorfismo é um princípio que permite invocar métodos de subclasses através de uma classe mãe (superclasse).
Explicação
Para entender o polimorfismo de verdade, é necessário conhecer alguns outros conceitos antes, como o conceito de herança, e o conceito de ponteiros, além de outros conceitos que vou apresentar aqui, como o fatiamento de objeto.
Para começar, observe o código abaixo escrito em C++
Observação: Não se prenda tanto à sintaxe de C++ caso você não esteja habituado, no momento que estou escrevendo esse material eu também não estou completamente habituado com a linguagem, entretanto, achei ela a ideal para ilustrar o conceito de Polimorfismo. Ainda que você não programe em C++, você deve ser capaz de entender o suficiente o que está acontecendo para acompanhar a explicação.
#include <iostream>
class Base
{
public:
void method()
{
std::cout << "I am a Base method.";
}
};
class Derivative: public Base
{
public:
void method()
{
std::cout << "I am a Derivative method.";
}
};
int main()
{
Base *baseObject = new Base();
baseObject->method();
return 0;
};
Uma breve explicação do que está acontecendo aqui. Nós iniciamos o código com #include <iostream>
, isso inclui ao nosso ambiente bibliotecas com objetos padrão de Entrada e Saída (Input/Output ou I/O, por isso o "io"). Fizemos isso para termos disponível o cout
(C Output).
Se ficou interessado(a) você pode encontrar mais sobre a biblioteca
iostream
aqui. Ou então mais sobre ocout
aqui
Depois disso criamos duas classes, uma classe Base
e uma classe Derivative
(derivada) que deriva da classe Base
(ou herda, se você preferir dizer assim).
Ambas as classes tem um mesmo método público chamado method()
que diz de qual classe que o método pertence.
Do Python ao C++
Se você está tendo dificuldades para acompanhar a explicação por conta da sintaxe do C++, e você está habituado(a) com Python, eu vou tentar esclarecer um pouco as coisas.
class Derivative: public Base
seria o equivalente declass Derivative(Base)
em Python.std::cout<<"Hello World"
seria o equivalente deprint("Hello World")
em Python- O
void
na frente da declaração dos métodos significa que a função não retorna nada. Em C++ precisamos definir na declaração da função o tipo de dado que ela vai retornar, e void caso ela não retorne nada (amain()
do nosso exemplo, retorna 0, por isso declaramos ela com int).
Agora na nossa função main()
, nós criamos um ponteiro do tipo Base
que aponta para uma região de memória onde nós alocamos e ocupamos com um objeto da classe Base
.
Quando compilamos e executamos o código, temos o seguinte resultado:
I am a Base method.
Até então tudo faz sentido, temos um objeto de Base
chamando o método da classe Base
, tudo certo!
Se trocarmos a classe para Derivative
, o resultado deve ser diferente.
int main()
{
Derivative *derivativeObject = new Derivative();
derivativeObject->method();
return 0;
};
Observação: Eu estou mostrando apenas o recorte do código referente a função
main()
, para não poluir a visualização, se em algum momento eu alterar alguma parte do restante do código, vou exibir o que foi alterado, como aqui eu só mexi na funçãomain
, estou exibindo apenas ela, assuma que todo o resto do código está igual estava da última vez que eu mostrei.
O resultado dessa vez é o seguinte:
I am a Derivative method.
Perfeito, criamos um objeto de Base
e o método que ele chamou foi da classe Base
, depois criamos um objeto de Derivative
e o método que ele chamou foi da classe Derivative
.
Mas, o que acontece se fizermos o seguinte:
int main()
{
Base *derivativeObject = new Derivative();
derivativeObject->method();
return 0;
};
A única alteração que eu fiz foi no tipo do ponteiro derivativeObject
para Base
.
Agora temos um problema... O tipo do ponteiro diz que ele aponta pra um objeto de Base
, porém, o que colocamos na memória foi um objeto de Derivative
. E agora?
Apesar do ponteiro estar declarado como do tipo Base
, o que realmente está salvo na memória é um objeto de Derivative
, portanto quando chamarmos o método, vai ser o método da classe Derivative
, certo?
Errado! O resultado é esse:
I am a Base method.
Mas por que?
Bom, para entender o que está acontecendo aqui, precisamos entender um pouco do que está acontecendo na memória durante a execução do nosso programa.
Quando nós criamos um objeto de Derivative
e chamamos o seu método method()
, temos o resultado do método de Derivative
, como já era esperado. Entretanto, isso não aconteceu porque nós trocamos um método pelo outro, ambos foram salvos na memória.
Mas você pode se perguntar por que raios ele dá preferência ao método de Derivative
se o método de Base
existe, é acessível e tem o mesmo nome, e isso acontece pois ele vai procurar executar a função disponível no escopo mais específico possível. Ele procura o método method()
na classe Derivative
, se encontrar ele executa, se não encontrar ele procura na classe mãe Base
, se encontrar ele executa, se não encontrar ele retorna um erro pois você chamou um método que não existe.
Com isso tudo eu quero chegar no ponto de que ambos estão na memória, tanto as coisas da classe Base
quanto da Derivative
estão gravadas em sequência na memória.
Só que antes, eu tinha definido o ponteiro do tipo Derivative
, e agora defini do tipo Base
, o que fez com que ele interpretasse meu objeto como do tipo Base
, mesmo sendo do tipo Derivative
.
"Ué, mas se tem o objeto
Derivative
na memória, por que o ponteiro finge que ele só tem as coisas deBase
?"
Na verdade ele não finge, nós é que enganamos ele quando dissemos que se tratava de um objeto de Base
. Vou criar um exemplo que vai deixar isso mais claro.
Na memória, nós só temos um grande caos de zeros e ums, o ponteiro só consegue recuperar esses dados porque ele sabe onde começar e onde parar.
No exemplo a seguir, ignore todos os números.
Vamos imaginar que a classe Base
tem os dados aba, e a classe Derivative
herda de Base
e tem os dados próprios cate, juntas elas formam a palavra abacate, pois na memória primeiro serão criadas as coisas da classe mãe (aba) e depois da classe filha (cate). Então no meio da memória teríamos algo parecido com isso:
9 8 7 3 2 a b a c a t e 8 9 2 3 7 2 8 3
Nosso "abacate" começa na sexta posição dessa sequência, e tem 7 letras, com essa informação sabemos onde a classe Derivative
começa (na sexta posição) e onde ela termina (depois de 7 letras).
Porém, se Derivative
tem 7 letras, ao contrário de Base
, que nesse nosso exemplo tem apenas 3 letras.
Se o ponteiro, sabendo que nossos dados começam na sexta posição, achar que se trata de um objeto de Base
, ele só vai pegar as três primeiras letras "aba".
Note que o "cate" continua existindo, entretanto, nosso ponteiro acha que nosso objeto tem apenas 3 letras, então ele para quando chega na terceira, fazendo parecer que "cate" não existe.
E o que isso tem a ver com o exemplo anterior?
Vamos trocar o "abacate" pelo nosso objeto Derivative
com o método method()
.
Você já entendeu que Derivative
tem as coisas tanto de Base
como de Derivative
, e que elas estão gravadas em sequência na memória.
Só falta conectar isso com o nosso exemplo do "abacate".
Vamos retomar o código:
int main()
{
Base *derivativeObject = new Derivative();
derivativeObject->method();
return 0;
};
Nesse código, estamos gravando na memória um objeto de Derivative
, e estamos colocando um ponteiro apontando pra essa região da memória, porém estamos dizendo para o nosso ponteiro que se trata de um objeto de Base
.
Como ilustrado na imagem acima, temos as coisas de Base
em um objeto de Base
, e ambas as coisas de Base
e Derivative
no objeto de Derivative
.
Se nós dissermos para o nosso ponteiro que se trata de um objeto de Base
, ele vai ver qual é o tamanho de um objeto dessa classe, e vai, partindo da posição que ele sabe qual é, ler os dados até onde teoricamente acabaria um objeto com o tamanho de um objeto de Base
.
Lembra que no exemplo do abacate, nossa superclasse tinha só as letras "aba"? É exatamente o que está acontecendo agora, estamos dizendo para o ponteiro que o objeto é menor do que ele realmente é, então ele está lendo apenas uma parte desse objeto.
Esse conceito é o que chamamos de fatiamento de objeto.
Ok, mas e o Polimorfismo?
Você entendeu como funciona os ponteiros e entendeu como que o jeito que eles funcionam causa o fatiamento de objeto. Agora, como isso se conecta com o Polimorfismo?
No Polimorfismo, podemos dizer a grosso modo que queremos fazer o inverso da herança.
Se na herança nós queremos, através de um objeto de uma classe filha, usar um método de uma classe mãe, no polimorfismo queremos ser capazes de, através da classe mãe, acessar um método da classe filha.
E como fazemos isso?
Tabela Virtual
O compilador do C++ tem um mecanismo que permite saber qual foi o objeto alocado no espaço de memória, ainda que isso discorde do tipo do ponteiro. Por enquanto assuma apenas que esse mecanismo existe, caso queira saber mais, você pode encontrar um artigo sobre a tabela virtual aqui.
Desse modo, podemos marcar métodos como virtuais, fazendo com que o método seja chamado como uma classe derivada.
#include <iostream>
class Base
{
public:
virtual void method() // alternar o "virtual" alterna o resultado
{
std::cout << "I am a Base method.";
}
};
class Derivative: public Base
{
public:
void method()
{
std::cout << "I am a Derivative method.";
}
};
int main()
{
Base *derivativeObject = new Derivative();
derivativeObject->method();
return 0;
};
E o resultado é esse:
I am a Derivative method.
Observe que eu inclui a palavra-chave virtual
no método da classe Base
. Quando eu faço isso, todas as classes que herdarem essa classe vão ter esse método marcado como virtual, ainda que ele seja sobrescrito.
Na prática, o que acontece é o seguinte quando nós chamamos o método method()
:
- O compilador verifica se esse método existe, confirmando que ele existe na classe Base.
- O compilador verifica que se trata de um método virtual, e através da tabela virtual, descobre que foi alocado um objeto de
Derivative
, mesmo que o ponteiro diga que é deBase
- O compilador passa a tratar esse objeto como um objeto da subclasse
Derivative
, como está na memória, para chamar o método deDerivative
.
Entendi, mas pra que serve isso?
Existem várias utilidades para o Polimorfismo, dentre elas, a implementação do Princípio Aberto-Fechado do SOLID pode ser feita através dele.
Mas não precisamos nos limitar a isso. Lembra quando eu disse que, se o ponteiro diz que é um objeto de Base
, ele não vai saber que tem mais coisas (coisas de Derivative
) ali? Isso faz com que ele não possa ler esses dados, mas também faz com que ele não possa apagar, uma vez que ele acredita não fazer parte do objeto.
E se tentarmos apagar um objeto de Derivative
chamando ele de Base
, o que vai acontecer é que só vamos apagar o conteúdo dele que for correspondente a classe Base
, o resto vai continuar na memória, e pior, o programa não sabe disso. O único nessa história que sabe onde ficava o objeto que nós apagamos é o ponteiro, e ele não apagou a parte exclusiva de Derivative
, pois ele acredita que essa parte não existe.
E é isso!
Espero que você tenha conseguido compreender o conceito de Polimorfismo. Se ficou alguma dúvida pode deixar nos comentários, ou se conseguir, entrar em contato comigo.
Bons estudos, e até mais o/
Top comments (1)
vei