Programación en el entorno GNOME |
---|
Una de las técnicas más modernas y utilizadas en la actualidad en la programación es lo que se denomina “programación orientada a objetos”, o POO, que consiste en enfocar la programación de una forma mucho más cercana a la percepción real de las personas (los programadores), usando el concepto de objetos para encapsular las distintas funcionalidades de las aplicaciones.
La POO permite estructurar los programas de una forma mucho más clara para la percepción humana que la programación “tradicional” (programación estructurada) de forma que, cada funcionalidad específica de la aplicación, está claramente separada y encapsulada (implementación oculta al resto de la aplicación). Esto permite varias cosas:
Desarrollar cada una de las partes de la aplicación independientemente del resto.
La POO normalmente está asociada a lenguajes de programación que la soportan, tales como Java™, C++™, SmallTalk™, etc. Sin embargo, al ser una técnica realmente potente para el desarrollo de aplicaciones, en el proyecto GNOME, y más concretamente en GTK+™, se decidió usar dicha técnica en el lenguaje más usado dentro del proyecto: C™. Éste no es un lenguaje preparado para POO, pero con un esfuerzo mínimo, y gracias al trabajo de los desarrolladores de GTK+™, se puede usar en este lenguaje. Esto se consigue mediante el sistema de objetos de GLib™ (GObject), que, mediante el uso de las características del lenguaje C™, ofrece la posibilidad de usar POO.
Este sistema de objetos de GLib™ tiene algunas limitaciones propias del uso del lenguaje C™ (lenguaje no pensado para la POO), pero aun así, su funcionalidad es tal, que es más que probable que jamás se eche ninguna funcionalidad en falta. Dicha funcionalidad incluye:
herencia, característica imprescindible de cualquier lenguaje orientado a objetos que se precie de serlo; permite la creación de clases que heredan la funcionalidad de otras clases ya existentes. Esto permite crear clases con la funcionalidad básica y, basadas en dicha clase, crear otras que añadan una funcionalidad más específica.
polimorfismo, que permite tratar a un mismo objeto bajo distintas personalidades.
interfaces, que permite la definición de interfaces (clases abstractas) y su posterior implementación en clases.
GLib incluye un sistema dinámico de tipos, que no es más que una base de datos en la que se van registrando las distintas clases. En esa base de datos, se almacenan todas las propiedades asociadas a cada tipo registrado, información tal como las funciones de inicialización del tipo, el tipo base del que deriva, el nombre del tipo, etc. Todo ello identificado por un identificador único, conocido como GType.
Para crear instancias de una clase, es necesario que el tipo haya sido registrado anteriormente, de forma que esté presente en la base de datos de tipos de GLib™. El registro de tipos se hace mediante una estructura llamada GTypeInfo, que tiene la siguiente forma:
struct _GTypeInfo { /* interface types, classed types, instantiated types */ guint16 class_size; GBaseInitFunc base_init; GBaseFinalizeFunc base_finalize; /* classed types, instantiated types */ GClassInitFunc class_init; GClassFinalizeFunc class_finalize; gconstpointer class_data; /* instantiated types */ guint16 instance_size; guint16 n_preallocs; GInstanceInitFunc instance_init; /* value handling */ const GTypeValueTable *value_table; };
La mayor parte de los miembros de esta estructura son punteros a funciones. Por ejemplo, el tipo GClassInitFunc es un tipo definido como puntero a función, que se usa para la función de inicialización de las clases.
Para comprender esta estructura, lo mejor es ver un ejemplo de cómo se usa. Para ello, se usan las siguientes funciones:
GType g_type_register_static (GType parent_type, const gchar *type_name, const GTypeInfo *info, GTypeFlags flags); GType g_type_register_fundamental (GType type_id, const gchar *type_name, const GTypeInfo *info, const GTypeFundamentalInfo *finfo, GTypeFlags flags);
Con estas dos funciones y la estructura GTypeInfo se realiza el registro de nuevos tipos en GLib™ y, todo ello normalmente, se realiza en la función _get_type de la clase que se esté creando. Esta función es la que se usará posteriormente para referenciar al nuevo tipo, y tiene, normalmente, la siguiente forma:
GType my_object_get_type (void) { static GType type = 0; if (!type) { static GTypeInfo info = { sizeof (MyObjectClass), (GBaseInitFunc) NULL, (GBaseFinalizeFunc) NULL, (GClassInitFunc) my_object_class_init, NULL, NULL, sizeof (MyObject), 0, (GInstanceInitFunc) my_object_init }; type = g_type_register_static (PARENT_TYPE, "MyObject", &info, 0); } return type; }
Como se puede apreciar, esta función simplemente tiene una variable (type) estática, que se inicializa a 0 y, cuando se le llama, si dicha variable es igual a 0, entonces registra el nuevo tipo (llamando a g_type_register_static) y rellena una estructura de tipo GTypeInfo antes, con los datos de la nueva clase, tales como el tamaño de la estructura de la clase (MyObjectClass), la función de inicialización de la clase (my_object_class_init), el tamaño de la estructura de las instancias de la clase (MyObject) y la función de inicialización de las instancias que se creen de esta clase (my_object_init).
Una vez que se tiene la función que registra la clase, crear instancias de esa clase es tan sencillo como llamar a la función g_object_new, que tiene la siguiente forma:
gpointer g_object_new (GType object_type, const gchar *first_property_name, ...);
Esta función tiene una lista variable de argumentos, que sirve para especificar una serie de propiedades a la hora de crear el objeto. Pero, de momento, lo único interesante de g_object_new es conocer su funcionamiento. Por ejemplo, para crear una instancia de la clase MyObject, una vez que se tiene la función de registro de la clase (my_object_get_type), no hay más que llamar a g_object_new de la siguiente forma:
GObject *obj; obj = g_object_new (my_object_get_type (), NULL);
De esta forma, se le pide a GLib™ que cree una nueva instancia de la clase identificada por el valor devuelto por la función my_object_get_type, que será el valor devuelto por g_type_register_static, tal y como se mostraba anteriormente.
Muchos de los tipos que se registran no son directamente instanciables (no se pueden crear nuevas instancias) y no están basados en una clase. Estos tipos se denominan tipos “fundamentales” en la terminología de GLib™ y son tipos que no están basados en ningún otro tipo, tal y como sí ocurre con los tipos instanciables (u objetos).
Entre estos tipos fundamentales se encuentran algunos viejos conocidos, como por ejemplo gchar y otros tipos básicos, que son automáticamente registrados cada vez que se inicia una aplicación que use GLib™.
Como en el caso de los tipos instanciables, para registrar un tipo fundamental es necesaria una estructura de tipo GTypeInfo, con la diferencia que para los tipos fundamentales bastará con rellenar con ceros toda la estructura, excepto el campo value_table.
GTypeInfo info = { 0, /* class_size */ NULL, /* base_init */ NULL, /* base_destroy */ NULL, /* class_init */ NULL, /* class_destroy */ NULL, /* class_data */ 0, /* instance_size */ 0, /* n_preallocs */ NULL, /* instance_init */ NULL, /* value_table */ }; static const GTypeValueTable value_table = { value_init_long0, /* value_init */ NULL, /* value_free */ value_copy_long0, /* value_copy */ NULL, /* value_peek_pointer */ "i", /* collect_format */ value_collect_int, /* collect_value */ "p", /* lcopy_format */ value_lcopy_char, /* lcopy_value */ }; info.value_table = &value_table; type = g_type_register_fundamental (G_TYPE_CHAR, "gchar", &info, &finfo, 0);
La mayor parte de los tipos no instanciables estan diseñados para usarse junto con GValue, que permite asociar fácilmente un tipo GType con una posición de memoria. Se usan principalmente como simples contenedores genéricos para tipos sencillos (números, cadenas, estructuras, etc.).
En el apartado anterior se mostraba la forma de registrar una nueva clase en el sistema de objetos de GLib™, y se hacía referencia a distintas estructuras y funciones de inicialización. En este apartado, se va a indagar con más detalle en ellos.
Tanto los tipos fundamentales como los no fundamentales quedan definidos por la siguiente información:
Tamaño de la clase.
Funciones de inicialización (constructores en C++™).
Funciones de destrucción (destructores en C++™), llamadas de finalización en la jerga de GLib™.
Tamaño de las instancias.
Normas de instanciación de objetos (uso del operador new en C++™).
Funciones de copia.
Toda esta información queda almacenada, como se comentaba anteriormente, en una estructura de tipo GTypeInfo. Todas las clases deben implementar, aparte de la función de registro de la clase (my_object_get_type), al menos dos funciones que se especificaban en la estructura GTypeInfo a la hora de registrar la clase. Estas funciones son:
my_object_class_init: función de inicialización de la clase, donde se inicializacirá todo lo relacionado con la clase en sí, tal como las señales que tendrá la clase, los manejadores de los métodos virtuales si los hubiera, etc.
my_object_init: función de inicialización de las instancias de la clase, que será llamada cada vez que se solicite la creación de una nueva instancia de la clase. En esta función, las tareas a desempeñar son todas aquellas relacionadas con la inicialización de una nueva instancia de la clase, tales como los valores iniciales de las variables internas de la instancia.
Los interfaces GType son muy similares a los interfaces en Java™ o C#™. Es decir, son definiciones de clases abstractas, sin ningún tipo de implementación, que definen una serie de operaciones que debe segir las clases que implementen dichos interfaces deben seguir.
En GLib™, para declarar un interfaz, es necesario registrar un tipo de clase no instanciable, que derive de GTypeInterface. Por ejemplo:
struct _GTypeInterface { GType g_type; /* iface type */ GType g_instance_type; }; typedef struct { GTypeInterface g_iface; void (*method_a) (FooInterface *foo); void (*method_b) (FooInterface *foo); } FooInterface; static void foo_interface_method_a (FooInterface *foo) { foo->method_a (); } static void foo_interface_method_b (FooInterface *foo) { foo->method_b (); } static void foo_interface_base_init (gpointer g_class) { /* Initialize the FooInterface type. */ } GType foo_interface_get_type (void) { GType foo_interface_type = 0; if (foo_interface_type == 0) { static const GTypeInfo foo_interface_info = { sizeof (FooInterface), /* class_size */ foo_interface_base_init, /* base_init */ NULL, /* base_finalize */ NULL, NULL, /* class_finalize */ NULL, /* class_data */ 0, /* instance_size */ 0, /* n_preallocs */ NULL /* instance_init */ }; foo_interface_type = g_type_register_static (G_TYPE_INTERFACE, "FooInterface", &foo_interface_info, 0); } return foo_interface_type; }
Un interfaz queda definido por una sola estructura cuyo primer miembro deber ser del tipo GTypeInterface, que es la clase base (ver la “Herencia”) para los interfaces. Aparte de este primer miembro, la estructura que define el interfaz debe contener punteros a las funciones definidas en el interfaz. Es decir, los métodos del interfaz. Aparte de eso, como muestra el ejemplo anterior, constituye buena práctica incluir funciones que encapsulen el acceso a esos punteros a funciones, tal y como hacen las funciones foo_interface_method_a y foo_interface_method_b del ejemplo anterior. Aunque esto no es obligatorio, es aconsejable, pues ayuda en la legibilidad del código donde se haga uso del interfaz.
Hasta aquí, la definición del interfaz, que, como se puede apreciar, no contiene ninguna implementación. Simplemente, contiene la definición de los métodos que deben ser implementados para soportar este interfaz. Tras esto, para que el interfaz definido sea útil, es necesario hacer que otras clases definidas implementen dicho interfaz. Para ello, se usa la función g_type_add_interface_static, tal y como se muestra en el siguiente fragmento de código, en la función attach_interface_to_type:
static void implementation_method_a (FooInterface *iface) { /* Implementación de método A */ } static void implementation_method_b (FooInterface *iface) { /* Implementación de método B */ } static void foo_interface_implementation_init (gpointer g_iface, gpointer iface_data) { FooInterface *iface = (FooInterface *) g_iface; iface->method_a = implementation_method_a; iface->method_b = implementation_method_b; } static void attach_interface_to_type (GType type) { static const GInterfaceInfo foo_interface_implementation_info = { (GInterfaceInitFunc) foo_interface_implementation_init, NULL, NULL }; g_type_add_interface_static (type, foo_interface_get_type (), &foo_interface_implementation_info); }
g_type_add_interface_static registra, dentro del sistema de tipos de GLib™ que un determinado tipo implementa un determinado interfaz. Antes de eso, es necesario rellenar una estructura de tipo GInterfaceInfo, que contiene todos los datos (funciones de inicialización y finalización, así como datos de contexto a los que se podrá acceder desde cualquier punto, a través de la implementación del interfaz) referentes a una implementación concreta del interfaz:
struct _GInterfaceInfo { GInterfaceInitFunc interface_init; GInterfaceFinalizeFunc interface_finalize; gpointer interface_data; };
Un sistema orientado a objetos que se precie debe, de alguna forma, ofrecer la posibilidad de crear nuevas clases que contengan la funcionalidad base de otras clases ya existentes. Esto se conoce como herencia en la terminología de la POO y permite la reutilización de funcionalidad ya existente, de forma que, simplemente, haya que añadir la funcionalidad extra requerida.
En GLib™, desarrollada en C™, esto se consigue mediante el uso de estructuras, como se ha visto anteriormente, y usando, como primer miembro de la estructura que identifica a la nueva clase, un valor del mismo tipo que la clase base. Por ejemplo, si se quisiera crear una clase ClaseB que derivara de ClaseA, la nueva clase debería definirse de la siguiente manera:
struct ClaseA { ... }; struct ClaseAClass { ... }; ... struct ClaseB { struct ClaseA base; }; struct ClaseBClass { struct ClaseAClass base_class; };
Éste es el primer paso para usar la herencia. De esta forma, como el primer miembro de la clase derivada es del tipo de la clase base, se puede convertir (“casting” en la terminología del lenguaje C™) de un tipo a otro indistintamente. Así, por ejemplo:
ClaseA *clasea; ClaseB *claseb; claseb = g_object_new (claseb_get_type (), NULL); clasea = (ClaseA *) claseb;
Los demás pasos para conseguir la herencia ya han sido comentados anteriormente y se reducen a especificar la clase base de la nueva clase al registrarla (g_type_register_static).
<< GLib™ avanzado. | Señales >> |