Três são os conceitos básicos de programação a orientação a objeto: Herança, polimorfismo e encapsulamento. Neste artigo vamos ver como aplicar o polimorfismo no seu código em C. No artigo Orientação a objeto em C: Encapsulamento com estruturas opacas é apresentado como criar uma classe em C e sua leitura é sugerida.
Antes de entrarmos nos bits do tópico é válido lembrar que polimorfismo é o divisor de águas entre programação procedural e orientada a objetos. É dito que para uma linguagem ser verdadeiramente orientada a objetos é preciso suportar polimorfismo, de outra maneira, trata-se de uma linguagem baseada em objetos.
Exemplo de Polimorfismo
Polimorfismo permite que o comportamento das classes seja modificado através da herança, no artigo usa-se o exemplo hipotético de um aplicativo gráfico. Uma parte de código qualquer, desse aplicativo hipotético, terá um apontador para um objeto que será responsável por desenhar um retângulo e outro apontador para um objeto capaz de renderizar texto, ambos os objetos renderizadores terão a função draw() que deve se comportar diferente para cada caso, um, desenhando retângulos e outro texto. O polimorfismo entra para resolver o seguinte problema: nem sempre o código terá a posse de um texto e de um retângulo, como no exemplo, durante a execução ele pode ter a posse de qualquer objeto desenhável e em qualquer quantidade, e, portanto, os seus apontadores precisam apontar um tipo genérico de elemento desenhável, ou seja, é usado uma classe comum para representar qualquer elemento desenhável onde o comportamento da sua função draw() é alterada de acordo com quem herda-a.
O exemplo ilustrado pode ser implementado em C com a representação UML abaixo:
A chave do polimorfismo se dá pela tabela virtual, em C trata-se de uma simples struct ou, neste caso, classe, que contém apontadores para funções que serão substituídas e mais informações que são necessárias para que métodos da classe herdada sejam implementados pela classe herdeira.
Para implementar o polimorfismo em C, são necessários alguns atores como identificado no diagrama UML:
- Classe Abstrata: widget_t é a classe abstrata, que será usada sempre para chamar a função draw();
- Classes Herdeiras: text_t e rectangle_t implementam a função draw() de acordo com as suas necessidades;
- Tabela Virtual: widget_vtable_t é preenchida por text_t ou por rectangle_t, a tabela contém os métodos substituíveis através da herança, no caso, chamadas para o método draw() de widget_t são redirecionadas para as classes text_t e rectangle_t através da tabela virtual.
Em OOP, quando uma classe filha é criada, o construtor da classe pai também é chamado, no nosso caso, temos que também configurar a tabela virtual. Na solução proposta, a tabela virtual é uma classe assim como widget_t e demais, porém possui uma política de uso bem definida onde deve sempre ser criada e configurada por text_t e rectangle_t e transferida para widget_t através do seu construtor. Essa característica é observada no código abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void draw(rectangle_t *); rectangle_t * rectangle_create() { rectangle_t * obj = (rectangle_t *) calloc(1, sizeof(struct s_rectangle_instance)); /* Create a vtable with draw and intance parameters configured */ obj->self_widget_vtable = widget_vtable_create(obj, (void(*)(void *))draw); /* Create the parent class widget_t passing the vtable on the constructor */ obj->self_widget = widget_create(obj->self_widget_vtable); return obj; } |
Em OOP também ocorre uma conversão implícita de um objeto da classe herdeira para qualquer classe herdada, agora temos que representar esse comportamento:
1 2 3 4 |
widget_t * rectangle_get_widget(rectangle_t * const obj) { return obj->self_widget; } |
A função draw() da classe widget_t tem que chamar a implementação correta de acordo com a tabela virtual:
1 2 3 4 |
void widget_draw(widget_t * obj) { obj->vtable->draw(obj->vtable->parent_instance); } |
E, finalmente, o uso do esquema apresentado se dá da seguinte maneira:
1 2 3 4 5 6 7 |
widget_t * widget_list[2]; widget_list[0] = text_get_widget(text_create()); widget_list[1] = rectangle_get_widget(rectangle_create()); widget_draw(widget_list[0]); widget_draw(widget_list[1]); |
No exemplo ilustrado não existe destrutor virtual por fins didáticos, ainda assim, é muito importante permitir que a classe herdeira possa ser destruída corretamente a partir da classe herdada, portanto o método de destruição também deve estar presente na tabela virtual.
Os códigos apresentados são encontrados na íntegra em https://github.com/felipe-lavratti/marsh/tree/master/marsh/src. Leves modificações foram efetuadas por fins didáticos.
Referências
Design Pattern for Embedded Systems in C, Bruce Powel Douglass – Introduction
Marsh, Interface Gráfica em ANSI C de código aberto, Github.
Olá Felipe, este é o segundo artigo seu que eu li (o anterior foi sobre o princípio da responsabilidade única) e gostei muito da maneira como você busca difundir o uso de padrões de projeto no mundo dos embarcados. O Polimorfismo é sem dúvida uma característica fundamental da OO. Muitas vezes nos deparamos com a necessidade de criar em C uma mesma função que tenha comportamento diferente dependendo do contexto em que se encontra. E no C, a maneira “padrão” de resolver este problema é utilizando condicionais, como um “switch” ou um “if else”; porém a implementação fica muito mais… Leia mais »
Obrigado Richard, você está certo, implementar polimorfismo em C não acontece sem alguns overheads.