Bueno, todos sabemos que estos es imposible porque básicamente C es un lenguaje estructurado, ¿Entonces porque hablar de Orientación a Objetos en ANSI C? Es bastante simple, cualquier lenguaje de programación moderno trabaja con el paradigma de objetos y es natural que todos estemos mayoritariamente acostumbrados a eso. Por otro lado el paradigma de objetos nos permite pensar, entender y desarrollar con mayor modularidad que con lenguajes estructurados.
El punto de todo esto es tratar de adoptar un patrón de codificación que nos acerque a lo que todos estamos acostumbrados, y es por eso que después de mucho investigar voy a presentar algunos pequeños conceptos.
Relación Objeto – Mensaje:
Lo primero a comprender es como poder establecer una relación en la que yo tengo un objeto y le envío un mensaje ( es decir invocamos el método de un objeto ).
# Método I ( TAD ):
En C tenemos las queridas y amadas estructuras de datos, y quizás el patrón mas conocido es el TAD. Un TAD se define como una estructura de datos y sus funciones asociadas, como por ejemplo:
typedef struct {
char * name;
int age;
} t_person;
Y a esto tenemos sus funciones asociadas:
char* person_getname(t_person*); int person_getage(t_person*); void person_walk(t_person*);
Ahora bien, modificando un poco el criterio de la sintaxis podria definir lo siguiente:
struct Person {
char * name;
int age;
};
char* Person_getName(struct Person *);
int Person_getAge(struct Person *);
void Person_walk(struct Person *);
Esto mas que nada nos puede hacer pensar que Persona es nuestra clase y que getName es un método estático de la clase Persona y nosotros le pasamos la instancia de la clase. Quizás este puede ser una de las formas mas comunes de ver el código en C. Pero como todo suele tener varias problemáticas, las cuales veremos mas adelante.
# Método II:
Este método, también es uno de los mas conocidos y consiste en asociar las funciones con las estructuras de datos, haciendo que estas contengan los punteros.
struct Person {
char * name;
int age;
void (* walk) (void *_self);
void (* run) (void *_self);
void (* talk) (void *_self, void *otherPerson);
};
person1->age;
person1->run(person1);
person1->talk(person1, person2);
En este caso cuando nosotros creamos la estructura le asociamos los punteros a los métodos los cuales invocamos con una sintaxis que nos podría hacer pensar en objetos pero no lo es. La principal cuestión de este patrón es la recursividad, como se puede ver la invocación no tiene mucho sentido:
person1->run(person1);
El hecho de acceder a la función run de person1 y tener que volver a pasarle la instancia lo vuelve algo poco amigable.
# Método II (BIS):
Una posible alternativa que se podría suponer que funciones es utilizar inner functions ( solo soportado por el GCC ) para solucionar el problema anterior:
struct Person {
char * name;
int age;
void (* walk) ();
void (* run) ();
void (* talk) (void *otherPerson);
};
static void * Person_init(void * _self){
struct Person * self = _self;
void inner_walk(){
Person_walk(self);
}
self->walk = inner_walk;
void inner_run(){
Person_run(self);
}
self->run = inner_run;
void inner_talk(void *arg){
Person_talk(self, arg);
}
self->talk = inner_talk;
return _self;
}
person1->run();
Pero lamento comunicar que esto no funciona, ya que el scope de las inner functions esta limitado a al stack de llamada, por lo que llamar a person1->run(); fuera del Person_init genera un seg fault porque el self al que llaman todas las inner ya no esta mas dentro del stack.
# Método III:
Esta forma es una combinación de sintaxis del Método II con el concepto del Método I, es decir utilizamos punteros a funciones dentro de nuestra estructura pero lo trabajamos como si fueran clases con métodos estáticos:
struct PersonClass {
void (* walk) (void *_self);
void (* run) (void *_self);
void (* talk) (void *_self, void *otherPerson);
};
struct Person {
char * name;
int age;
};
void Person_walk(struct Person *);
void Person_run(struct Person *);
void Person_talk(struct Person *, struct Persona *);
static const struct PersonClass PersonClass = {
Person_walk,
Person_run,
Person_talk
};
struct Person *person1;
PersonClass.run(person1);
Bueno dejando de lado como inicializamos ( ya que lo voy a exponer mas adelante ), vemos que la forma de trabajar de esto es mas o menos similar a las anteriores pero rejunta un poco de cada una. El punto es ¿Porque hace PersonClass.run(person1); en vez de hacer Person_run(person1)?
La respuesta es simple, C no es un lenguaje que nos deje sobre-escribir funciones tal como se puede en otros lenguajes, y menos hacer sobrecarga de funciones por lo cual dentro de nuestro entorno de compilación solo puede existir un solo Person_run que tiene un único comportamiento predefinido. Cuando trabajamos con PersonClass.run, este resulta ser un puntero a una función por lo cual nosotros podemos optar por cambiar el puntero para que apunte a otra función y con eso logramos cambiar la implementación Esto seguramente nos puede resultar muy útil para de alguna forma mockear nuestras estructuras o realizar cierto tipo de herencia ( veremos mas adelante ).
Como detalle hay que tener en cuenta que el manejo de punteros a funciones bajo esta metodología no es de los mas performante que existe. Esto es porque el compilador no sabe resolver los punteros de las funciones para que queden de tal forma que sean contiguos. Esto nos podría generar que el IP del CPU valla saltando de lado a lado por la memoria generando una enorme cantidad de page fault, cosa que sin la utilización de punteros el compilador puede ordenar la memoria para minimizar esta situación
# Método IV:
Como ultimo método pediremos ayuda a las poderosas macros de C:
#define INIT(var) void var##_run() { Person_run(var); } \
void var##_walk() { Person_walk(var); } \
void var##_talk(arg) { Person_talk(var, (void*)arg); }
#define NEW(var, type) type *var = malloc( sizeof(type) ); INIT(var)
NEW(person1, struct Person);
person1_run();
person1_talk(person2);
Tal y como se observa corremos con las ventaja de que en esta ocasión el pre-compilador nos ayuda a construir las invocaciones y finalmente podremos lograr una sintaxis en la que invocamos el método de una instancia. Nuestro principal problema acá es que nuestro scope es reducido ya que el NEW es local al bloque y si pasamos person1 como referencia a otra función dentro de esta otra deberíamos hacer INIT(person1). Por otro lado hay que tener en cuenta que eso obviamente nos genera una gran cantidad de código extra que si bien no es visible en la edición del código si lo puede ser cuando se compila.
Instanciación & Destrucción:
Hasta lo que mencione ahora otro de los factores a tener en cuenta en el modelo orientado a objetos es la instanciaciones de un objeto. A los fines prácticos cuando uno instancia una clase se reserva memoria para el objeto y se inicializa esta misma. Vamos a ver como logramos eso:
#ifndef CLASS_H_
#define CLASS_H_
#include <stddef.h>
#include <stdarg.h>
struct Class {
char * name;
size_t size;
void * (* ctor) (void * self, va_list * app);
void * (* equals) (const void * _class1, const void * _class2);
void * (* dtor) (void * self);
};
int Class_equals(const void * _class1, const void * _class2);
#endif
int Class_equals(const void * _class1, const void * _class2) {
const struct Class * class1 = _class1;
const struct Class * class2 = _class2;
return strcmp(class1->name, class2->name) == 0
&& class1->size == class2->size;
}
Acá tenemos definido lo que seria el molde de una clase genérica, lo que vamos a hacer es crear un tipo de clase, que para este caso vamos a tomar los String, y vamos a ver como manejarlo utilizando el Método III de relación entre objetos y mensajes.
Pero antes es necesario implementar nuestro mecanismo de construcion de objetos y para ellos vamos a tener:
#ifndef OBJECT_H_
#define OBJECT_H_
void * new (const void * type, ...);
int instanceOf (const void * obj, const void * class);
void delete (void * obj);
#endif /* OBJECT_H_ */
#include
#include
#include
#include
#include "Class.h"
#include "Object.h"
void * new(const void * _class, ...) {
const struct Class * class = _class;
void * p = calloc(1, class->size);
assert(p);
*(const struct Class **) p = class;
if (class->ctor) {
va_list ap;
va_start(ap, _class);
p = class->ctor(p, &ap);
va_end(ap);
}
return p;
}
int instanceOf(const void * obj, const void * _class) {
const struct Class ** cp = obj;
const struct Class * class = _class;
return strcmp((*cp)->name, class->name) == 0
&& (*cp)->size == class->size;
}
void delete(void * obj) {
const struct Class ** cp = self;
if (self && *cp && (*cp)->dtor)
self = (*cp)->dtor(self);
free(self);
}
Como se observa ahora tenemos un mecanismo de instanciación y destrucción que nos permite llamar a los métodos correspondientes de una clase y liberar su memoria. Pero ahora necesitamos nuestra clase String y definir sus comportamientos de destrucción y construcción.
#ifndef STRING_H_
#define STRING_H_
#include "Class.h"
struct String {
const void * class; /* must be first */
char * text;
};
struct StringClass {
void * (* clone) (struct String *);
int (* equals) (struct String *, struct String *);
};
void * String_constructor (void * _self, va_list * app);
void * String_destructor (void * _self);
void * String_clone (void * _self);
int String_equals (void * _self, void * _other);
static const struct Class _String = {
"String",
sizeof(struct String),
String_constructor, Class_equals, String_destructor
};
static const void * String = &_String;
static const struct StringClass StringClass = {
String_clone,
String_equals
};
#endif /* STRING_H_ */
#include
#include
#include
#include
#include "Class.h"
#include "Object.h"
#include "String.h"
void * String_constructor (void * _self, va_list * app){
struct String * self = _self;
const char * text = va_arg(* app, const char *);
self->text = malloc(strlen(text) + 1);
assert(self->text);
strcpy(self->text, text);
return self;
}
void * String_destructor (void * _self){
struct String * self = _self;
free(self->text), self->text = NULL;
return self;
}
void * String_clone (void * _self){
struct String * self = _self;
return new(String, self->text);
}
int String_equals (void * _self, void * _other){
struct String * str1 = _self;
struct String * str2 = _other;
return strcmp(str1->text, str2->text) == 0;
}
Finalmente, ya tenemos nuestra “clase” String con un constructor, un destructor, un clonador y un comparador. Para una pequeña prueba:
int main(void) {
struct String *str = new(String, "Hola!!!!");
struct String *aux = StringClass.clone(str);
printf("%s\n", str->text);
printf("%s\n", aux->text);
printf("%d\n", StringClass.equals(str, aux));
return EXIT_SUCCESS;
}
Output:
Hola!!!!
Hola!!!!
1
FUENTE: http://docs.google.com/viewer?url=http://www.planetpdf.com/codecuts/pdfs/ooc.pdf
La verdad que esta genial!
Yo ya me apunté el doc para leerlo este finde. =P
Facu siempre está más allá de lo evidente. Mira todo con la espada del Augurio.