TEMA 11: POO CON LENGUAJES NO ORIENTADOS A OBJETOS.

Contenidos

1. ¿Es necesario un LOO para hacer POO?

  • La utilización del paradigma de programación orientada a objetos requiere cierta disciplina por parte del programador y elementos sintácticos por parte del lenguaje empleado.
  • Con los lenguajes no orientados a objetos (LNOO) esto último no lo tenemos disponible pero aún así, sí que es posible hacer POO con un LNOO.
  • No vamos a disponer de los elementos que convierten a un lenguaje de programación en un LOO, pero sí que vamos a poder simularlos de manera bastante efectiva.
  • En este tema explicaremos cómo hacerlo en el lenguaje C.

1.1. ¿Qué podemos simular?

  • Representación de clases.
  • Paso de mensajes.
  • Constructores y destructor de una clase.
  • Representación de la herencia simple.
  • Resolución de métodos en tiempo de ejecución.

1.2. ¿Cómo es posible hacerlo?

  • Dado que trabajaremos en C, vamos a aprovechar la gestión de punteros y la reserva de memoria dinámica de que dispone.
  • Esto permitirá que la simulación de las características antes comentadas se lleve a cabo con poca o nula pérdida de eficiencia.
  • De hecho veremos ejemplos reales que emplean esta técnica.

2. Representación de clases

  • Empleamos aquello que más se le parece: struct
  • De manera que cada clase del diseño se convierte en un struct del lenguaje.
  • Cada atributo (datos) definido en la clase es un campo del struct creado.
  • Los objetos serán instancias de estos struct y podrán estar ubicados en el almacenamiento global, en la pila o en memoria dinámica (preferiblemente en este último).
  • Por lo tanto la referencia a un objeto se puede representar mediante un puntero a su struct.

2.1. Ejemplo de una clase en C

typedef float Length;
struct Window {
  Length xmin;
  Length ymin;
  Length xmax;
  Length ymax;
}
...
struct Window* w;
...
Lenght x1 = w->xmin;            /* Mejor emplear setters/getters */

3. Paso de mensajes

  • Debemos seguir un convenio para nombrar a las funciones de manera que las identifiquemos con métodos de una clase: NombreClase_nombreMetodo.
  • En el caso de constructor y destructor emplearemos: NombreClase_create y en el del destructor NombreClase_destroy.
  • Cada método de instancia recibirá un primer parámetro, lo llamaremos self, que representará al objeto receptor del mensaje. Hemos de hacerlo de manera explícita.
  • El tipo de este parámetro será puntero a la clase receptora del mensaje.

3.1. Un ejemplo de la signatura de un método

/*************************/
/* Clase: Window         */
/* Metodo: addToSelected */
/*************************/
Window_addToSelected (struct Window* self, struct Shape* s);
  • El paso de parámetros lo hacemos por puntero ya que es más eficiente.
  • Como en cualquier función, tenemos accesibles sus parámetros para realizar cualquier operación que queramos con ellos.

4. Constructores y destructor de una clase

/* Objetos en memoria dinamica */
struct Window* Window_create(Length x, Length y, Length w, Length h) {
  struct Window* ventana;
  ventana = (struct Window*) malloc(sizeof(struct Window));
  ventana -> xmin = x;
  ventana -> ymin = y;
  ventana -> xmax = x + w;
  ventana -> ymax = y + h;
  return ventana;
}
void Window_destroy (struct Window* self) {
   if (self != NULL) free (self);
}

5. Representación de la herencia simple.

5.1. Preliminares

  • La simulación de la herencia simple se basa en una idea muy sencilla:
    1. Colocamos al principio de la clase derivada los atributos heredados de la clase base y en el mismo orden en el que están en la clase base.

      Esto se puede simplificar si los agrupamos en un struct.

    2. Añadimos a continuación la parte específica de la clase derivada, es decir, los atributos nuevos que añade la clase derivada.
  • Veamos un ejemplo:

5.2. Simulando la herencia simple

herencia6.png

5.3. Simulando la herencia simple

herencia9.png

5.4. Descriptores de clase

  • Para que esta simulación sea completa y podamos añadir posteriormente los métodos a la clase, necesitamos añadir un campo más a cada una de las struct anteriores. Lo añadiremos al principio.
  • Se trata de un puntero a su descriptor de clase. Se llamará dc.
  • Los descriptores de clase son otro tipo de struct que incluyen a los métodos y variables de clase.
  • Por tanto el gráfico anterior quedaría así:

herencia11.png

5.5. Descriptores de clase

  • Utilizando esta técnica podemos pasar un puntero a un objeto de clase Rectangulo o Circulo a una función que espere un puntero a un objeto de tipo Figura.
  • Así conseguimos que entre dos clases relacionadas por herencia, la representación en memoria de sus primeros N-bytes sea igual. Esta es la base de la representación de la relación Es Un (Is A) que hay entre una clase derivada y su base.
  • En el siguiente ejemplo un puntero a un Rectangulo se interpreta como un puntero a una Figura:

    struct Rectangle* r;
    struct Window*    w;
    
    /* prototipo de: Window_addToSelected */
    void Window_addToSelected (struct Window* self, struct Shape* s);
    ...
    Window_addToSelected (w, r);
    

5.6. Resolución dinámica de métodos

  • Se trata de elegir en tiempo de ejecución qué método invocar en respuesta a un mismo mensaje enviado a distintos objetos.
  • En lenguaje C vamos a emplear punteros a funciones para realizarlo.
  • Para implementarlo de forma sencilla vamos a hacer uso del descriptor de clase comentado anteriormente.
  • El descriptor de clase es una estructura que contiene:
    • Una cadena con el nombre de la clase que representa.
    • Un puntero a cada método de la clase, incluídos los heredados.
    • Las variables de clase que pueda tener la clase a la que representa.
  • Los descriptores de clase sólo son necesarios para aquellas clases que vayan a tener instancias y no para clases abstractas tales como `Figura'.
  • El nombre de la estructura que representa al descriptor de clase es el mismo que el de la clase añadiéndole el sufijo Class: ShapeClass, CircleClass, etc…
  • Veamos un ejemplo de descriptores de clase:
/**********************************/
/* Descriptor de clase para Shape */
/**********************************/
struct ShapeClass {
  char*   classname;
  void    (*move)    ();
  Boolean (*selected)();
  void    (*ungroup) ();
  void    (*draw)    ();
};

/* Descriptor de clase para Circle */ /* Descriptor de clase para Rectangle */
struct CircleClass {                  struct RectangleClass {
  char*   classname;                    char*   classname;
  void    (*move)     ();               void    (*move)    ();
  Boolean (*selected) ();               Boolean (*selected)();
  void    (*ungroup)  ();               void    (*ungroup) ();
  void    (*draw)     ();               void    (*draw)    ();
};                                    };
  • La estructura del descriptor de clase define los nombres de las operaciones visibles de la clase y, si existen, contendrá las variables de clase de la clase a la que representa.
  • Todavía tenemos que definir e iniciar un objeto descriptor de clase para cada clase.
  • Cada uno de estos objetos descriptores de clase es una única variable global, la cual será la única instancia de la clase descriptor de clase correspondiente.
  • Cada campo del objeto descriptor de clase debe ser iniciado con el nombre de la función de C (su dirección) definida o heredada por la clase, por ejemplo:
struct RectangleClass RectangleClass = {
   "Rectangle",
   Shape_move,         /* void    (*move)    () */
   Rectangle_selected, /* Boolean (*selected)() */
   Shape_ungroup,      /* void    (*ungroup) () */
   Rectangle_draw      /* void    (*draw)    () */
};

struct CircleClass CircleClass = {
   "Circle",
   Shape_move,      /* void    (*move)    () */
   Circle_selected, /* Boolean (*selected)() */
   Shape_ungroup,   /* void    (*ungroup) () */
   Circle_draw      /* void    (*draw)    () */
};
  • Cuando se crea un objeto, guardamos en su primer campo dc, que es de tipo puntero a su descriptor de clase, la dirección del objeto global descriptor de la clase.
  • De este modo, y en tiempo de ejecución, podemos obtener:
    • El nombre de esta clase.
    • Sus variables de clase.
    • Los métodos asociados a esta clase.
  • Por ejemplo, la creación de un objeto de clase Circle se haría así:
struct Circle*
Circle_create(Length x0,Length y0,Length r) {
    struct Circle* nc;

    nc = (struct Circle*) malloc(sizeof(struct Circle));
    nc -> dc     = &CircleClass; /* descriptor de clase */
    nc -> x      = x0;
    nc -> y      = y0;
    nc -> radius = r;
    return nc;
}
  • ¿Y si crearámos varios círculos?
  • Visualmente podríamos representarlo así:

    dclase4.png

  • Como vamos a ver, la resolución en tiempo de ejecución de un método para un objeto se realizará a partir del objeto descriptor de clase al que apunta su campo dc.
  • Para ello accedemos al campo del descriptor de clase al que se refiere la operación que queremos.
  • Si crearamos un círculo y le enviáramos mensajes:

    /* Primero: Ya se han creado los descriptores de clase */
    struct Shape* f;
    struct Circle* c1 = Circle_create(...);
    f = c1;
    
    f->dc->move(f, ...); /* Invoca Figura::mover */
    f->dc->draw(f, ...); /* Invoca Circulo::dibujar */
    

6. Casos de uso

  • Estas técnicas se emplean (y se amplían) en proyectos software reales.
    1. Uno de esos proyectos es la biblioteca de creación de interfaces de usuario Gtk+. Con ella está construída la interfaz de usuario del IDE geany o de nemiver (ambos instalados en la máquina virtual empleada en prácticas).
    2. Veamos algunos de los archivos de cabecera que reflejan su jerarquía de clases.
  • También es la base del código C generado por el compilador de Vala , se puede ver con este sencillo ejemplo:
 // Compilar con valac -C valaoop.vala

 // Clase base
 public class Droid {
        public Droid (string n) {
          name = n;
        }

        public string name {get; set;}
        public virtual void move (int x, int y) {
                this.x = x;
                this.y = y;
        }

        protected int x;
        protected int y;
}
// Clase derivada
public class AquaDroid : Droid {
        public AquaDroid(string n, int md = 100) {
                base (n);
                depth = md;
        }

        public override void move (int x, int y) {
                this.x = x/2;
                this.y = y/2;
        }

        private int depth;
}

7. Aclaraciones

  • En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.

Created: 2024-01-01 lun 17:45

Validate