Marino
Marino
Marino
Tutores:
i
Agradecimientos
ii
tan buena persona con nosotros.
Gracias a mis suegros Hugo y Lita, si no fuera por su ayuda a la familia sería
muy difícil disponer del tiempo para proyectos de cualquier tipo. Gracias
por su amor, tolerancia y apertura.
Gracias a mis tutores de tesis que en forma desinteresada se ocupan de los
alumnos que deseamos recibirnos con este honor.
Gracias a mi primer jefe, Daniel Litvinov socio de Dhacel SRL, que me
permitió desarrollarme y aplicar en los sistemas embebidos los conocimientos
que fueron la semilla de esta tesis. Gracias a mi último jefe hasta el momento,
Sergio Starkloff socio de Surix SRL, por darme lugar a utilizar el generador
de código creado para esta tesis en un ambiente de producción ayudando a
profundizar mi conocimiento en el tema.
Gracias a la UBA por la educación formal que he recibido y a la nación
Argentina que me la ha dado gratuitamente.
Gracias a cada uno de los familiares, amigos y personas que influencian en
mi vida para bien.
Gracias al pueblo judío por preservar su milenaria tradición religiosa de la
que me he nutrido diariamente desde los 21 años y me ayuda a ver la vida
de forma distinta, con un propósito y, por lo tanto, con más energía.
iii
Tabla de contenidos
Resumen i
Agradecimientos ii
Abreviaturas
1 Introducción 1
1.1 Acerca . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Estructura de la tesis . . . . . . . . . . . . . . . . . . . . . 2
Referencias 192
Abreviaturas
Introducción
1.1 Acerca
1
de código desde diagramas de clase UML analizando la ventaja obtenida al
utilizarla.
2
Capítulo 2
Programación orientada a
objetos y UML
2.1.1 Introducción
3
conocidas: modularidad, flexibilidad, extensibilidad, reutilización de código.
La orientación a objetos ha sido la aplicación de los buenos principios
de programación enseñados con anterioridad a su existencia (Douglass
2010). El enfoque de la orientación a objetos ha demostrado ser valioso en
la comprensión de problemas y su solución en todo tipo de dominios de
problemas, así también como grados de tamaño y complejidad. Además,
la mayoría de los lenguajes contemporáneos están orientados a objetos de
alguna manera, lo que proporciona una causa mayor para ver el mundo en
términos de objetos. Fuera de esta definición simplista, definir exactamente
qué es la orientación a objetos es una tarea muy difícil. Algunos consideran
como esencial lo que para otros no lo es. Alrededor de la orientación a
objetos distintos conceptos han sido elaborados, y cada implementación a
tomado un subconjunto de aquellos (Hendrickx 2004).
2.1.2 Conceptos
2.1.3 Encapsulación
4
1998). El implementador de la clase define los tipos de datos de la misma y
la forma de interactuar con ellos mediante una interfaz bien definida, puede
haber datos totalmente accesibles por el usuario y, por el otro lado, otros
totalmente invisibles al mismo.
Si una clase sólo permite acceder a sus datos a través de su interfaz, esto se
llama encapsulación fuerte. Ciertas implementaciones (tanto lenguajes como
en nuestro caso frameworks) de POO pueden obligar la encapsulación fuerte.
2.1.4 Herencia
5
criticada por la complejidad que puede traer en los diseños y la complejidad
de resolución del problema del diamante (Estrada 2007).
Una clase derivada es aquella que hereda de una o varias clases, que pueden
ser a su vez derivadas o base. Se dice que una clase derivada o hija hereda
de una clase padre.
2.1.5 Polimorfismo
6
que sí sepa responder a dicho mensaje, dicho objeto es un delegado.
2.1.6 Interfaces
2.1.7 Mixin
Los mixins son una mezcla entre clases e interfaces. Definen y pueden
implementar nuevos métodos, e incluso pueden poseer datos miembros pero
no pueden ser instanciados. Lo único que puede hacerse con un Mixin es
que sea heredado por una clase. Puede verse a un Mixin como una interfaz
pero que define una implementación.
7
2.1.8 Metaclases
Las instancias de las clases son objetos. Una metaclase es una clase cuyas
instancias son clases. Esto permite crear clases en tiempo de ejecución.
8
2.1.12 Reflexión
2.1.14 Genericidad
9
2.1.14.1 En lenguajes fuertemente tipados
10
importantes de un sistema desde un enfoque conceptual. Puede describir
desde procesos de negocio y funciones de un sistema así como cosas
concretas como clases escritas en un lenguaje de programación específico y
componentes reutilizables de software.
La notación de UML está especificada por el metamodelo de UML. En esta
tesis usaremos la versión de metamodelo 2.5.1 (OMG 2017).
La mayoría del contenido de las próximas secciones está basado en el
libro “The Unified Modeling Language User Guide” de los autores de UML
(Grady Booch, James Rumbaugh e Ivar Jacobson) (Grady Booch 1998).
11
código nunca dejó un modelo que documente lo que estaba en su cabeza como
solución al problema, esa información puede perderse o llegar a reproducirse
parcialmente si ese programador no estuviese más.
12
2.2.4.1 Clases
UML provee una representación gráfica de una clase. La siguiente figura nos
muestra tal representación.
13
2.2.4.2 Visibilidad
2.2.4.3 Alcance
14
el atributo isAbstract) lo que significa que no provee una implementación
y la misma se posterga a clases hijas, o también puede ser polimórfica
especificando una implementación, esto se consigue con el atributo isLeaf
en false. Una operación con el atributo isLeaf en true significa que no es
polimórfica.
Por último, una operación puede ser constante, esto significa que no modifica
al objeto al que se le hace la llamada. Esto se consigue con el atributo
isConst en true.
2.2.4.5 Paquetes
2.2.4.7 Relaciones
15
2.2.4.7.2 Asociación Una asociación es una relación estructural que
describe un conjunto de vínculos entre dos clases. Una asociación contiene
una multiplicidad (una instancia de la clase A con cuantas instancias de la
clase B puede estar vinculada y viceversa). La siguiente figura nos muestra
una asociación entre una clase A y otra B donde cada A se vincula con varios
B (representado por el asterisco) y cada B con un solo A (representado por
el número uno).
16
2.2.4.7.2.1 Agregación Una agregación es un tipo especial de
asociación que representa una relación estructural entre el todo y sus
partes. Una agregación adiciona la semántica “es un” a una asociación. La
agregación es representada por un rombo vacío como en la siguiente figura.
17
2.2.4.7.3 Generalización Una generalización es una relación de
generalización o especialización en la que los objetos del elemento
especializado (el hijo o child en inglés) son sustituibles por los objetos del
elemento generalizado (el padre o parent en inglés). De esta manera, la
clase hija comparte la estructura y el comportamiento de la clase padre.
El siguiente diagrama nos muestra a la izquierda un ejemplo de herencia
simple y a la derecha un ejemplo de herencia múltiple.
18
2.2.4.8 Estereotipos
19
nuevas propiedades definidas para esos estereotipos.
2.2.4.9 Interfaces
20
restringir el uso de ciertas funciones UML. Por ejemplo, UML le permite
modelar herencia múltiple, pero JAVA permite sólo herencia simple. Puede
elegir restringir a los desarrolladores el modelado de la herencia múltiple (lo
que hace que sus modelos dependan del lenguaje) o desarrolle modismos que
transformen estas características en el lenguaje de implementación (lo que
hace que el mapeo sea más complejo).
• Utilice valores etiquetados para especificar su idioma de destino. Puede
hacerlo a nivel de clases individuales si necesita un control preciso. También
puede hacerlo en un nivel superior, como en los paquetes.
• Usa herramientas de ingeniería directa para los modelos.
21
Capítulo 3
Programación orientada a
Objetos en C
22
3.2 La portabilidad y eficiencia del código escrito en
lenguaje C
23
programación orientada a objetos que hemos visto. Sin embargo, la siguiente
pregunta surge: ¿puede programarse orientado a objetos con un lenguaje
estructurado? Douglass compara esto a cuando antiguamente se hacían la
misma pregunta con respecto al lenguaje ensamblador, ¿podría ser utilizado
para programar en forma estructurada? La respuesta a ambas preguntas es
sí, tan sólo se debe buscar la forma de implementar los conceptos de cada
paradigma en el lenguaje deseado (Douglass 2010).
Cada uno de estos son parte de los diferentes aspectos que posee la OO:
análisis, diseño, programación, etc. Los distintos aspectos de la OO pueden
ser independientes entre sí. Esto significa:
1. Un diseño puede ser Orientado a Objetos, incluso si el programa resultante
no lo es (Madsen 1988).
2. Un programa puede ser Orientado a Objetos, incluso si el lenguaje en el
que está escrito no lo es (Madsen 1988).
3. Un programa Orientado a Objetos puede ser escrito en casi cualquier
lenguaje, pero un lenguaje no puede ser asociado con la orientación a objetos
amenos que promueva programas Orientados a Objetos (Stroustrup 1991).
24
al programador escribir programas OO (Stroustrup 1991). La opinión de
Bjarne Stroustrup es la más popular afirmando que un lenguaje que permita
escribir programas OO no debería ser considerado para tal función si se
requiere de un esfuerzo excepcional para hacerlo (Stroustrup 1991). Esta
opinión es un tanto injusta, ya que un lenguaje es solo una herramienta
en el conjunto de las que pueden utilizarse para crear programas OO. Las
dificultades que presenta el lenguaje para expresarse en una manera OO
puede ser suplida por una o varias herramientas (Hendrickx 2004).
25
un generador de código desde diagramas de clase UML como facilitador para
dicha codificación.
26
embebidos involucran el desarrollo de hardware electrónico y mecánico
simultáneamente con el software. Esto introduce un desafío especial para
el desarrollador que debe elaborar el software solamente sobre la base
de especificaciones de prediseño, de cómo el hardware debería trabajar.
Muy frecuentemente, esas especificaciones son nada más que nociones de
funcionalidad, haciendo imposible crear el software correcto hasta que el
hardware es entregado.” (Douglass 2010). Estos problemas pueden ser
atacados eficientemente con un diseño orientado a objetos. Una inversión
en la cadena de dependencias posibilita que la implementación del negocio
a resolver sea independiente del hardware. A la vez, identificar por dónde
vendrán los cambios en las capacidades del sistema o sus optimizaciones,
junto con un buen diseño orientado a objetos permite que el impacto en el
código original por los cambios sea minimizado, reduciendo los costos de
dichos cambios.
Además de esto, para tener un testing efectivo se debe pensar en un diseño
orientado a las pruebas. Bruce Powel Douglass, gurú de diseño y procesos
para sistemas embebidos, recomienda utilizar TDD (desarrollo dirigido
por las pruebas) (Douglass 2010). James W. Grenning es autor de “Test
Driven Development for Embedded C”, un libro dedicado a este asunto para
sistemas embebidos. Él nos escribe: “El componente central de TDD son
las pruebas unitarias, sin un diseño modular que permita descomponer el
sistemas en partes no sería esto posible” (Grenning 2011). La modularidad
es un elemento básico en la orientación a objetos contenida en el concepto
de encapsulamiento.
Toda propuesta de cómo implementar características de orientación a
objetos en C embebido debe seguir la normativa de no hacerlo a costa de
un obligado incremento en el costo de hardware. Al mismo tiempo, para
sistemas complejos y menos restringidos se debería permitir contar con más
facilidades para el desarrollador para que haga frente a tal complejidad con
un menor costo de diseño, de ser necesario cediendo algo de eficiencia.
Cualquier avance en los procesos de desarrollo de software embebido en
C implicará un importante impacto en la mayoría de los desarrollos en
sistemas embebidos actuales.
27
3.6 Definiciones en COO
struct A{
char * name;
};
struct B{
struct A a;
int age;
};
void doSomethig(struct A * a);
/*...*/
struct B b;
doSomethig((struct A*) &b);
28
agregando más capas de herencia esto se vuelve muy molesto para el
programador. Hay distintas maneras de mitigar este problema, por ejemplo
codificando macros o funciones que manipulen dichos datos a partir de
referencias a las estructuras.
En el capítulo siguiente veremos otras dos formas de resolver esto una
utilizando estructuras anónimas de ISO C11 y la otra utilizando macros de
listas de los miembros de las estructuras.
29
Además introduciremos 2 frameworks (Dynace y COS) que utilizan otro
enfoque para el soporte del polimorfismo, la de las funciones genéricas
que introducimos en el capítulo anterior. Bajo este enfoque, la función
encargada de recibir el mensaje al objeto (el despachador) es la que contiene
las referencias a función a ser ejecutadas para cada tipo de clase. Esto se
consigue registrando cada función de implementación en el dispatcher.
En general, existe una función que representa a cada mensaje que puede ser
aplicado a todos o algunos objetos. La misma es encargada de ejecutar la
función correspondiente para el tipo de clase del objeto. Esta función es
llamada despachador o selector (o en inglés dispatcher o selector).
30
Capítulo 4
31
1. La de Ben Klemens en su libro 21st Century C (Klemens 2013)
2. SOOPC de Miro Samek (Samek 2015)
3. OOPC de Laurent Deniau (Deniau 2001)
4. OOC de Tibor Miseta (Miseta 2017)
5. OOC-S de Laurent Deniau (Deniau 2007b)
6. OOC de Axel Tobias Schreiner (A. T. Schreiner 1993)
7. GObject de glib (GLib 2019)
8. Dynace de Blake McBride (McBride 2004)
9. COS de Laurent Deniau (Deniau 2009).
Los modelos de objetos nos muestran como están organizados los datos que
dan soporte a la implementación de los conceptos de orientación a objetos
de cada framework.
Para los frameworks más adecuados para sistemas altamente restringidos
hemos incluido un análisis detallado de las estructuras que dan soporte al
framework. Esto permite evaluar al framework en su uso de memoria (RAM o
ROM) y uso de tiempo de CPU (que depende de la cantidad de indirecciones
existentes para llegar a los datos). Se han representado, con explicaciones,
en diagramas de clase UML.
32
4.2.1 Cómo leer los modelos de objetos
33
• Los arreglos se identifican bajo el estereotipo <<array>>. Un arreglo
posee un tipo. Los arreglos pueden componerse de instancias de su
tipo y definen los elementos del arreglo.
4.3 Codificación
34
4.4 Dificultades en la codificación
Para cada framework se dará una propuesta de expresión bajo UML. Con
la misma se busca mostrar que UML no posee las dificultades estudiadas.
Además nos servirá como base para el estudio de generación de código desde
UML para estos frameworks. Las propuestas de expresión en UML estarán
basadas en la definición de UML 2.5.1 dadas por la OMG (OMG 2017).
35
4.6 Ben Klemens
4.6.1 Introducción
object->method(object);
36
La herencia representada en la figura como la relación Padre-Hijo se
consigue incluyendo una instancia del Padre en el comienzo de la instancia
de Hijo. De esta manera las funciones y métodos escritos para las instancias
de Padre se pueden utilizar para las instancias de Hijo.
No existe una estructura que contenga datos de clase, aunque sí se podría
referenciar a estos con punteros. Por ejemplo, si se desea definir una
variable de clase entera i todos los objetos podrían contener un puntero al
mismo llamado -por ejemplo- class_i. Lo mismo es aplicable a métodos
de clase. Comparado con un modelo de objetos basados en tabla virtual
como los siguientes 5, o como otros LOOP como C++ o JAVA, este modelo
generalmente presentará un mayor consumo de memoria RAM ya que todas
las referencias a métodos son copiadas individualmente en la estructura de
la instancia. Por otro lado, ejecutar estos métodos polimórficos consumirá
menos tiempo ya que no es necesario desreferenciar la tabla virtual. Además,
las referencias individuales se pueden utilizar para cambiar los métodos a
ejecutar en tiempo de ejecución en base al estado del objeto, lo que puede
describir más limpiamente el comportamiento dependiente de estados como
sucede con lenguajes prototipados como JavaScript.
37
4.6.4 Codificación
//parent.h
typedef void (*method_p_t)(parent*self);
typedef struct parent{
int private_attribute1;
int attribute2;
method_p_t method;
}parent;
parent * parent_new();
parent * parent_copy(parent * parentToCopy);
void parent_free(parent * parentToFree);
//parent.c
static void method(parent * self){
//do something
}
parent * parent_new(){
38
parent * out = malloc(sizeof(parent));
*out = (parent){ .method = method, .private_attribute1 = 0,
.attribute2 = 0};
return out;
}
El siguiente código nos muestra como definir una clase que herede de otra.
//child.h
typedef struct child{
union{
struct parent;
struct parent asParent;
};
39
//new attributes and polymorphic methods...
}child;
//new(),copy(),free() and other non polymorphic methods...
//parent.h
#define _method(_parent) _Generic((_parent), \
parent:_parent -> method ( _parent ), \
default:_parent -> method ( _parent.asParent ) )
_Generic es una nueva facilidad de C11 que permite evaluar una variable
de acuerdo a su tipo. Con esta macro, para enviar el mensaje method() a
una instancia de parent o a un objeto que herede de parent (por ejemplo
aChild), no se necesita más que llamar a _method(aChild). Si el argumento
que pasamos no es un parent o una clase heredada del mismo entonces
obtendremos un error en tiempo de compilación por no encontrarse el
miembro asParent en su estructura.
40
//child.c
static void method(parent * self){
//do something
}
child * child_new(){
child * out = (child *) parent_new();
out -> method = method;
// other initialization for child
return out;
}
Tipo de
# dificultad Nombre Descripción
41
Tipo de
# dificultad Nombre Descripción
42
4.6.6 Propuesta de expresión en UML
El diagrama UML que puede contener estos conceptos es directa, ya que son
los más básicos para la orientación a objetos.
El siguiente diagrama los expresa de forma correcta.
4.7.1 Introducción
OOPC fue ideado por el autor como una forma simple de representar los
conceptos centrales de la POO en APIs escritas en C. Busca ocultar la
43
dificultad de implementar POO en C para el desarrollador de la aplicación.
Es compatible con el estándar ISO C89.
44
virtual heredada de Parent, referencia a su propia tabla virtual del mismo
tipo que la tabla virtual de Parent. Si bien el autor de esta especificación
no lo contempla, si la clase Child agregara más métodos virtuales para ella
y para sus clases heredadas la referencia a la tabla virtual de Child podría
ser un tipo heredado de ParentVtbl, una ChildVtbl, al igual que Child
hereda de Parent.
4.7.4 Codificación
/*Parent.h*/
typedef struct {
void (*method)(const Parent * self);
/*más métodos o variables de clase aquí*/
} ParentVtbl;
/*Parent.h*/
typedef struct {
const ParentVtbl * vptr;
/*variables de instancia van aquí*/
} Parent;
45
/*Parent.c*/
void Parent_metodo_(Parent * self)
{
/*cuerpo del método*/
}
Las implementaciones de los métodos terminan con guión bajo(_), esto es así
para diferenciarlo de las funciones que llaman a dichos métodos (dispatchers).
Los dispatchers hacen las llamadas a los métodos referenciados por las
tablas virtuales, y son los utilizados por los usuarios de las clases. Hay tres
alternativas para implementarlos: como una función, como una función
“inline” (en caso de trabajar con C99) o como una macro. A continuación
la presentaremos como función:
/*Parent.c*/
void Parent_metodo(Parent * self)
{
self -> vptr -> metodo (self);
}
/*Child.h*/
typedef struct {
46
Parent super;
/* más variables de instancia van aquí*/
} Child;
/*Child.c*/
void Child_metodo_(Parent * self)
{
/*cuerpo del método*/
}
47
Tipo de
# dificultad Nombre Descripción
48
Tipo de
# dificultad Nombre Descripción
49
Para especificar que el método polymorphic_method es polimórfico la
propiedad isLeaf debe ser falsa de acuerdo a la definición de UML (OMG
2017). La herencia se indica como de costumbre en UML. Para especificar
en el diagrama que Child contiene su propia implementación del método
polymorphic_method se lo vuelve a incluir como miembro de Child. Para
que el atributo class_attribute sea de clase (o sea esté referenciado a
través de la tabla virtual por todas las instancias de la clase), la propiedad
isStatic debe ser verdadera de acuerdo a la definición de UML (OMG
2017). Cualquier especificación de visibilidad en los miembros de la clase
o instancia son ignorados exceptuando los métodos no polimórficos que
pueden ser públicos (incluidos en el archivo header de la clase) o privados
(no incluidos en el archivo header de la clase). Ninguna de las dificultades
enumeradas en la sección anterior aplican para este diagrama.
4.8.1 Introducción
50
tecnología de desarrollo o para no depender de la API de C++), aunque
también presenta facilidades adicionales bajo el estándar ISO C99, y además
provee interesantes facilidades para depurar.
1. Encapsulamiento
2. Herencia (múltiple)
3. Polimorfismo
4. Genericidad
5. Excepciones
51
Analizaremos los cuatro elementos del diagrama:
* La unión, representado con el estereotipo U, t_Parent contiene todos
los datos de instancia dentro de una estructura sin identificador de tipo
y anidada m y una referencia a su tabla virtual mediante __vptr. Cada
instancia de la clase Parent significará una instancia de la estructura
52
t_Parent.
* La tabla virtual _ooc_vtbl_Parent es una instancia de la estructura
_ooc_vtbl_Parent, y contiene los métodos polimórficos de instancia de la
clase Parent. Contiene una instancia de la estructura _ooc_vtbl_object
bajo el símbolo _, que contiene las variables offset e info. info es una
referencia a la estructura _ooc_info_Parent que contiene la información de
la clase Parent (RTTI). offset es utilizada, como veremos, para manejar
la herencia múltiple.
* La estructura de información de tipo _ooc_info_Parent contiene la
información de la clase Parent, su nombre en name, una instancia de
t_Parent constante no inicializada pero con referencia a la tabla virtual en
obj, y una referencia la estructura Parent.
* La estructura Parent es una instancia de _ooc_class_Parent, contiene
los miembros de clase (atributos y métodos de clase) y métodos de instancia
no polimórficos. Entre los métodos de clase generados están Parent() que
inicializa la clase y retorna una instancia constante inicializada con valores
por defecto y con su referencia a la tabla virtual (_ooc_obj_Parent), el
destructor de instancia _Parent(), y el instanciador alloc() que retorna
una copia en heap de _ooc_obj_Parent. Es común en esta estructura
definir métodos de clase como new, copy e init. La referencia a la tabla
virtual __vptr permite llamar a los métodos polimórficos de Parent en
forma no polimórfica cuando el tipo del objeto es conocido, además permite
que la clase misma (y no una instancia de la misma) sea tratada como un
objeto (al contener métodos de clase polimórficos redefinibles por las clases
heredadas).
El siguiente diagrama muestra las estructuras que dan soporte a una clase
Child que hereda de Parent.
53
En gris se encuentran los elementos ya presentados para la clase Parent.
La unión t_Child incluye a la unión t_Parent como primer elemento.
54
Esto, como ya hemos visto en los frameworks anteriores, permite que las
instancias de Child (o t_Child) puedan ser vistas como instancias de
Parent (o t_Parent) para los métodos definidos para Parent.
La tabla virtual _ooc_vtbl_Child contiene la estructura de la tabla
virtual de Parent (_ooc_vtbl_Parent) como primer elemento de su
estructura. Esto también es necesario para que una instancia de Child
pueda ser considerada una de Parent conteniendo los mismos métodos
polimórficos (aunque pueden redefinirse para Child). Luego de la estructura
_ooc_vtbl_Parent vienen los métodos polimórficos definidos especialmente
para Child.
La estructura de RTTI es análoga a la de la clase base Parent pero el
atributo super es inicializado con la estructura de RTTI de Parent. Esto
permite realizar casteos dinámicos donde no se conoce la clase del objeto.
55
Podemos apreciar que la estructura t_Child está compuesta primero por la
estructura t_Parent continuada por la estructura t_Parent2.
Cada una de sus referencias a una tabla virtual (la contenida en t_Parent y
la contenida en t_Parent2) referencia a una tabla diferente. t_Parent2
en t_Child referencia a una tabla virtual con la misma estructura a la
de cualquier instancia de una clase Parent2 y con la misma referencia a
la estructura RTTI de un Parent2. Pero existe una gran diferencia con
cualquier instancia de una clase Parent2, el atributo offset guarda donde
se encuentra la estructura t_Parent2 en t_Child, esta información permite
que al pasar una referencia de Parent2 en una instancia de Child (por
ejemplo a una función que espera recibir un Parent2 como parámetro) se
pueda referenciar, a la instancia de Child que la contiene, en tiempo de
ejecución (casteo dinámico). Por otro lado t_Parent en t_Child referencia
a los mismas instancias que si tan solo hubiese una herencia simple entre
56
t_Parent y t_Child, la única diferencia es que en la estructura de RTTI se
indica mediante el atributo extraSuper la cantidad de herencias por encima
de la simple y en extraOffset la posición relativa de las mismas dentro de
la estructura t_Child.
4.8.4 Codificación
OOPC facilita varias macros para la codificación y uso de las clases mediante
la inclusión del archivo ooc.h
//Parent.h
#undef OBJECT
#define OBJECT Parent
/* Object interface */
BASEOBJECT_INTERFACE
BASEOBJECT_METHODS
void constMethod(print);
ENDOF_INTERFACE
/* Class interface */
CLASS_INTERFACE
57
void method_(init) char const name[] __;
void method_(copy) t_Parent const*const parent __;
int class_attr;
ENDOF_INTERFACE
58
4.8.4.1.2 Implementación (.c)
//Parent.c
// include de otros módulos
#define IMPLEMENTATION
#include <Parent.h>
void
constMethodDecl(print)
{
// print implementation
}
BASEOBJECT_IMPLEMENTATION
methodName(print)
ENDOF_IMPLEMENTATION
/*
--------------------
Class implementation
--------------------
*/
initClassDecl() /* requerido */
{
/* código de inicialización de la clase */
objDefault(attribute) = 1;
}
dtorDecl() /* requerido */
{
/* código del destructor de instancias */
59
}
t_Parent
classMethodDecl_(*const new) char const name[] __
{
/* código de método new */
return this;
}
void
methodDecl_(init) char const name[] __
{
/* código de método init */
}
void
methodDecl_(copy) t_Parent const*const per __
{
/* código de método copy */
}
CLASS_IMPLEMENTATION
methodName(new),
methodName(init),
methodName(copy),
0 /*class_attr=0*/
ENDOF_IMPLEMENTATION
60
implementacion interfaz
classMethodDecl() classMethod()
classMethodDecl_() classMethod_()
methodDecl() method()
methodDecl_() method_()
constMethodDecl() constMethod()
constMethodDecl_() constMethod_()
//Child.h
#include <Parent.h>
61
#include <Parent2.h>
#undef OBJECT
#define OBJECT Child
/* Object interface */
OBJECT_INTERFACE
INHERIT_MEMBERS_OF (Parent);
INHERIT_MEMBERS_OF (Parent2);
int private(child_attribute);
OBJECT_METHODS
INHERIT_METHODS_OF (Parent);
INHERIT_METHODS_OF (Parent2);
void method(child_method);
ENDOF_INTERFACE
/* Class interface */
CLASS_INTERFACE
t_Child*const classMethod_(new)
char const name[]__;
void method_(init)
char const name[]__;
void method_(copy) t_Child const*const child __;
ENDOF_INTERFACE
62
métodos propios de Child. La declaración de los miembros de clase es la
misma que la de una clase base.
//Parent.c
#define IMPLEMENTATION
#include "Child.h"
void
methodDecl(child_method)
{
/*código para child_method()*/
}
void
constMethodOvldDecl(print, Parent)
{
/*código para redefinición de print()*/
}
OBJECT_IMPLEMENTATION
SUPERCLASS (Parent),
SUPERCLASS (Parent2),
methodName(child_method)
ENDOF_IMPLEMENTATION
initClassDecl()
{
/* inicializar clases padre */
initSuper(Parent);
initSuper(Parent2);
63
/* redefinición de métodos */
overload(Parent.print) =
methodOvldName(print, Parent);
dtorDecl()
{
/*Liberación de recursos de Child*/
Parent._Parent(super(this,Parent));
Parent2._Parent2(super(this,Parent2));
}
/*...*/
void
methodDecl_(init)
char const name[]__
{
Parent.init(super(this,Parent), name);
Parent2.init(super(this,Parent2));
/*más código de inicialización*/
}
CLASS_IMPLEMENTATION
methodName(new),
methodName(init),
methodName(copy)
ENDOF_IMPLEMENTATION
64
La siguiente tabla muestra las macros para las declaraciones en las clases
base y su equivalente para su redefinición en las clases heredadas:
methodDecl() methodOvldDecl()
methodDecl_() methodOvldDecl_()
constMethodDecl() constMethodOvldDecl()
constMethodDecl_() constMethodOvldDecl_()
65
deben ser reemplazadas por 0.
//array.h
/* integer array */
#define gTypePrefix i
#define gType1 int
#include <g_array.h>
#undef gType1
#undef gTypePrefix
/* double array */
#define gTypePrefix d
#define gType1 double
#include <g_array.h>
66
#undef gType1
#undef gTypePrefix
//array.c
/* integer array */
#define gTypePrefix i
#define gType1 int
#include <g_array.c>
#undef gTypePrefix
#undef gType1
/* double array */
#define gTypePrefix d
#define gType1 double
#include <g_array.c>
#undef gTypePrefix
#undef gType1
//g_array.h
#if defined(GENERIC) || !defined(G_ARRAY_H)
#define G_ARRAY_H
/*...*/
#undef OBJECT
#define OBJECT GENERIC(array)
67
/* Object interface */
OBJECT_INTERFACE
INHERIT_MEMBERS_OF(GENERIC(memBlock));
gType1* private(data);
OBJECT_METHODS
INHERIT_METHODS_OF(GENERIC(memBlock));
gType1* method(getData);
/*...*/
ENDOF_INTERFACE
CLASS_INTERFACE
/*...*/
ENDOF_INTERFACE
#endif
68
Tipo de
# dificultad Nombre Descripción
69
Tipo de
# dificultad Nombre Descripción
70
Tipo de
# dificultad Nombre Descripción
71
De acuerdo al manual de referencia de UML (OMG 2017), los atributos
y métodos privados se marcan con el atributo visibility en private, y
los públicos con public, si son constantes entonces el atributo isReadOnly
debe ser true y si son de clase isStatic (por ejemplo class_attr). Los
métodos constantes contienen el atributo isReadOnly (por ejemplo print())
en true, los abstractos isAbstract (no se genera implementación para el
método), los no polimórficos isLeaf (por ejemplo init() y copy()) y los
de clase isStatic (por ejemplo new() ). Además, las clases pueden ser
abstractas (no proveen el método alloc()) con el atributo isAbstract en
true este podría ser el caso por ejemplo de Parent2 (manteniendo el ejemplo
de codificación dado). Todo esto se puede especificar para OOPC.
72
La clases genéricas pueden heredarse por otras. También pueden ser
instanciadas en clases concretas asignando un tipo concreto a sus tipos
genéricos. En el ejemplo, la relación entre gChild y ip_gChild o
dp_gChild no es de herencia sino de vinculación de elementos genéricos
(template binding), donde se asigna el tipo concreto. Las clases vinculantes
(ip_gChild y dp_gChild) no pueden declarar miembros nuevos, pero sí
lo podría hacer una clase que herede de cualquiera de ellas (esto permite
no tener que obligar esa herencia para permitir la declaración de nuevos
miembros como lo permite UML). Las clases vinculantes deben terminar
con el nombre de la clase genérica siendo el prefijo restante el asignado a la
macro gTypePrefix. Ninguna de las dificultades enumeradas en la sección
anterior aplican para estos diagramas.
4.9.1 Introducción
73
Está escrito bajo ISO C89. Junto con la biblioteca del sistema de objetos y
macros para facilitar la codificación, provee una herramienta para facilitar
la creación de clases, interfaces y mixins desde templates u otras clases
ya implementadas. El autor escribe acerca de la codificación de estos
artefactos:
“Crear clases en ooc escribiéndolas desde cero puede requerir mucho
trabajo, es propenso a errores, pero principalmente aburrido”[Tibor2017].
Lamentablemente esta herramienta no facilita la inicialización de la tabla
virtual ni la implementación de miembros de clase o instancia como sí lo
puede hacer un generador de código desde UML.
1. Encapsulamiento
2. Herencia
3. Polimorfismo
4. Interfaces (sin herencia de interfaces)
5. Mixins (sin herencia de mixins)
6. Excepciones
4.9.3.1 Clases
El siguiente diagrama nos muestra las estructuras que dan soporte a las
clases en ooc.
74
La estructura base de la cual todas las estructuras de instancia de clases
heredan es BaseObject. Como en los frameworks anteriores, la herencia se
consigue incluyendo a la estructura de la clase padre como primer miembro
de la estructura de la clase derivada.
BaseObject contiene una referencia a una tabla virtual (con estructura
BaseVtable_stru), de las cuales también heredan las tablas virtuales
de otras clases para declarar sus métodos polimórficos. La tabla virtual
referencia a la estructura que representa a la clase y que sirve de estructura
de RTTI, contiene los constructores y destructores de clase (para instanciar
y liberar la clase) y de instancia. El nombre de la clase y su tipo (para una
clase _OOC_TYPE_CLASS). También incluye un listado de interfaces y mixins
(qué es un mixin se explicará a continuación) a través de la referencia
al arreglo itable, el mismo nos dice donde se encuentran los métodos
75
definidos para la interfaz dentro de la tabla virtual (a través del atributo
vtab_offset) y en caso de tratarse de un mixin, dónde se encuentran sus
datos dentro de la estructura de la instancia de clase (a través del atributo
data_offset). Qué mixin o interfaz se está implementando se asigna en la
referencia id.
76
ParentVtable_str con BaseVtable_str que le permite usarse como una
tabla virtual y extenderse definiendo nuevos métodos de instancia y métodos
y atributos de clase. Esta tabla virtual referencia a su propia instancia de la
estructura Class donde se definen miembros comunes a todas las clases. El
atributo parent de la clase, que referencia a la clase padre de la clase Parent,
referencia a BaseClass lo que indica que esta es una clase base. Una clase
que herede de Parent directamente tendría una referencia a ParentClass
en este atributo.
4.9.3.2 Interfaces
Las estructuras en instancias que dan soporte a una interfaz son muy
sencillas. El siguiente diagrama nos muestra un ejemplo con una interfaz
llamada Serializable
77
4.9.3.3 Mixins
Los mixins son muy parecidos a las clases pero en vez de instanciarse, son
heredados por una clase. A la vez agregan una interfaz a la clase contenedora
del mixin por lo que son como una Interfaz que define parcial o totalmente
su implementación. El siguiente diagrama nos muestra las estructuras e
instancias que dan soporte a un mixin llamado Countable (podría llamarse
de cualquier otra forma).
78
Al igual que una interfaz, se define una estructura terminada en “Methods”
(en nuestro caso CountableMethods) que a su vez define las referencias
a los métodos definida para el mixin, y a diferencia de las interfaces se
define una estructura de variables (CountableFields_) definida para el
mixin. La instancia de MixinTable (CountableID) es análoga a la estructura
ClassTable de una clase. Incluye la información RTTI del Mixin. Esto
permite identificar las instancias de las estructuras CountableMethods y
CountableFields_ al asociarlas con CountableID. Al igual que una clase, un
mixin contiene, métodos de inicialización y finalización del mixin, y métodos
de inicialización, copia y finalización de instancia. Las instancias de mixins
son componentes de las clases que las heredan.
79
El siguiente diagrama nos muestra cómo quedarían las estructuras de Parent
al heredar el mixin Countable
80
también bajo el nombre Countable, al igual que con una interfaz.
El arreglo ParentItable contiene una instancia de la estructura
InterfaceOffsets_struct, la misma relaciona a la tabla que describe al
mixin (CountableID) con la posición del mixin dentro de la tabla virtual
(vtab_offset) y la posición del mixin dentro de las variables de instancia
(data_offset). De esta manera desde la estructura de RTTI de la clase
Parent se puede obtener.
4.9.4 Codificación
4.9.4.1 Visibilidad
4.9.4.2 Clases
//Parent.h
DeclareClass( Parent, Base );
81
de las interfaces (tanto interfaces comunes como mixins) a través de la macro
Interface:
//Parent.h
Virtuals( Parent, Base )
void (* method) ( Parent );
int classVariable;
Interface( MyMixin );
Interface( MyInterface );
EndOfVirtuals;
//implement/Parent.h
ClassMembers( Parent, Base )
int instanceVariable;
MixinData(MyMixin);
EndOfClassMembers;
//Parent.c
InterfaceRegister( Parent )
{
AddMixin( Parent, MyMixin ),
AddInterface( Parent, MyInterface )
};
82
//Parent.c
AllocateClassWithInterface( Parent, Base );
Existen una variedad de funciones que deben implementarse para cada clase:
inicialización y finalización de la clase, constructor y destructor de instancia
y constructor de copias. En la inicialización de la clase se debe inicializar la
tabla virtual con la implementación de los métodos declarados en la misma
(sólo para los métodos en los que la clase defina o especialice al método, los
métodos heredados son automáticamente inicializados en la tabla virtual por
los ancestros de la clase):
static
void
Parent_initialize( Class this )
{
ParentVtable vtab = & ParentVtableInstance;
((ParentVtable)vtab)->method = Parent_method;
((ParentVtable)vtab)->MyInterface.myInterfaceMethod =
(void(*)(Object self))Parent_myInterfaceMethod;
4.9.4.3 Interfaces
//MyInterface.h
DeclareInterface( MyInterface )
void (*myInterfaceMethod)(Object self);
EndOfInterface;
83
4.9.4.3.2 Implementación(.c) Por último se debe instanciar la
estructura de RTTI de la interfaz en cualquier archivo, por ejemplo uno
particular para la interfaz:
//MyInterface.c
AllocateInterface( MyInterface );
4.9.4.4 Mixins
Se componen de los mismos tres archivos que las clases (o sea uno de
implementación y dos de interfaz).
//MyMixin.h
DeclareInterface( MyMixin )
unsigned int (*myMixinMethod)(Object self);
EndOfInterface;
//implement/MyMixin.h
MixinMembers( MyMixin )
unsigned int instanceNum;
EndOfMixinMembers;
84
//MyMixin.c
AllocateMixin( MyMixin );
Junto con esta macro deben implementarse varias funciones para el mixin:
inicialización y finalización de la estructura de RTTI del mixin, constructor,
constructor copia y destructor de la instancia del mixin en la clase (estas se
llaman automáticamente al crear, copiar o destruir la instancia de la clase)
y por último un método populate para actualizar la tabla virtual de la clase
con los métodos que vayan a implementarse en el mixin mismo:
//MyMixin.c
static
void
MyMixin_populate( MyMixin mymixin )
{
mymixin->myMixinMethod = MyMixin_myMixinMethod;
}
Tipo de
# dificultad Nombre Descripción
85
Tipo de
# dificultad Nombre Descripción
86
Tipo de
# dificultad Nombre Descripción
ooc ofrece una herramienta para crear clases en base a plantillas para mitigar
muchas de estas dificultades (específicamente las dificultades número 3 y 5
y en parte la 4)
87
4.9.6 Propuesta de expresión en UML
Cualquier clase puede realizar una o varias interfaces, heredar uno o varios
mixins, y heredar de hasta una clase. Todas las combinaciones de estas
posibilidades son permitidas. Cualquier método en una clase o mixin puede
definirse como polimórfico o no (mediante el atributo isLeaf), si es de clase
o no (mediante el atributo isStatic), si es constante o no (mediante el
atributo isConst), y si es abstracto o no (mediante el atributo isAbstract).
Las interfaces sólo definen métodos abstractos, pero pueden ser definidos
como constantes. Los mixins se representan mediante una interfaz. La
misma debe contener atributos o el estereotipo << mixin >>. De esta manera
se distinguen de las interfaces. Los atributos de una clase o mixin pueden ser
de clase (mediante el atributo isStatic) y constantes (mediante el atributo
isConst). Los atributos de visibilidad son ignorados. Las opciones de
visibilidad para ooc ya fueron explicadas en la sección Visibilidad.
88
4.10 Object Oriented C - Simplified (OOC-S) de
Laurent Deniau
4.10.1 Introducción
1. Encapsulamiento
2. Herencia
3. Polimorfismo
4. Interfaces
89
4.10.3 Modelo de objetos
90
object, referencia a una tabla virtual (por ejemplo parent_interface)
que puede definir nuevos métodos así como incluir interfaces (como
interface_example), las interfaces son referenciadas mediante una
única instancia a la interfaz cuyo nombre es el de la interfaz seguido
de interface (como interface_example_interface). Para obtener
la interfaz implementada en un objeto (por ejemplo la interfaz
interface_example implementada por parent) se realiza una llamada
al método interface() con la referencia a la interfaz. Por ejemplo
para obtener la implementación de la interfaz interface_example de
la clase del objeto referenciado por una variable obj se debería ejecutar
obj->i->interface(obj,interface_example_interface). Entonces la
implementación dada por el usuario del método interface() debe, al recibir
como segundo argumento la instancia interface_example_interface,
devolver la instancia de interface_example dentro de su tabla virtual.
4.10.4 Codificación
4.10.4.1 Clases
91
#include "object.h"
#include "interface_example.h"
#define PARENT_INTERFACE \
OBJECT_INTERFACE \
INTERFACE_EXAMPLE_INTERFACE \
void (*method1)(TYPE*);
TYPE {
INTERFACE *i;
};
INTERFACE {
struct object_interface *super;
PARENT_INTERFACE
};
#undef TYPE
#undef INTERFACE
92
4.10.4.1.3 Interfaz de implementación (_p.h)
Esta interfaz es la que provee la visibilidad de implementación
haciendo visibles los atributos de la clase. La siguiente interfaz
sigue al ejemplo anterior.
#include "parent.h"
#include "object_p.h"
struct parent_attribute {
struct object_attribute object;
/* atributos aquí */
};
#include "parent_p.h"
Luego se crea una instancia privada de la tabla virtual de la clase, que puede
ser inicializada y obtenida mediante la función parent_interface()
93
dirección del primer método implementado. En caso de que esta clase no
realice la interfaz consultada se reenvía la consulta su padre lo que esto
continúa hasta llegar a object que retorna con valor NULL al no realizar
ninguna interfaz.
static void*
parent_interfaces(TYPE* self, void *ref_i)
{
if (ref_i == &interface_example_interface)
return &self->i->method2;
return interface.super->interface(SUPER,ref_i);
}
static TYPE*
parent_init(TYPE *self)
{
interface.super->init(SUPER);
/* inicialización de atributos */
return self;
}
static void
method1(TYPE *self){}
static void
method2(TYPE *self){}
94
de la función parent_interface() y en caso de no estar inicializada se
llama a parent_initialize(). Primero se hace una copia de la tabla
virtual del padre, en este caso object, en la tabla virtual de la clase, esto
permite heredar el comportamiento del padre (la implementación de sus
métodos). Luego se realiza una copia de las interfaces por defecto en la
posición de la interfaz en la tabla virtual, esto permite heredar los métodos
por defecto definidos por la interfaz. Luego a la tabla virtual de la clase se
le asignan las implementaciones específicas de los métodos de la clase que
pueden sobrescribir los métodos copiados anteriormente, además la referencia
a super, es decir, a la clase padre es sobrescrita con la correspondiente, en
este caso con object.
static void
parent_initialize(void)
{
struct object_interface *super_interface =
,→ object_interface();
interface.super = super_interface;
interface.interface = parent_interfaces;
interface.init = parent_init;
interface.method1 = method1;
interface.method2 = method2;
/* put parent methods initialization here */
}
Por último tenemos las funciones para obtener la tabla virtual y para
instanciar un objeto de la clase en memoria dinámica.
INTERFACE*
parent_interface(void)
95
{
if (!interface.interface)
parent_initialize();
return &interface;
}
TYPE*
parent_new(void)
{
TYPE *self = parent_interface()->alloc(sizeof *SELF);
assert(self);
self->i = &interface;
return parent_init(self);
}
4.10.4.2 Interfaces
Lo primero que define una interfaz es una macro con un listado de los
métodos miembro. Esta macro es incluida en la tabla virtual de las clases
que realicen la interfaz.
#define INTERFACE_EXAMPLE_INTERFACE \
void (*method2)(TYPE*); \
/* more interface_example methods here */ \
/* TYPE* (*method_example)(TYPE*); */
96
Luego se define un tipo para un objeto que realice la interfaz y una estructura
que representa la interfaz, es decir el conjunto de métodos que la componen.
TYPE {
struct interface *i;
};
INTERFACEINTERFACE
};
#undef TYPE
#undef INTERFACE
#include "interface_example.h"
97
static void method2(TYPE*arg){}
INTERFACE interface_example_interface=
{method2 }
;
Tipo de
# dificultad Nombre Descripción
98
Tipo de
# dificultad Nombre Descripción
99
4.10.6 Propuesta de expresión en UML
100
4.11 OOC de Axel Tobias Schreiner
4.11.1 Introducción
1. Encapsulamiento
2. Herencia
3. Polimorfismo
4. Reenvío de mensajes (message forwarding) / Delegados (Delegates)
5. Metaclases
6. Excepciones
101
El siguiente diagrama representa las estructuras que dan soporte al modelo
de objetos.
102
un método que se encarga del reenvío de mensajes (forward()) como se
explicará. Una clase a la vez hereda de Object, por lo que la convierte en
un objeto que tiene su propia referencia a su clase. Esta última clase (la
clase de una clase) se llama una metaclase y contiene los mismos métodos
polimórficos por defecto que ya nombramos. Esto significa, por ejemplo, que
a través de una metaclase puede instanciarse una nueva clase mediante el
método new(). Este método permite redefinir los métodos que se deseen
para la nueva clase. Los métodos definidos en la metaclase son métodos de
clase.
103
En estas estructuras básicas es interesante hacer notar que la clase Object
desciende de sí misma y la clase de la metaclase Class es Class (lo que
significaría su meta-metaclase) y así continúan recursivamente. Esto es lo
que se llama un modelo de objetos uniforme, donde tanto las clases como los
objetos son objetos con el mismo uso (Deniau 2009). Cada método en una
clase se define con la estructura Method, la misma contiene un tag (bajo el
104
nombre tag), una implementación (method) y un selector (selector). El tag
permite referenciar a un método mediante una cadena de caracteres. Esto
permite crear módulos desacoplados pero que pueden interactuar entre sí
mediante convención de nombres. Esto se logra a través de un método no
polimórfico llamado respondsTo() que recibe como argumentos el objeto a
interrogar si posee un método con cierto tag y el tag mismo. La función
devuelta por respondsTo() es la del selector y no la de la implementación,
por eso se incluye al selector en esta estructura ya que, de lo contrario,
se lo puede llamar directamente por su prototipo declarado en el archivo de
interfaz. Los objetos que poseen métodos que se ajustan a las convenciones de
otros objetos, permitiéndoles a estos últimos utilizarlos, se llaman delegados
(delegates en inglés). El selector (o despachador, dispatcher en inglés) es
el encargado de seleccionar la implementación correcta del método al cual
representa y ejecutarlo. Para eso recibe como argumento el objeto que se
supone implementa el método en cuestión. En caso de que no lo implemente
se llama al método forward() del objeto con la información del mensaje que
se envió al método y desde ahí el objeto puede decidir reenviar el mensaje a
otro objeto o procesarlo de otra forma (por ejemplo imprimiendo un error y
terminando el programa). Esta capacidad de reenviar un mensaje hacia otro
método se llama message forwarding y provee una excepcional flexibilidad
al programador. La misma puede suplir la necesidad de la herencia múltiple
mediante la composición con otras clases y reenviando los mensajes a las
clases que implementan dichos mensajes.
Tanto las clases como las metaclases pueden heredarse. Heredar una
metaclase permite definir nuevos métodos y permitir instanciar nuevas
clases que implementen dichos métodos. El siguiente diagrama representa
las estructuras de una clase Child que hereda de Object y que define
también una nueva metaclase que hereda de Class.
105
Child incluye a Object como primer elemento lo que convierte a un
Child en un Object, la estructura Child puede contener más atributos de
instancia (data en el diagrama). Para definir nuevos métodos, se incluyen
en la estructura de la que se instancia que representa a la clase Child,
ChildClass, que no puede incluir atributos de clase (esto se debe a que
respondsTo() espera que haya solo métodos). La metaclase seguirá siendo
una instancia de la estructura Class pero cambiará principalmente su
constructor para construir instancias de la estructura ChildClass (una de
ellas la clase de Child misma). Si un nuevo objeto desea heredar de Child
pero no define nuevos métodos de instancia o métodos y atributos de clase,
entonces heredará de la estructura Child pero no precisa heredar de la
estructura ChildClass, ya que la metaclase de Child permite instanciar
una nueva clase con las implementaciones de los métodos definidos para la
nueva clase (al igual que sus tags).
106
4.11.4 Codificación
4.11.4.1 Visibilidad
//Child.r
# include "Object.r"
107
struct ChildClass { const struct Class _;
struct Method myMethod;
/*more methods here*/
};
//Child.c
int Child_myMethod (void * _self, void * anObject) {
/*...*/
}
if (isOf(class, ChildClass())
&& class -> myMethod.method) {
cast(Object(), anObject);
108
result =
((struct Object * (*) ()) class -> myMethod.method)
(_self, anObject);
} else
forward(_self, & result, (Method) add, "add",
_self, anObject);
return result;
}
cast(Object(), anObject);
109
super_forward(Child(), _self, result, selector, name,
,→ app);
}
110
}
111
4.11.5 Dificultades en la codificación
Tipo de
# dificultad Nombre Descripción
112
Tipo de
# dificultad Nombre Descripción
113
Tipo de
# dificultad Nombre Descripción
114
4.11.6.1 Propuesta de expresión con metaclases
explícitas
115
instancia de las clases que son instanciadas de dicha metaclase. Una
clase que provea una implementación para alguno de dichos métodos debe
incluir al método como una operación propia. Una clase que no posea
una implementación ni propia ni heredada de alguno de los métodos de
la metaclase de la cual se instancia, es una clase base (en el ejemplo
la clase Child no implementa el método method()) y alguna clase hija
puede proveer una implementación para los mismos (en el ejemplo la
clase GrandChild define una implementación de method()). Los métodos
ctor(), dtor(), puto(), new(), delete() y forward() definidos en una
clase son considerados implementaciones de los métodos definidos por
la metaclase Class. A las implementaciones de métodos polimórficos se
les puede aplicar el estereotipo <<tag>>. El mismo contiene un atributo
nombre que lo representa (el mismo lo referencia para obtenerlo a través
del método respondsTo()). Si el nombre estuviese vacío se le asignaría el
nombre del método. Las variables de instancia (data) pueden ser de clase
(lo que significa que se instancian en el archivo de implementación de la
clase (.c)) y constantes. Los métodos no polimórficos se implementan en
el archivo de implementación de la clase (.c) y si tienen visibilidad pública
se declaran en el archivo de interfaz de la clase. De acuerdo a ooc los
constructores reciben sus argumentos en una lista de argumentos variables,
sin embargo su representación es con una lista de argumentos fija. Para
especificar la capacidad de procesar un mensaje definido por una metaclase
(que no pertenece a la jerarquía de metaclases de la metaclase que se realiza)
a través del método forward() se utiliza un estereotipo que extiende la
relación usage y se llama <<forwards>>. El final de dicha relación (el
proveedor, supplier en inglés) es el método mismo y no la metaclase que
lo contiene. Se escogió la relación usage ya que según la especificación no
define el uso que el cliente da al proveedor (OMG 2017), pero mediante
el estereotipo sí lo especificamos. También se puede declarar el método
forward() dentro de una clase para implementarlo (como es el caso en
GrandChild).
116
4.11.6.2 Propuesta de expresión con metaclases
implícitas
Las clases que definan nuevos métodos polimórficos (es decir, que una clase
padre no lo haya ya definido), generan metaclases con el nombre de la clase
seguido por Class, además implican selectores y selectores de clase padre
(super class selector). En caso de no poseer métodos polimórficos nuevos,
tanto por esa clase como por las clases padre, entonces la metaclase de dicha
clase es Class. Los métodos ctor(), dtor(), puto(), new(), delete()
y forward() definidos en una clase no se consideran métodos nuevos ya
que ya son definidos para Object. A los métodos polimórficos se les puede
aplicar el estereotipo <<tag>>. El mismo contiene un atributo nombre que
117
lo representa (el mismo lo referencia para obtenerlo a través del método
respondsTo()). Si el nombre estuviese vacío se le asignaría el nombre
del método. Las variables de instancia (data) pueden ser de clase (lo que
significa que se instancian en el archivo de implementación de la clase (.c))
y constantes. Los métodos no polimórficos se implementan en el archivo de
implementación de la clase (.c) y si tienen visibilidad pública se declaran
en el archivo de interfaz de la clase. De acuerdo a ooc los constructores
reciben sus argumentos en una lista de argumentos variables, sin embargo
su representación es con una lista de argumentos fija. Para especificar la
capacidad de procesar un mensaje definida por otra clase a través del método
forward() se utiliza un estereotipo que extiende la relación usage y se llama
<<forwards>>. El final de dicha relación (el proveedor, supplier en inglés) es
el método mismo y no la clase que lo contiene. Se escogió la relación usage
ya que según la especificación no define el uso que el cliente da al proveedor
(OMG 2017), pero mediante el estereotipo sí lo especificamos. También se
puede declarar el método forward() dentro de una clase para implementarlo
(como es el caso en GrandChild).
4.12.1 Introducción
118
4.12.2 Conceptos soportados
1. Encapsulamiento
2. Herencia
3. Polimorfismo
4. Interfaces (sin herencia de interfaces)
5. Propiedades
6. Señales (sistema de notificaciones y mensajes de bajo acoplamiento
entre clases)
7. Programación clave-valor
4.12.4 Codificación
119
4.12.4.1 Interfaz de clases (.h)
#include <glib-object.h>
struct _ChildClass {
GObjectClass parent;
void (*method) (Child *self);
void (*signal1Process) (Child *self);
};
struct _Child {
GObject parent;
gint publicAttr;
};
120
#define TYPE_CHILD (child_get_type ())
#define CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST
,→ ((obj), \
TYPE_CHILD, Child))
#define CHILD_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST
,→ ((cls), \
TYPE_CHILD, ChildClass))
#define IS_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE
,→ ((obj),\
TYPE_CHILD))
#define IS_CHILD_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE
,→ ((cls),\
TYPE_CHILD))
#define CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS
,→ ((obj),\
TYPE_CHILD, ChildClass))
#include <glib-object.h>
#include "child.h"
#include "my_interface1.h"
121
enum {
PROP_0,
PROP_ATTR
};
enum {
SIGNAL1,
LAST_SIGNAL
};
struct _ChildPrivate {
int privateAttr;
};
Selector de method():
122
static void child_method1_impl (Child *self) {
/* ... */
}
Métodos get y set para dar soporte a las propiedades definidas por la clase:
switch (prop_id) {
case PROP_ATTR:
g_value_set_int(value, child->publicAttr);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
switch (prop_id) {
case PROP_ATTR: {
123
gint new_attr = g_value_get_int(value);
child->publicAttr = new_attr;
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
g_object_class->get_property = child_get_property;
g_object_class->set_property = child_set_property;
cls->signal1Process = child_signal1_process;
cls->method = child_method_impl;
attr_param = g_param_spec_int(
"attr", "attr", "attribute of child",
INT_MIN, INT_MAX,
0,
G_PARAM_READWRITE);
g_object_class_install_property(
124
g_object_class,
PROP_ATTR,
attr_param);
child_signals[SIGNAL1] = g_signal_new(
"signal1", /* signal_name */
TYPE_CHILD, /* itype */
G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, /* signal_flags
,→ */
G_STRUCT_OFFSET(ChildClass,
,→ signal1Process),/*class_offset*/
NULL, /* accumulator */
NULL, /* accu_data */
g_cclosure_marshal_VOID__VOID, /* c_marshaller
,→ */
G_TYPE_NONE, /* return_type */
0); /* n_params */
}
125
if (!child_type) {
static const GTypeInfo child_info = {
sizeof (ChildClass), /* class_size */
NULL, /* base_init */
NULL, /* base_finalize */
(GClassInitFunc) child_class_init, /* class_init */
NULL, /*
,→ class_finalize */
NULL, /* class_data */
sizeof (Child), /* instance_size
,→ */
0, /* n_preallocs */
NULL, /* instance_init */
NULL /* value_table */
};
child_type = g_type_register_static(
G_TYPE_OBJECT, /* parent_type */
"Child", /* type_name */
&child_info, /* info */
0); /* flags */
/* add interface */
GInterfaceInfo interface_info_my_interface1 = {
/* interface_init */
(GInterfaceInitFunc)child_init_my_interface1,
NULL, /* interface_finalize */
NULL, /* interface_data */
};
g_type_add_interface_static(child_type,
TYPE_MYINTERFACE1, &interface_info_my_interface1);
126
return child_type;
}
127
GType my_interface1_get_type (void);
128
GType my_interface1_get_type (void) {
static GType type = 0;
if (type) return type;
type = g_type_register_static(
G_TYPE_INTERFACE,
"MyInterface1",
&info,
0);
return type;
}
Tipo de
# dificultad Nombre Descripción
129
Tipo de
# dificultad Nombre Descripción
130
Tipo de
# dificultad Nombre Descripción
Comparado incluso con otros frameworks puede apreciarse una gran cantidad
de código de plantilla (en inglés boilerplate code) asociado al prototipado de
una clase y sus métodos.
131
4.12.6 Propuesta de expresión en UML
132
de la propiedad, y un flag para todas las opciones de flags en el tipo
enumerativo GParamFlags. El nombre y los valores minimos y maximos y
por defecto de la propiedad son tomados de los definidos para el atributo.
Un método puede poseer el estereotipo signal lo que lo hace un manejador
de la misma (no analizaremos como modelar una señal sin manejador), los
valores etiquetados del estereotipo serían (correspondientes a los nombres
de los parámetros pasados a la función g_signal_new()): name: el nombre
de la señal y un flag para todas las opciones de flags en el tipo enumerativo
GSignalFlags. Hay más opciones para las señales y, por consiguiente, más
análisis necesario de cómo modelarlas pero quedan fuera del alcance de
esta tesis, ya que, como veremos, no presentan una dificultad respecto a la
factibilidad de generar código desde tales modelos. Del mismo modo podría
buscarse una propuesta de expresión para closures.
4.13.1 Introducción
133
se define de acuerdo a la sintaxis entendible por su preprocesador, el cual
no es el estándar de C. Con esto, aunque no utiliza la sintaxis de C, logra
que sea muy amigable para el programador C, y el cuerpo de los métodos
sí conserva la sintaxis de C. Las facilidades en tiempo de ejecución de
Dynace lo hacen más costoso en el uso de memoria RAM y tiempo de
ejecución (pero distan mucho de programar, por ejemplo, para una máquina
virtual) por lo que no es recomendable para microcontroladores demasiado
restringidos. Las clases en Dynace son codificados en un único archivo
.d y el código generado sigue la norma ISO C89. Contiene una jerarquía
de clases núcleo equivalente a la de SmallTalk, una biblioteca de clases
completa, threads cooperativos así como soporte para threads nativos y
gran cantidad de documentación para el principiante. Ha sido usado por
más de 20 años en ambientes de producción.
1. Encapsulamiento
2. Herencia (múltiple)
3. Polimorfismo
4. Duck typing
5. Manejo de excepciones
6. Metaclases
7. Recolector de basura
8. Programación clave-valor
4.13.4 Codificación
134
Dynace define un único tipo para el uso de todas las clases, el tipo object.
La implementación de una clase, con variable de instancia, de clase y
especificación de una función de inicialización de clase (se usa en caso de
que se necesiten instanciar recursos para la clase) se realiza con la palabra
clave defclass:
//Parent.d
defclass Parent{
int instanceVariable; //Variable de instancia
class:
int classVariable; //Variable de clase
init: initParentClass; //función de inicialización de la
,→ clase
};
Las clases pueden tener visibilidad privada (por defecto) o pública, lo que
permite referenciar a la estructura de instancia de la clase (de otra forma la
misma está oculta).
//MyClass.d
defclass MyClass : SomeClass, SomeOtherClass;
//MyClass.d
imeth int gMyGeneric, gGeneric2, gGeneric3 (char param)
{
return param;
}
135
cmeth para métodos de clase y sus variantes con lista de argumentos variable:
ivmeth y cvmeth. El tipo de retorno en este caso es integer, pero en caso
de omitirse por defecto es object, la clase padre de todas las clases. La
simplicidad en la sintaxis muestra el poderío de Dynace como lenguaje de
programación.
La visibilidad de las clases y métodos sólo puede ser pública o privada. Los
atributos no tienen atributo de visibilidad. Todos los métodos públicos son
polimórficos, por lo que el atributo isLeaf es ignorado. Puede indicarse si el
método es de clase o no (mediante el atributo isStatic), si es constante o no
(mediante el atributo isConst). Existe una forma de exigir que un método
sea implementado mediante el método gSubclassResponsibility() que
arroja una excepción si un método no está siendo redefinido por un objeto
hijo, con esto pueden definirse métodos abstractos mediante el atributo
isAbstract. Si la implementación del método es el mismo para varios
136
otros métodos, entonces el estereotipo aliases es agregado con el valor
etiquetado names donde se indican los nombres de dichos métodos separados
por coma. Los atributos pueden ser de clase (mediante el atributo isStatic)
y constantes (mediante el atributo isConst). Cabe aclarar que por más
que las clases no tengan una ascendencia en común pueden implementar los
mismos métodos que son llamados con los mismos mensajes (en el ejemplo
tanto una instancia de Class1 como de Class4 pueden responder al mensaje
method1()).
4.14.1 Introducción
COS utiliza el preprocesador ISO C99 para generar código ISO C89. El
diseño y la sintaxis de COS están fuertemente inspirados en Objective-C y
CLOS (Common Lisp Object System), según el autor, uno de los modelos
de objetos más flexibles jamás desarrollados, y en menor medida por Cecil,
Dylan, Haskell, Python, Slate y SmallTalk . COS impone la encapsulación
fuerte y la separación de incumbencias (en inglés concern separation)
a través de su modelo de objeto abierto, que permite usar y extender
los componentes de COS (por ejemplo, clases) definidos en bibliotecas
compartidas sin tener el código fuente. Este modelo de objetos abierto exige
una etapa extra de linkeo para recolectar símbolos distribuidos en el código,
esto se realiza a través de los makefiles que acompañan al framework. El
kernel de COS es de sólo 7000 líneas de código fuente y cumple muy bien los
cinco principios a los que apunta: simplicidad, flexibilidad, extensibilidad,
eficiencia y portabilidad. Posee un modelo de objeto uniforme, y permite la
programación con contratos y closures.
El diseño de COS está optimizado para proporcionar una implementación
portátil y eficiente, especialmente en sus dos características principales:
los multimétodos, así como el reenvío de mensajes genéricos (es decir, la
delegación). El envío de mensajes en COS es de 1,7 a 2,3 veces más lento
que la llamada a una función indirecta (o sea a través de punteros) y
137
aproximadamente de 1,2 a 1,5 veces más rápido que el envío de mensajes
en Objective-C. El reenvío de mensajes en COS es tan rápido como el
envío de mensajes y aproximadamente de 40 a 80 veces más rápido que el
reenvío de mensajes en Objective-C, que, además, tiene fuertes limitaciones
en los valores devueltos. Estos dos conceptos junto a su modelo de
objetos permiten implementar fácilmente: mensajes de orden superior (high
order messages), predicado de clase (class-predicate dispatch), herencia
múltiple, herencia dinámica, clases dinámicas, modelo de objeto adaptativo,
reflexión y gestión avanzada de memoria. COS logra los principios de
simplicidad, flexibilidad y extensibilidad, así como los lenguajes de scripting
convencionales existentes (por ejemplo, PHP, Python, Ruby, Lua) al tiempo
que mantiene la eficiencia y la portabilidad en el rango de C(Deniau 2009).
No es recomendado para ser utilizado en sistemas altamente restringidos
con muy poca memoria RAM ya que la caché para optimizar el reenvío
de mensajes puede ocupar algunos megabytes (esto puede ser configurado
dentro de cierto rango).
1. Encapsulamiento
2. Herencia
3. Polimorfismo
4. Multimétodos
5. Reenvío de mensajes (message forwarding) / Delegados
6. Manejo de excepciones
7. Metaclases
8. Programación clave-valor
138
clases o tipos, las funciones genéricas y los métodos, que definen los tipos
de argumento para una función, puede existir varios métodos para una sola
función. Un método es una implementación o especialización de una función.
Las funciones son llamadas en tiempo de ejecución y la implementación a
ser ejecutada es decidida en base a los tipos en tiempo de ejecución de sus
argumentos (en COS hasta cinco argumentos pueden ser tomados en cuenta
para esta decisión), las implementaciones cuyos argumentos contienen la
misma especialización que los de la llamada son ejecutados, sino se escogen
implementaciones con argumentos menos especializados (o sea que sean
clases padre respecto a los argumentos de la llamada) priorizando los tipos
de los primeros argumentos,1 en caso de no encontrarse una implementación
apropiada la función genérica gunrecognizedMessageN () en llamada,
donde N es la cantidad de argumentos que son tomados en cuenta y por
defecto lanza una excepción. Esta última función da lugar a reenviar la
llamada a alguna otra clase (message forwarding). Esto nos lleva a un
modelo que en vez de tablas virtuales para cada clase tenemos tablas de
métodos para cada función.
4.14.4 Codificación
//gfunction.h
#include <cos/Object.h>
1
Para una descripción más detallada leer Methods specialization en la
documentación de COS.
139
defgeneric(void, gfunction, self, arg1);
//gfunction.h
#include <cos/Object.h>
4.14.4.2 Clases
//Child.h
#include "Parent.h"
defclass(Child, Parent)
int attribute;
OBJ class1;
endclass
140
4.14.4.2.2 implementación (.c) Mostraremos la instanciación de la
clase junto a la implementación de algunos métodos.
//Child.c
#include "Child.h"
#include "gfunction.h"
#include "gfunction2.h"
#include <cos/gen/message.h>
makclass(Child, Parent);
Antes de poder utilizar una clase externa, la misma debe ser declarada con
useclass():
useclass(Class1);
141
defmethod(void, gfunction, Child, Object)
/*...*/
endmethod
4.14.4.3 Propiedades
142
4.14.4.3.1 Definición
//value.h
#include <cos/Property.h>
defproperty( value );
//ChildProperties.c
#include "Child.h"
#include "value.h"
#include <cos/Number.h>
#include <cos/gen/value.h>
defproperty(Child, (attribute)value,int2OBJ,gint);
143
compleja produce que se arrojen una gran cantidad de mensajes de error
al introducir un error de sintaxis al codificar bajo la especificación del
framework, esto dificulta encontrar dicho error, por lo que una herramienta
previa que analice errores en el uso del framework podría ser de gran
utilidad.
144
La base de este modelado es relacionar clases (sujetos) y funciones genéricas
(verbos) a través de métodos (especialización).
Las clases poseen atributos y métodos (que explicaremos mejor a
continuación). Pueden heredar de una única clase y en caso de no indicarse
una herencia heredan de la clase Object. Las funciones genéricas se definen
como operaciones dentro de una clase con el estereotipo <<generic>> (esto
permitiría que una o varias funciones genéricas sean definidas en un mismo
archivo). Los argumentos de las funciones genéricas que no poseen un
tipo son tipos abiertos y se admiten hasta cinco (que deben además ser
los primeros), los argumentos que posean tipo serán tipos cerrados. Los
métodos pueden ser definidos tanto dentro de una clase como fuera de ellas
dentro de una clase con el estereotipo <<method>> (esto permite que sean
definidas en un archivo aparte del de alguna clase). Los métodos pueden
realizar varias funciones genéricas (en el ejemplo el método gfunction4()
en Child realiza gfunction4()), en caso de realizar más de una función
genérica con la misma signatura, las mismas son alias entre sí, si el método
tiene el mismo nombre que alguna de ellas, la misma es definida (defmethod)
y el resto son considerados alias (defalias). Los métodos deben contener
la misma cantidad de argumentos que la función genérica que realizan y
deben definir todos sus tipos. En el caso de que un método no realice una
función genérica se considera que realiza a la función genérica que posee su
nombre (en el ejemplo sería el caso de gunrecognizedMethod1()), de ser
145
necesario, una dependencia de tipo usage deberá hacer visible la función
genérica para el método (relacionando a ellos dos o a sus contenedores).
Un método around no agrega ninguna regla adicional a las anteriores para
poder modelarlo, tan sólo se precisa un método con el nombre de la función
genérica entre paréntesis (En el ejemplo es el caso de (gfunction2)()
dentro de Child).
Las propiedades se modelan como clases (ya que internamente es lo que
son) con el estereotipo <<property>> y pueden relacionarse con un atributo
que las realice (en el ejemplo el atributo attr de Child está realizando a la
propiedad value).
4.15 Conclusiones
Hemos visto una gran cantidad de frameworks muy dispares entre sí en sus
modelos de objetos, algunos tan solo dando soporte al polimorfismo y otros
con modelos comparables con los de C++ (OOPC), JAVA (OOC-S y OOC
de Miseta), Small Talk (Dynace) y otros con CLOS (Dynace y aún más
COS). Sin embargo, respecto a lo que atañe a la dificultad en el prototipado
bajo cada uno de ellos podemos sacar varias conclusiones:
Los frameworks que se valen de herramientas externas en el proceso de
convertir código fuente en artefactos como bibliotecas o ejecutables pueden
sortear las dificultades del prototipado de objetos en C, en general la
herramienta más utilizada es un preprocesador como en el caso de Dynace
(también OOC de Schreiner y para el caso de GObject existen compiladores
fuente a fuente como Vala y Genie). La desventaja principal que se
introduce al utilizar este enfoque es que el editor que se utilice no entenderá
el código que estamos escribiendo por lo que herramientas de búsqueda,
autocompletar o refactoring no nos servirán. Además, un nuevo lenguaje
-y no estándar- debe aprenderse. Por otro lado, como se vió en el caso de
Dynace, un preprocesador externo puede dejarnos tan sólo con la dificultad
un lenguaje muy reducido en nuevas palabras respecto del lenguaje C. Entre
los frameworks que se valen de herramientas externas COS se distingue de
los demás valiéndose tan sólo de una etapa extra de linkeo y una increíble
biblioteca de macros C99, esto le permite utilizar C puro sin adición de
146
dificultades significativas en el prototipado. Creemos que este framework
debe tomarse como objeto de investigación para futuros frameworks que
deseen implementar un modelo de objetos distinto y quizás uno aplicable
a los sistemas embebidos. Respecto del resto de los frameworks que no se
valen de herramientas externas (de los estudiados: Ben Klemens, SOOPC,
OOC de Tibor Miseta, OOPC, OOC-S y GObject), a pesar de que el
procesador de C y bibliotecas puede ayudar, precisan una gran cantidad
de código repetitivo y repetición de información para el prototipado de
clases. La forma en que el código crece en líneas al adicionar métodos a
las clases es de aproximadamente 3 a 5 veces más respecto de utilizar un
LPOO. Justamente algunos de estos frameworks son los más apropiados
para sistemas altamente restringidos. Pero como ya hemos dicho, evaluar
el framework en soledad no es algo correcto. Todavía podemos introducir
herramientas dentro del tool suite del programador que disminuyan dichas
dificultades y nos permitan seguir trabajando bajo el lenguaje C. Nuestra
propuesta es un generador de código desde diagramas UML a introducir en
el siguiente capítulo.
147
Capítulo 5
148
5.2 Estado del arte en generadores de código C desde
diagramas de clase UML
149
de referencia’ y ‘Nombre del parámetro de referencia’ en la página
‘Especificaciones de C’ para admitir esto parámetro de referencia.
Limitaciones de la Programación Orientada a Objetos en C
150
5.2.3 Astah
Como puede apreciarse existe una relación de herencia entre la clase padre
Abstract y la clase hija Concrete, Abstract está definida como una clase
abstracta en el diagrama.
El generador de código genera 3 archivos por cada clase, uno de
implementación (.c), y 2 de interfaz (.h). En uno de los archivos de
interfaz, si la clase es abstracta, entonces se crea una estructura con
punteros a función que corresponden a las operaciones definidas para esa
clase, esta estructura serviría para instanciar una tabla virtual para la clase
y sus derivadas. Luego es definida una estructura de instancia de clase
cuyo primer miembro es un puntero a la tabla virtual y a continuación los
atributos de la clase.
Las clases hijas no extendien a la tabla virtual por más que también sean
abstractas, por lo que esta facilidad solo sirve para 1 nivel de herencia.
Los demás archivos de la clase hija son esqueletos de las operaciones
definidas, por lo que no existen más facilidades que esta.
151
La clase hija no contiene ninguna referencia a la clase padre por lo
que tampoco brinda facilidades en la implementación del polimorfismo
(instanciación de la tabla virtual, asignación de su referencia en una
instancia, asignación de implementaciones de funciones).
//Abstract.c
#include "Abstract.h"
//Abstract.h
typedef struct AbstractStruct *Abstract;
#include "AbstractPrivate.h"
//AbstractPrivate.h
typedef struct AbstractInterfaceStruct *AbstractInterface;
struct AbstractStruct {
152
AbstractInterface vtable;
uint64_t id;
char *type;
};
//Concrete.c
#include "Concrete.h"
//Concrete.h
typedef struct ConcreteStruct *Concrete;
153
void Concrete_Create(Concrete super, uint64_t id, const char
,→ *type);
void Concrete_Destroy(Concrete super);
uint8_t Concrete_Operation(Concrete super);
#include "ConcretePrivate.h"
//ConcretePrivate.h
struct ConcreteStruct {
};
Como puede apreciarse, la ayuda que provee este generador de código es muy
reducida dejando gran cantidad de trabajo al programador tan solo para el
prototipado.
5.2.4.1 C generator
154
por ejemplo, una clase privada representa un archivo de implementación (.c),
una interfaz un archivo de interfaz (.h) y una clase pública a ambos. Esto
ya nos indica que ni siquiera el concepto de clase de la OO es facilitado por
este modelado.
Para extender la semántica de UML posee un perfil (en inglés profile) UML
que posee los estereotipos que precisa para su modelado.
El modelado y la generación de código orientado a objetos soportados son
muy similares a los vistos en Enterprise Architect y Rhapsody. Una clase
representa un TDA (tipo de dato abstracto) cuyos atributos son declarados
en una estructura de instancia de la clase, las operaciones públicas definidas
en la clase pasan a ser funciones declaradas en el archivo de interfaz donde se
declara el TDA y las privadas son definidas en el archivo de implementación.
La relación de generalización no tiene significado para este generador por lo
que los conceptos de herencia no son facilitados.
5.3 Resumen
155
expresar la estructura contenida en dicho código. Debido a que C no es
un LPOO mientras que UML es un lenguaje inherentemente orientado a
objeto ha llevado a un recorte del lenguaje o redefinición de su semántica
para ser utilizado junto con C. En general, de los conceptos de la OO, el
encapsulamiento es el único concepto soportado por estos generadores. El
único generador que incorpora en alguna medida herencia y polimorfismo es
el uml2c de astah, la herencia soportada es de hasta un nivel (un padre y
varios hijos que no pueden a su vez ser padres de otras clases) y el código
generado deja mucho de la implementación al programador (inclusión de la
clase padre en la del hijo, inicialización de la tabla virtual, inicialización de
un objeto con su clase padre).
El estado actual de los generadores de código C desde UML nos muestra que
un generador que implemente varios conceptos de la orientación a objetos y
que se encargue en su totalidad del prototipado de los artefactos relacionados
con la orientación a objetos (clases, interfaces, etc.) está vacante.
156
Capítulo 6
Implementación de generadores
de código C embebido, eficiente
y orientado a objetos desde
diagramas de clase UML
157
6.2 Factibilidad
6.3 Verificaciones
158
6.4 Mejoras en el código para facilitar la generación de
código
Si bien nos limitaremos a generar código para los seis frameworks que se
han vuelto el objetivo de esta tesis (Ben Klemens, SOOPC, OOC-S, OOPC
, OOC de Miseta y OOC de Schreiner) no nos contendremos en proponer
mejoras en las especificaciones de codificación de dichos frameworks que
faciliten el mapeo entre modelos UML y dicho código. Esto será útil ya que
simplificará a los generadores de la nueva especificación e incluso facilitará
en alguna manera al programador de clases. Estas mejoras pueden tener
su contrapartida, quizás la adición de más líneas de código o el cambio de
estándar de codificación C (por ejemplo de ISO C89 a C99).
159
(por tratarse de herencia simple esto puede obtenerse en una lista). Esto
podemos hacerlo utilizando una consulta (query) recursiva.
[query
public
getEntireClassesHierarchechy(aClass: Class) : OrderedSet (
,→ Class ) =
if aClass<>null then
aClass.superClass.getEntireClassesHierarchechy() ->
,→ asOrderedSet()
->prepend(aClass)
else
OrderedSet{}
endif
/]
Luego de que podemos obtener esta lista podemos fácilmente obtener la lista
de clases padre de una clase excluyendo a la clase misma de la lista. Esta
consulta la llamamos getAncestors().
Luego para cada clase deberemos saber si los métodos virtuales que definen
se tratan de una redefinición polimórfica de los mismos (por lo que está
declarada en una tabla virtual anterior) o es la primera definición de la
misma por lo que genera una referencia nueva en su tabla virtual.
La siguiente consulta nos resuelve eso:
[query
public
isAMethodRedefinition(o: Operation) : Boolean =
o.class.getAncestors().allNotFinalOperations()
->exists(o1:Operation |redefine(o1,o))/]
160
con . entonces la función es operada sobre cada miembro del contenedor
devolviendo un contenedor nuevo (conteniendo los tipos devueltos por la
función).
De esta manera la consulta anterior se lee de la siguiente manera: para
la operación o se obtiene la clase la contiene, de esa clase se obtiene
la lista de sus ancestros, luego se obtiene un contenedor reemplazando
cada ancestro por sus métodos polimórficos (devueltos por la función
allNotFinalOperations()) luego se interroga si dentro del conjunto de los
métodos polimórficos de las clases padre existe (exists()) un método que
tiene el mismo nombre que el método o (redefine(o1,o)).
De esta manera podemos escribir fácilmente otra consulta que nos ayude a
obtener los métodos polimórficos definidos por primera vez por la clase que
llamaremos getNotRedefine ().
La última información que deberemos poder obtener es cual es la última
implementación disponible de cierto método para asignarla a la tabla virtual.
Para eso usamos la recursividad comenzando desde las operaciones de la
clase yendo hacia la de su ancestro.
[template
public
getLastClassDefinerName(aClass: Class ,aMethod: Operation)]
[if (aClass.ownedOperation->exists(m:Operation |
m.redefine( aMethod ) ))]
[aClass.name/]
[else]
[if aClass.hasSuperclass()]
[aClass.getSingleInheritanceSuperClass().
getLastClassDefinerName(aMethod)/]
[/if]
[/if]
[/template]
161
Luego que tenemos todas estas consultas podemos crear una plantilla para
instanciar la tabla virtual de una clase:
162
6.5.1.1 Verificación
163
siguiente tabla virtual para la clase GrandChild:
};
6.5.1.2 Mejoras
164
#define [aClass.name.toUpper()/]_METHOD_IMPLEMENTATIONS \
[aClass.superClass.toUpper()/]_METHOD_IMPLEMENTATIONS \
[for (o : Operation | aClass.allNotFinalOperations() )]
.[o.name/] = [aClass.name/]_[o.name/]_,
[/for]
Ahora una clase hija que defina de nuevo el método polimórfico no lo estará
incluyendo en su estructura de tabla virtual por más que desconozca que
165
se trata de una redefinición. La desventaja de esto es que ahora no podrán
existir dos métodos con el mismo nombre. De esta manera también pasamos
la resolución de esta problemática al precompilador C.
Los métodos polimórficos son declarados por primera vez en alguna clase
tabla virtual de alguna clase con el primer argumento (self) siendo del tipo
de esa clase. Si una clase heredada quiere redefinir dicho método, deberá
respetar su firma original para no obtener errores de parseo. Para esto es
necesario poder saber cual es la primer clase que definió el método. Esto
se puede conseguir con una consulta getFirstClassDefinerName() que
funcione a la inversa de la ya presentada getLastClassDefinerName().
Con esa consulta podemos generar la siguiente plantilla para los prototipos
166
de métodos virtuales:
6.5.2.1 Verificación
Se puede ver que a pesar de que Child es la clase que está redefiniendo el
método, el argumento utilizado para el argumento self es la clase Parent
la cuál es la primera en declarar el método vPrint().
6.5.2.2 Mejoras
167
#define [o.name.toUpper()/]_SELF_TYPE [aClass.name/]
[o.generateSOOPCMethodPrototype()/],
#endif
[/for]
}
El primer objetivo entonces es encontrar las clases que definen por primera
vez el método. Para eso de nuevo debemos recurrir a la recursividad con la
siguiente consulta:
[query
public
168
getFirstClassesDefinerName(aClass: Class ,anOperation:
,→ Operation): Set(Class)=
la idea es que una clase solo debe agregarse como definidora si las clases
padre de la misma no contienen dicho método. Para preguntar si las clases
padre contienen dicha operación utilizamos la consulta hasOperation():
[query
public
hasOperation(aClass: Class ,anOperation: Operation): Boolean=
if( aClass.ownedOperation->exists( op
| op.redefines(anOperation) ) ) then
true
else
if aClass.superClass->isEmpty() then
false
else
aClass.superClass->exists(s:Class
| s.hasOperation(anOperation))
endif
endif
/]
169
Esto lo podemos conseguir con las siguientes queries:
[query
public
parameterIndex (op1 : Operation,par1: Parameter) : Integer =
op1.ownedParameter->asOrderedSet()->excluding
(op1.ownedParameter->any(direction =
,→ ParameterDirectionKind::return))
->asOrderedSet()->indexOf(par1)/]
La siguiente tarea será conseguir todo el camino de herencia desde las clases
que definen por primera vez la operación hasta la clase a generar. Si nos
encontramos en un caso del problema del diamante los caminos pueden ser
varios. Este problema es difícil de resolver en OCL y requerimos ayuda de
la comunidad de Acceleo para resolverla.
Las siguiente consulta nos devuelve toda la jerarquía de clases:
170
let superSuperPaths : Set(Sequence(uml::Class)) = if
,→ aClass.superClass->isEmpty()
then OrderedSet(Sequence(Class)){Sequence(Class){}}
else aClass.superClass->iterate(superClass1;
result : Set(Sequence(uml::Class)) =
,→ Set(Sequence(uml::Class)){}
| result->union(superPaths(superClass1)))
endif in
superSuperPaths->collectNested(superPath :
,→ Sequence(uml::Class) |
superPath->prepend(aClass))->asOrderedSet()
/]
Con esta consulta podemos armar otra que nos devuelva todos los caminos
de una clase a otra:
171
=
,→ invoke('org.eclipse.acceleo.module.oopc.services.UML2OOPCServices',
,→
'getPathsJAVA(org.eclipse.uml2.uml.Class,
,→ org.eclipse.uml2.uml.Class)',
Sequence{arg0, arg1})
/]
import org.eclipse.uml2.uml.Class;
if(superClasses.isEmpty())return result;
172
for(List<Class> path : subPath){
path.add(0, superClass);
result.add(path);
}
}
}
return result;
}
}
Fue mucho más sencillo resolver la consulta con JAVA ya que es un lenguaje
conocido y gracias a las herramientas de depuración.
6.5.3.1 Mejoras
173
6.5.3.2 Verificación
174
overload([for(c:Class|path)][c.name/].[/for][anOp.name/])
,→ = methodOvldName([anOp.name/],[aClass1.name/])
[/for]
[/if]
[/for]
[/for]
[/template]
overload(Class4.Class2.Operation1) =
,→ methodOvldName(Operation1,Class2)
overload(Class4.Class3.Operation1) =
,→ methodOvldName(Operation1,Class3)
overload(Class4.Class2.Class6.Operation2) =
,→ methodOvldName(Operation2,Class6)
overload(Class4.Class3.Class6.Operation2) =
,→ methodOvldName(Operation2,Class6)
Si distintas clases definen por primera vez el método como en el caso del
método Operation1() por las clases Class2 y Class3 entonces pueden
obtenerse los caminos hacia cada una.
175
si la consulta getFirstClassesDefinerName() introducida en la sección
anterior devuelve la clase que se está generando como la primera en definir el
método. Esta última manera fue la utilizada en la plantilla de la verificación
anterior.
6.5.4.1 Mejoras
6.5.4.2 Verificación
defmethod(Class2_Operation1)
[query
public
176
getNotInterfaceRedefiner (seq : Sequence(Operation) )
: Sequence(Operation) =
seq->select(op : uml::Operation
| not(op.isAnInterfaceMethodRedefinition()))
->asSequence()/]
[query
public
isAnInterfaceMethodRedefinition (anOperation : Operation ) :
,→ Boolean =
,→ anOperation.class.getEntireClassesHierarchechy().getAllInterfaces()
.getOwnedOperations()->exists(anInterfaceOperation:Operation
|redefines(anInterfaceOperation,anOperation))/]
,→ getNotInline()->getNotRedefine()->getNotInterfaceRedefiner()]
177
[for (o : Operation | seqOp) ]
[o.generateReturnType()/]
,→ (*[o.name/])([o.generateOOCMethodArguments()/]);
[/for]
[/let]
EndOfVirtuals;
6.5.5.1 Mejoras
178
6.5.5.2 Verificación
179
Virtuals( Child, Parent );
char * (*getNameFollowedByClassName)(Child self);
EndOfVirtuals;
Como en los frameworks anteriores que generan sus tablas virtuales anidando
estructuras no anónimas (no como en el caso de Ben Klemens o OOC-S) para
la inicialización de la tabla virtual se debe conocer la clase que realiza por
primera vez cada método. En este caso además, cuando el método pertenece
a una interfaz, se debe saber cuál es la clase que realiza por primera vez la
interfaz. La siguiente plantilla inicializa la tabla virtual dentro de la función
de inicialización:
,→ [o.generateMethodUpCast(classLink)/][o.class.name/]_[o.name/];
[/for]
[/let]
[for ( aClassLinkInterface : uml::Interface
180
|classLink.getAllInterfaces().oclAsType(uml::Interface) )]
[let aClassLinkInterfaceOperations :
,→ Sequence(uml::Operation)
= aClassLinkInterface.getOwnedOperations() ]
[for (anInterfaceOperation : Operation
|
,→ aClassVirtuals->intersection(aClassLinkInterfaceOperations))]
(([classLink.getName()/]Vtable)vtab)-> \
[aClassLinkInterface.getName()/].[anInterfaceOperation.getName()/]
,→ = \
[anInterfaceOperation.generateInterfaceMethodUpCast(aClassLinkInterface)/]
,→ [aClass.getName()/]_[anInterfaceOperation.getName()/];
[/for]
[/let]
[/for]
[/for]
[/let]
6.5.6.1 Mejoras
6.5.6.2 Verificación
181
ChildVtable vtab = & ChildVtableInstance;
((ParentVtable)vtab)->vPrint = (void(*)(Parent
,→ self))Child_vPrint;
((ParentVtable)vtab)->Serializable.serialize =
(void(*)(Object self, char * out))Child_serialize;
((ChildVtable)vtab)->getNameFollowedByClassName =
(char *(*)(Child
,→ self))Child_getNameFollowedByClassName;
182
en JAVA es tener que codificar un envolvedor (wrapper en inglés) para cada
una. Sería importante generar un estudio comparativo entre Acceleo y otras
tecnologías para generar código desde UML, Xtend por ejemplo está siendo
ampliamente usado en distintos proyectos como Yakindu1 y Papyrus.2
6.7 Resumen
183
un generador de código. Otra manera de simplificar el mapeo es utilizando
una forma de modelar que requiera inferir menos información (como vimos
para para OOC en metaclases explícitas respecto de las implícitas).
Hemos utilizado el conocimiento adquirido para esta tesis para implementar
un generador de código para los frameworks SOOPC, OOC de Miseta
y Dynace. Para esto hemos modificado el proyecto UML Generators y
en especial el generador UML to Embedded C Generator. El proyecto
resultante puede encontrarse en el repositorio especificado en el apéndice.
Hemos tenido la oportunidad de utilizar el generador en los procesos de
producción de una empresa dedicada a los sistemas embebidos de forma
satisfactoria.
184
Capítulo 7
Tres han sido los ejes que han impulsado el desarrollo de esta tesis.
1. ¿Qué clase de diagramas de clase UML son fácilmente mapeables a código
C escrito bajo un framework de orientación a objetos tomando en cuenta sus
conceptos soportados?
2. ¿En qué medida es más sencillo expresar bajo estos frameworks artefactos
como clases o interfaces comparado con hacerlo desde el código?
3. ¿Cuál es la factibilidad y sencillez de implementar un generador de código
a partir de los diagramas?
185
es el introductorio en muchas carreras relacionadas al software); además el
costo de la programación bajo estos conceptos es mucho más visible.
2. Salvo para los frameworks que utilizan una herramienta externa en
el proceso de convertir código fuente en artefactos como bibliotecas o
ejecutables, las dificultades en el prototipado de clases y su evolución
escribiendo nuevos métodos son variadas e importantes comparadas con
otros LPOO, se utiliza mucho código de plantilla, se repite información
en el código, se empeora la legibilidad del código si la implementación de
conceptos está mezclada con la implementación de la solución al problema
y se debe tener conocimiento de la jerarquía de clases o por lo menos de
si el método polimórfico declarado se trata de una redefinición o no. Esto
empeora la experiencia del programador al tener que estar concentrado en
la implementación de conceptos de OO en vez de la solución del problema
y al obligarlo a escribir más código con posibilidad a equivocarse en el
proceso. UML ha sido concebido para expresar con sencillez los conceptos
de la orientación a objetos por lo que no son objeto de las dificultades
anteriores. Otra forma de expresión utilizada para disminuir la dificultad
es la de un preprocesador externo, el mismo implica dejar de codificar en C
por lo que muchas herramientas como las de análisis de código, refactoring y
autocompletar se vuelven obsoletas, además un nuevo lenguaje no estándar
debe aprenderse. UML es un lenguaje estándar y no reemplaza el lenguaje
de implementación que es usado.
3. Hemos podido escribir un generador de código completo para algunos de
los frameworks analizados, el código del mismo se encuentra en el repositorio
especificado en el apéndice y hemos analizado los puntos necesarios para
hacer factible generar código para los seis frameworks objetivo. Para eso
elegimos Acceleo dentro del conjunto de herramientas especializadas en
generación de código desde UML. La necesidad de información adicional,
comparado con otros LPOO, que poseen los frameworks nos llevó a
escribir consultas en OCL (tecnología central en Acceleo), un lenguaje
altamente declarativo y que hemos encontrado difícil de usar, esto nos
llevó a incursionar en realizar consultas en JAVA en vez de en OCL lo
que ha resultado muy conveniente cuando la dificultad de la consulta
se vuelve grande. La necesidad de información adicional podría suplirse
modificando la especificación de codificación de los frameworks, esto puede
186
traer consecuencias importantes como tener que cambiar el estándar en el
que se está escribiendo el código, la consecuencia de tener que escribir más
líneas de código es relativa si se está utilizando un generador de código.
Otra manera de simplificar el mapeo es utilizando una forma de modelar
que requiera inferir menos información (como vimos para para OOC en
metaclases explícitas respecto de las implícitas). El resultado de tener un
generador de código en UML ha mostrado ser muy bueno en el área de
desarrollo, además de las ventajas inherentes para cualquier lenguaje de
programación, mejora la experiencia del programador COO volviéndola
parecida a trabajar con otros LPOO y que es la forma adecuada de
compararla con ellos: desde el toolsuite utilizado y no desde el lenguaje en
aislamiento. El problema en la legibilidad del código se resuelve generando
un archivo aparte para la implementación de los métodos que no se mezcla
con la implementación de la OO en lenguaje C. Para la educación si se
desea enseñar orientación a objetos con estos frameworks un generador de
código desde UML puede facilitar la tarea.
187
creado y puede no ser un buen lenguaje de modelado para los mismos. Esta
tesis puede ser tomada como puntapié para el modelado de estos pero un
estudio más formal es requerido.
3. Al haber logrado satisfactoriamente crear generador de código desde
UML, el costo de líneas de código generadas es despreciable por lo que
nuevos frameworks pueden surgir explotando esta característica, por ejemplo
para aumentar las facilidades al usuario de clases.
4. Utilizar Acceleo como tecnología para la generación de código ha
mostrado sus contras en un contexto en que se debe extraer información
indirecta al diagrama. Podría analizarse si bajo otras tecnologías como
Xtend lo hacen más simple.
5. Nuestro estudio explotó el lado de la ingeniería directa, pero se vería
sumamente potenciado si se desarrollara también la ingeniería inversa,
permitiendo expresarse en código o en modelos según le resulte más cómodo
al programador en cada caso.
6. Los modelos permitidos por cada framework son reducidos en
comparación a los que UML permite (por ejemplo en permitir herencia
múltiple, esto no está limitado a ciertos frameworks sino que lenguajes como
JAVA también tienen esas limitaciones). Mediante OCL se puede obtener
un verificador de UML para constatar que los modelos se ajustan a los
límites de cada framework.
188
Apéndice: Estructura del
repositorio que acompaña este
escrito
Ubicación
El código y los proyectos que acompañan a esta tesis y de los cuáles hemos
hecho referencia en la misma se encuentran en github: https://github.com/
jonyMarino/tesis así como en el CD que se entrega con esta tesis. Dentro del
mismo se encuentran archivos README que indican cómo utilizar y compilar
cada proyecto.
Estructura
.
��� ejemplos_de_codificacion_para_cada_framework
� ��� BenKlemens
� ��� COS
� � ��� COS
� � ��� COSDemo
� ��� Dynace
� � ��� Dynace
189
� � ��� DynaceDemo
� ��� GObject
� ��� OOC_ATS
� � ��� ejemplo_tesis
� ��� OOCS
� ��� OOC_Tibor_Miseta
� ��� OOPC
� ��� SOOPC
��� generador_de_codigo_para_SOOPC_OOC_Y_DYNACE
� ��� UML2ooec
� � ��� org.eclipse.umlgen.gen.embedded.c
� � ��� plugins
� � ��� org.eclipse.umlgen.gen.embedded.c
� � ��� org.eclipse.umlgen.gen.embedded.c.profile
� ��� verificaciones_generacion_de_codigo_para_OOC_TM
� ��� verificaciones_generacion_de_codigo_para_SOOPC
��� consultas_para_un_generador_de_codigo_para_OOPC
��� org.eclipse.acceleo.module.oopc
��� model
��� src
��� src-gen
190
nuestro generador). También se encuentran en la carpeta dos proyectos
utilizados para verificar el correcto funcionamiento del generador tanto para
el framework SOOPC como OOC de Tibor Miseta, los mismos contienen
un modelo UML modelado con papyrus y el código generado se encuentra
en la carpeta src-gen.
Por último, la carpeta consultas_para_un_generador_de_codigo_para_OOPC
contiene un proyecto Acceleo dónde se encuentran en la carpeta src las
consultas necesarias para implementar un generador de código para el
framework OOPC. En la carpeta model se encuentra el modelo utilizado
para corroborar el correcto funcionamiento de las consultas. En la carpeta
src-gen se encuentra el código generado por la plantilla que utiliza dichas
consultas.
191
Referencias
Barr, M., 2018. Embedded C Coding Standard, 20251 Century Blvd, Suite 330
Germantown, MD 20874: Barr Group.
Deniau, L., 2009. The C Object System Using C as a High-Level Object-Oriented Language,
CERN – European Organization for Nuclear Research.
Douglass, B.P., 2010. Design Patterns for Embedded Systems in C: An Embedded Software
Engineering Toolkit, Newnes.
Eckel, B., Strong Typing vs. Strong Testing. Available at: {http://bit.ly/2CSS6lS}.
Grady Booch, I.J., James Rumbaugh, 1998. Unified Modeling Language User Guide, The,
192
Programmers.
Klemens, B., 2013. 21st Century C: C tips from the new school, 1st edition, O’REILLY.
Lennis, L. & Aedo, J., 2013. Generation of Efficient Embedded C Code from
UML/MARTE Models, Department of Electronic Engineering, University of Antioquia,
ARTICA, Medellín, Colombia.
Madsen, O.L., 1988. What object-oriented programming may be - and what it does not
have to be, Lecture Notes in Computer Science.
M. Maranzana, J.L.S., J. -F. Ponsignon & Bernier, F., 2004. Timing performances of
automatically generated code using mda approaches, ECSI.
Murillo, L.G., 2009. Bridging the Gap Between Model Driven Engineering and HW/SW
Codesign, ALaRI Institute, Lugano, Switzerland.
OMG, 2017. OMG Unified Modeling Language (OMG UML) Version 2.5.1, OMG.
Peter W. Madany, P.K., Nayeem Islam, 1992. Reification and reflection in C++: an
operating systems perspective, Technical Report UIUCDCS–R–92–1736,Department of
Computer Science, Urbana-Champaign.
Samek, M., 2012. Economics 101: UML in Embedded Systems. Available at:
{https://embeddedgurus.com/state-space/2012/04/economics-101-uml-in-embedded-systems}.
193
J. Hartmanis, editors, Proc. European Conf. on Object-Oriented Programming,
Paris (France), 1987, revisited 1991. Lecture Notes in Computer Science no. 276.,
Springer-Verlag.
Xtend, 2016. Papyrus, Adding a New Code Generator. Available at: {http://bit.ly/37gEPBt}.
194