Ir al contenido principal

Principios de OOP aplicados en C para Embedded Systems

Los principios del diseño orientado a objetos [1,2] aplicados a la programación, no son propios de un lenguaje o herramienta particular, sino más bien, son una manera disciplinada de organizar, diseñar y codificar software. Estos son: el encapsulamiento, la herencia y el polimorfismo. Su aplicación, en sistemas de software, incluyendo los embedded systems, tiene grandes beneficios, ya que fomenta la producción de software modular, reutilizable, flexible, transportable y sumamente legible. Además, facilita y naturaliza, el uso de lenguajes de modelado de software como UML [1,2]. Este nos ayuda a describir y diseñar el sistema, por medio de una serie de notaciones gráficas estandarizadas, como los diagramas de estados Statecharts [3], los diagramas de secuencia, entre otros. Fundamentalmente, UML nos proporciona un conjunto herramientas conceptuales para representar o modelar el sistema.


Introducción

Aunque estos principios junto al uso de UML, se asocian comúnmente con los lenguajes orientados a objetos, como C++ o Java, pueden aplicarse en casi cualquier lenguaje de programación, incluyendo C, que es claramente un lenguaje estructurado y preponderante en la construcción embedded systems. Más aún, buena parte de los sistemas de software de complejidad relativa, utilizan estos principios de una forma u otra, en la totalidad del sistema o en módulos específicos, por ejemplo, la implementación del soporte de archivos y device drivers de Linux, que obviamente está íntegramente escrito en C, está basado en el encapsulamiento, la abstracción, y el polimorfismo.

Por otro lado, lo que motiva al uso de UML junto a los principios de la OOP (Object-Oriented Programming), para la construcción de embedded software en C, es simple: aumentar la productividad y la calidad del software. Esta es la razón por la cual escribí el presente artículo.

Si bien estos conceptos no son nuevos, su aplicación al "mundo embedded" no es del todo tradicional, fundamentalmente, creo que esto se debe a la formación de los desarrolladores y gerentes que intervienen en la construcción de este tipo particular de sistemas. Por mi parte y como desarrollador de C, durante años utilicé estos conceptos y corroboré sus beneficios, en sistemas de variada índole y complejidad, pero aplicándolos como prácticas aisladas de programación, desconociendo que detrás de estos, existía un formalismo, que una vez descubierto me permitió, no sólo ponerle nombre a las "cosas", sino también redefinir, integrar y mejorar su aplicación en el mundo complejo de los embedded systems. Inclusive, logré incorporar paulatinamente los diagramas UML, para mejorar la representación de los diferentes aspectos del sistema, como ser los funcionales, de comportamiento, interactivos, y estructurales. Ligado a estos, los patrones de diseño [1,2], y los frameworks reactivos [4,8]. Generalmente, y por diferentes razones, la estrategia que empleo para  utilizar UML en el desarrrollo de aplicaciones embedded es la denominada modelado basado en objetos, la cual difiere a la estrategia modelado orientado a objetos [9]. Los diseños basados en objetos no cumplen estrictamente con todos los principios de OOP y por lo tanto, no pueden acceder a todas las ventajas de UML.

Anidando estructuras

Una estructura, struct en C, es una coleccción de una o más variables, posiblemente de tipos diferentes, agrupadas bajo un único nombre. Lo cual facilita la organización de datos complejos, permitiendo que un grupo de variables relacionadas sean tratadas como una unidad, en lugar de entidades separadas. Las variables que componen una estructura se denominan miembros o campos. Frecuentemente, estos definen los atributos de la entidad que los agrupa [7].

Para demostrar el uso efectivo de los principios de la OOP en lenguaje C se propone un simple ejemplo. Supongamos una función, de nombre Message_receive(), que recibe por argumento un mensaje, llamadado Z, el cual transporta cierta información, proveniente, por ejemplo, de un protocolo de comunicaciones. Dicho mensaje se representa por una estructura de tipo Message.

typedef struct Message Message;
struct Message
{
    uint8_t var1;
    uint32_t var2;
    TimeStamp timestamp;
};

La función Message_receive() recibe y procesa acordemente el mensaje Z, regresando el resultado de la operación.

int
Message_receive(Message *m)
{
    printf("[%08d] Message = MSG_Z with args %03d, %04X\n", 
           m->timestamp, m->val1, m->val2);
}

Supongamos ahora que se requiere recibir y procesar dos nuevos mensajes, por medio de Message_receive(), X e Y, los cuales se representan por medio de las estructuras de tipo MsgX y MsgY, respectivamente.

typedef struct MsgX MsgX;
struct MsgX
{
    double var1;
    uint32_t var2;
    TimeStamp timestamp;
};

typedef struct MsgY MsgY;
struct MsgY
{
    uint8_t var1[Y_VAR1_SIZE];
    TimeStamp timestamp;
};

Para resolver esta situación y demostrar el objetivo de artículo, se imponen las siguientes premisas a la solución:
  1. La función Message_receive() debe continuar siendo el único punto de acceso para la recepción de mensajes, y su prototipo debe mantenerse tal como fue concebido.
  2. Debe ser flexible para soportar la extensión de futuros mensajes y modificaciones a los existentes.
  3. Debe minimizar las modificaciones al programa existente.
Para comenzar se analizan las características de los mensajes X, Y y Z, con lo cual:
  • Se ordenan sus miembros e indentifican los atributos comunes entre mensajes.
  • Se agrega un campo en cada estructura de forma tal que la misma pueda identificarse fácilmente, mediante un valor enumerado. 
La figura F1 muestra el reordenamiento propuesto.

F1: Estructura de mensajes redefinida
Luego, y de acuerdo con la figura anterior, se agrupan los miembros comunes en una nueva estructura, la cual adopta el nombre Message. Esta se incluye en cada estructura de mensaje, por medio del campo denominado base, donde tanto su ubicación (u offset) como su tipo, son parte fundamental de la solución propuesta, sus razones se dilucidarán en breve. Así, las estructuras se redefinen como muestra el siguiente fragmento de código. Observemos que la estructura del mensaje original cambia de nombre a MsgZ.

typedef enum MsgId MsgId;
enum MsgId
{
    MSGID_X, MSGID_Y, MSGID_Z
};

typedef struct Message
{
    MsgId id;         /* Message identification */
    TimeStamp timestamp;
};

typedef struct MsgX
{
    Message base;   /* Base or common structure */
    double var1;
    uint32_t var2;
};

typedef struct MsgY
{
    Message base;   /* Base or common structure */
    uint8_t var1[Y_VAR1_SIZE];
};

typedef struct MsgZ
{
    Message base;    /* Base or common structure */
    uint8_t var1;
    uint32_t var2;
};

Antes de continuar, surge la siguiente pregunta: con los cambios explicados ¿podremos mantener el mismo prototipo de Message_receive() para todo mensaje?, es decir, evitar modificar el código que la invoca.

Generación de mensajes

De acuerdo con la nueva definición de estructuras, se presenta a continuación la idea básica para generar mensajes. A modo ilustrativo se muestran dos funciones Comm_setMsgX() y Comm_setMsgY(), las cuales producen los mensajes X e Y, respectivamente:

static void
setMsg(Message *m, MsgId id)
{
    m->id = id;
    m->timestamp = Sys_getTimeStamp();
}

void
Comm_setMsgX(MsgX *msg_x, double var1, uint32_t var2)
{
    /* Fill the base structure */
    setMsg((Message *)msg_x, MSG_IDX);

    /* Now, complete the message attributes */
    msg_x->var1 = var1;
    msg_x->var2 = var2;
}

void
Comm_setMsgY(MsgY *msg_y, uint8_t *var1)
{
    /* Fill the base structure */
    setMsg((Message *)msg_y, MSG_IDY);
        
    /* Now, complete the message attributes */
    memcpy(msg_y->var1, var1, Y_VAR1_SIZE);
}

Donde:

    (9,20) Las funciones Comm_setMsgX() y Comm_setMsgY() establecen los valores para los mensajes X e Y. Una vez ejecutadas las funciones en cuestión, los mensajes están listos para enviarse a su destino, del ejemplo propuesto, la función Message_receive().
     (4,5) Cada mensaje tiene asociado un número que lo identifica y el tiempo de su generación, id y timestamp de la estructura Message respectivamente. La función setMsg() se encarga de completar dicha estructura para todo tipo de mensaje. Para cumplir con lo anterior, la conversión de tipo (casting) efectuada en la llamada a setMsg() permite tratar de forma segura, un puntero de tipo MsgX o MsgY como uno de tipo Message. Siendo esto último legal, transportable y garantizado por el standard de C, ya que el anidamiento de estructuras propuesto siempre alinea el miembro base al comienzo de cada instancia de la estructura, de acuerdo con ISO/IEC 9899:TC2 [10], es decir, no hay rellenos (padding) al comienzo de una estructura.
     (2) Esta función accede a la estructura base de los mensajes.
     (15) El acceso a los atributos propios del mensaje se realiza de manera tradicional.

Es claro que la función setMsg() es común para todos los mensajes, sin embargo, a modo ilustrativo, se muestran alternativas para acceder a la estructura Message, sin necesidad de invocar a setMsg().

/* Fill the base structure by setMsg() */
/* setMsg((Message *)msg_x, MSG_IDX);

/* Alternative #1 ------------------- */
((Message *)(msg_x))->id = MSG_IDX;
((Message *)(msg_x))->timestamp = Sys_getTimeStamp();

/* Alternative #2 ------------------- */
msg_x->base.id = MSG_IDX;
msg_x->base.timestamp = Sys_getTimeStamp();

/* Alternative #3 ------------------- */
Message *mb = &msg_x->base;

mb->id = MSG_IDX;
mb->timestamp = Sys_getTimeStamp();

Inclusive, para encapsular la conversión de tipos, puede recurrirse a macros, que aumentan la  legibilidad del código y ocultan los detalles innecesarios:

#define MSG_CAST_BASE(m_)    ((Message *)(m_))
#define MSG_CAST(t_, m_)    ((t_ *)(m_))
...

MSG_CAST_BASE(msg_x)->id = MSG_IDX;

Recepción de mensajes

Tal como se detalló en los párrafos anteriores, se prentende recibir y procesar nuevos mensajes, manteniendo el prototipo de la función Message_receive(), es decir, extender o simplemente modificar su funcionalidad, sin afectar el programa que la invoca, de esta manera se obtiene un mejor diseño, manteniendo las partes del código desacopladas e independientes a sus cambios internos. El siguiente fragmento muestra la idea principal.

static const char *msgName[] =
{
    "MSG_X", "MSG_Y", "MSG_Z"
};

static void
printMsg(Message *m)
{
    printf("[%08d] Message = %s with args ", m->timestamp, 
           msgName[m->id]);
}

int
Message_receive(Message *m)
{
    MsgX *msg_x;
    MsgY *msg_y;
    MsgZ *msg_z;
    int i, r = RCVMSG_OK;

    switch(m->id)
    {
        case MSGID_X:
            msg_x = MSG_CAST(MsgX, m);
            printMsg(m);
            printf("%4.2d, %04X\n",  msg_x->val1, 
                   msg_x->val2);
            break;
        case MSGID_Y:
            msg_y = MSG_CAST(MsgY, m); 
            printMsg(m);
            /*...*/
            break;
        case MSGID_Z:
            msg_z = MSG_CAST(MsgZ, m);
            printMsg(m);
            printf("%03d, %04X\n",  msg_z->val1, 
                   msg_z->val2);
            break;
        default:
            r = RCVMSG_UNKNOWN;
            break;
    }
         return r;
}

Donde:

     (14) Recibe el mensaje a procesar como un puntero de tipo Message, manteniendo el mismo prototipo que la implementación anterior, no obstante extiende el soporte de nuevos mensajes.
     (21) Dado que la estructura Message está contenida en cada mensaje, el miembro id permite identificar el tipo de mensaje por si mismo.
     (23,29,34) Identificado el tipo de mensaje, se realiza la conversión de tipo apropiada, para acceder a la información asociada del mensaje. En este caso, se utilizó la macro MSG_CAST(), para aumentar legibilidad del código y ocultar los detalles innecesarios de la conversión de tipo.
     (9) Accede y muestra la información básica del mensaje recibido.
     (24) La conversión de tipo realizada por MSG_CAST(), permite acceder fácilmente a la estructura del mensaje correspondiente, dicho de otra manera, tratar el puntero m de tipo Message como uno de tipo MsgX. Por otro lado, el uso de las variables automáticas msg_x, msg_y y msg_z para acceder al mensaje en cuestión, es sólo por cuestiones de estilo y legibilidad, ya que podría utilizarse, sin problemas, la macro MSG_CAST() cada vez que se requiera acceder a un miembro de la estructura de mensaje, por ejemplo:

case MSGID_X:
    printMsg(m);
    printf("%4.2d, %04X\n", MSG_CAST(MsgX, m)->val1,
           MSG_CAST(MsgX, m)->val2);
break;

Haciéndo uso del anidamiento de estructuras propuesto, la función Message_receive() mantiene su prototipo y extiende sus servicios, recibiendo y procesando los mensajes de interés. Cabe aclarar, que el método aplicado, agiliza el soporte de nuevos mensajes e inclusive, modificaciones en lo que respecta a los atributos asociados, evitando modificar el código que invoca a la función en cuestión.

Por otro lado, el medio y modo para la entrega de mensajes a la función Message_receive() queda fuera del alcance del artículo. No obstante, es sumamente importante resaltar que estos, siempre se entregan como punteros de tipo Message, sin embargo, son referencias a instancias de tipo más elaboradas, como MsgX o MsgY. Nuevamente, esto se logra mediante el anidamiento de estructuras propuesto y la conversión de tipo, que permite tratar de forma segura, un puntero de tipo MsgX como uno de tipo Message.

A diferencia de la generación de mensajes, en la cual un puntero de tipo MsgX o MsgY es tratado como uno de tipo Message, la recepción de mensajes trata un mensaje de tipo Message como uno de tipo MsgX o MsgY, es decir, realiza el proceso inverso. Para lo cual, primero identifica el tipo de mensaje mediante el miembro id de Message, luego, aplica la conversión correspondiente y accede de forma segura a los datos asociados (atributos) del mensaje recibido.

Formalizando la práctica

La técnica empleada finalmente nos permitió resolver la consigna de una manera particular, aún así, surge una nueva inquietud: ¿qué hay detrás de estas prácticas de programación?.

Como era de esperarse, el método descripto no es simplemente una técnica de codificación, sino más bien, es una implementación en lenguaje C que emula el concepto de herencia de clases [1,5,6] de la programación orientada a objetos (OOP). Donde una clase no es más que una estructura de C, con una particularidad especial, contiene dos tipos de características: datos (atributos) y comportamiento (operaciones), a diferencia de la estructura en C que sólo contiene datos.
Aún así, el comportamiento puede asociarse indirectamente a una estructura de C, definiendo funciones separadamente e incluyendo punteros a estas dentro de la estructura de C.
De aquí en más utilizaremos los términos clases y estructuras indistintamente, salvo que se indique lo contrario.

Específicamente, la herencia es una manera de representar clases que son un "tipo especializado de" otra clase. La regla básica es que la clase especializada hereda las características de la clase más general. Por lo tanto, todas las características de la clase más general (también conocida como clase base o padre) son también caracteristicas de la clase especializada (también conocida como clase derivada o hija), no obstante está permitido especializar y/o extender una clase más general. Por lo tanto, una de las principales ventajas de la herencia es la capacidad de reutilizar el código.

Especializar significa que una clase derivada puede redefinir las operaciones provistas por la clase base. Esto está íntimamente relacionado con el polimorfismo de los lenguajes orientados a objetos, que permite a un mismo nombre de función representar diferentes comportamientos, de acuerdo al contexto. Por supuesto, en lenguaje C no soporta implícitamente esta característica, sin embargo, puede imitarse. Generalmente, el polimorfismo se implementa en C mediante sentencias condicionales como if o switch-case, tal como se demostró en la función Message_receive() del ejemplo anterior, sin embargo, es mucho más flexible si en su lugar se utilizan punteros a función, las cuales se denominan funciones polimórficas, ya que adoptan diferentes funcionalidades (formas) de acuerdo a su contexto de operación. En lenguaje C++ estas funciones se denominan funciones virtuales.

Extender significa que una clase derivada define nuevas características, como atributos u operaciones, desde el punto de vista de su clase base. Existen diferentes maneras de implementar la extensión de clases en C, el presente artículo lo realiza incluyendo la clase base como una estructura dentro de la clase derivada, ubicada específicamente como primer miembro de esta última. Generalmente, este método se denomina herencia por composición [6].

Por lo tanto, en el ejemplo anterior, la estructura Message, común a todos los mensajes, es la estructura base, mientras que las estructuras MsgX, MsgX y MsgX son estructuras derivadas de Message, y como consecuencia, heredan los atributos de esta última. A su vez, cada una de estas extienden los atributos de la clase base. Por otro lado, y en términos de OOP, la conversión de tipo de una clase derivada a una clase base más general se denomina upcasting:

((Message *)(msg_x))->id = id; 

o de manera equivalente, encapsulando la conversión en una macro:

#define MSG_CAST_BASE(m_)    ((Message *)(m_))

/*...*/
MSG_CAST_BASE(msg_x)->id = id;

Por otro lado, la conversión de tipo de una clase base a una clase derivada se denomina downcasting. Aunque no es una práctica 100% segura, el ejemplo en cuestión la utiliza sin riesgos, ya que la clase base Message indica que tipo de clase derivada representa, mediante id.

case MSGID_X:
    /*...*/
    printf("%4.2d, %04X\n",  ((MsgX *)m)->val1,
           ((MsgX*)m)->val2);
    break; 

o de manera equivalente, encapsulando la conversión en una macro:

case MSGID_X:
    /*...*/
    printf("%4.2d, %04X\n",  MSG_CAST(MsgX, m)->val1,
           MSG_CAST(MsgX, m)->val2);
    break;

Entendamos, que la aplicación de esta técnica, y en general los conceptos de OOP al lenguaje C en embedded systems, no es una imitación caprichosa de C++ o Java, si no más bien, es la aplicación de ideas y conceptos maduros y formalizados, que bien empleados y en su "justa medida", permiten aumentar no sólo la productividad sino también la calidad del software desarrollado.
Indirectamente, estas prácticas generan un lenguaje común y formal, para el diseño de embedded software.
    Polimorfismo y funciones derivadas

    Como se explicó, una estructura derivada puede especializar o redefinir las operaciones de una estructura más general, por ejemplo, aplicando la idea de funciones virtuales proveniente de C++. Obviamente, el lenguaje C no soporta dicha característica, sin embargo, existen diversas maneras de "imitarlas". El presente artículo utiliza el anidamiento de estructuras para implementar las funciones virtuales o polimórficas.

    El siguiente fragmento de código muestra el resultado de aplicar funciones virtuales a la función Message_receive(). Comparar esta con su versión anterior.

    int
    Message_receive(Message *m)
    {
        (*m->vptr->receive)(m);
        return RCVMSG_OK;
    }
    

    Tal como sugiere Message_receive(), la implementación en lenguaje C requiere un nivel más de indirección, que permita acceder a la operación correspondiente según el tipo de mensaje recibido. Si bien no es la intención del artículo, se presentará una breve reseña a una alternativa para la implementación de funciones virtuales en C, basada en el anidamiento de estructuras.

    Básicamente, la estructura base almacena un puntero (vptr), que referencia a una tabla de punteros a función (vtbl), también conocida como tabla de funciones virtuales. Dichas funciones definen las operaciones de la clase. Con esta estrategia, toda estructura derivada que requiera especializar las operaciones heredadas, proveerá su propia tabla vtbl, la cual será referenciada por el puntero vptr. A continuación se muestra un ejemplo.

    typedef struct Message Message;
    typedef struct MsgX MsgX;
    
    typedef struct MsgVtbl MsgVtbl;
    struct MsgVtbl
    {
        void (*receive)(Message *const me); /* Virtual function */
    };
    
    typedef struct MsgXVtbl MsgXVtbl;
    struct MsgXVtbl
    {
        MsgVtbl base;                    /* Inherited functions */
        void (*control)(MsgX *const me); /* Extended function */
    };
    
    struct Message
    {
        const MsgVtbl *vptr;
        TimeStamp timestamp;
    };
    
    struct MsgX
    {
        Message base; /* Base or common structure */
        double var1;
        uint32_t var2;
    };
    

         (17) Ya no requiere del identificador "tipo de mensaje", id, sin embargo, se agrega el puntero vptr, el cual permite acceder a la tabla de funciones virtuales. De esta forma, cada estructura derivada que requiera definir las operaciones de su clase base (heredadas), debe proveer su propia tabla de funciones virtuales.
         (5) La tabla de funciones virtuales no es más que una estructura, en la que cada miembro es un puntero a función.
        (11) En este caso, la clase derivada MsgX, extiende y especializa las operaciones. La extensión la realiza agregando la función virtual control. Sin embargo, esta última no puede accederse desde su clase base.
        (23) La clase derivada MsgX no sufre cambio alguno respecto de la implementación sin soporte de funciones virtuales, esto se debe a las capacidades del anidamiento de estructuras, que encapsula las características de la clase base en una única unidad lógica. Por lo tanto, y como regla general, un  cambio que impacte en todos los mensajes, se realiza únicamente sobre la clase base, sin necesidad de modificar las clases derivadas. Aquí se demuestra, las ventajas del encapsulamiento y la abstracción.

    De acuerdo con la nueva definición de Message el puntero vptr debe apuntar a la tabla de funciones virtuales que corresponda al tipo de mensaje en cuestión.

    static void
    MsgX_receive(Message *const me)
    {
        MsgX *msg_x = MSG_CAST(MsgX, me);
        printMsg(me);
        printf("%4.2d, %04X\n",  msg_x->val1, msg_x->val2);
    }
    
    static void
    controller(MsgX *const me)
    {
        /*...*/
    }
    
    /* Initialize the virtual table for message X */
    static const MsgXVtbl vtbl =
    {
        {MsgX_receive}, /* inherited virtual function */
        controller      /* extended virtual function */
    };
    
    /* Set the virtual table for message X */
    msg_x->base.vptr =  &vtbl->base;
    
    /* or */
    /* MSG_CAST_BASE(msg_x)->vptr = &vtbl->base; */
    
    /* or */
    /* MSG_CAST_BASE(msg_x)->vptr = MSG_CAST(MsgVtbl, &vtbl); */
    
    /* Useful macro */
    #define MSG_RECEIVE(me_)    ((*(me_)->vptr->receive)(me_))
    

    Como es de suponer, ya no es necesario utilizar el miembro id de Message para identificar los tipos de mensajes, debido a que cada mensaje establece su propia función de procesamiento, la cual está asociada directamente con su tipo.

    Esto implica, que la clase base define la operación de procesamiento, y sus clases derivadas deben proveer  sus respectivas implementaciones. También podría ocurrir que la clase base provea su propia implementación de la operación receive()y que sus clases derivadas la utilicen como tal o la especialicen según requieran. El siguiente fragmento muestra la implementación provista por la clase base, a esta última podríamos denominarla "implementación por defecto".

    static void
    Msg_receive(Message *const me)
    {
        printf("[%08d] Process message\r\n", m->timestamp);
    }
    
    /* Initialize the virtual table */
    static const MsgVtbl vtbl =
    {
        Msg_receive /* inherited virtual function */
    };
    
    /* Set the object virtual table of Message type */
    msg->vptr =  &vtbl;
    

    Por otro lado, comparando las implementaciones de Message_receive() podemos ver que las funciones virtuales no sólo generan código más compacto sino también código más flexible, fácil de mantener y probar.

    Otras alternativas

    Alternativamente a la implementación previa, se muestran los detalles fundamentales de otra manera de manipular funciones virtuales y herencia en lenguaje C. La misma consiste en agregar el miembro offset a Message, de forma tal de independizar la posición relativa de esta estructura en cada una de sus derivadas, como se muestra en la definición de MsgX.

    struct Message
    {
        size_t offset;
        const MsgVtbl *vptr;
        TimeStamp timestamp;
    };
    
    struct MsgX
    {
        double var1;
        uint32_t var2;
        Message base; /* Base or common structure */
    };
    
    int
    Message_receive(Message *me)
    {
        if (me)
        {
            const MsgVtbl *vptr = me->vptr;
            size_t addr = (size_t)me;
            void *realMe = (void *)(addr - me->offset);
            (*vptr->receive)(realMe);
        }
        return RCVMSG_OK;
    } 

    Existen otras manera de implementar la herencia de clases, el polimorfismo y las funciones virtuales en C ver [1,5,6].

    Conclusiones

    La aplicación de los principios fundamentales de OOP, como el encapsulamiento, la herencia y el polimorfismo, como base del diseño de embedded systems implementados en C, no es una imitación caprichosa de lenguajes orientados a objetos, si no más bien, es la aplicación de ideas y conceptos maduros y formalizados, que bien empleados y en su "justa medida", permiten aumentar no sólo la productividad sino también la calidad del software. Asimismo, facilitan la aplicación de diagramas UML para describir y representar tanto la estructura como el comportamiento del sistema.

    Como ejemplo, RKH [8] es un framework para sistemas reactivos basado en Statecharts para embedded systems, escrito íntegramente en C, cuyo diseño se basa en objetos.

    En próximos artículos, justificaré con más ejemplos, la aplicación de los principios y técnicas de este artículo.

    Referencias

    [1] Bruce Douglass, “Design Patterns for Embedded Systems in C", Octuber 7, 2010.
    [2] Bruce Douglass, “Real-Time Design Patterns: Robust Scalable Architecture for Real-Time
    Systems", September 27, 2002. 
    [3] D. Harel, "Statecharts: A Visual Formalism for Complex Systems", Sci. Comput. Programming 8 (1987), pp. 231–274.
    [4] M. Samek, “Practical UML Statecharts in C/C++, Second Edition: Event-Driven Programming for Embedded Systems,” Elsevier, October 1, 2008.
    [5] M. Samek, “C+ 3.0 Programmer's Manual” Rev. C, May, 2007.
    [6] Dan Saks, "Alternatives Idioms for Inheritance in C", Embedded.com, April 2, 2013.
    [7] Kernighan & Ritchie, "C Programming Language (2nd Edition)", April 1, 1988.
    [8] RKH, “RKH Sourceforge download site,”, http://sourceforge.net/projects/rkh-reactivesys/
    [9] Bruce Douglass, “UML for the C Programming Language", June, 2009.
    [10] ISO/IEC International Standard, "ISO/IEC 9899:TC2: Programming languages - C", May 6, 2005

    Comentarios

    1. Coincido con voz en que OOP es un concepto y no algo asociado a un lenguaje en particular. Una forma de pensar el modelo que lleve a resolver problemas.
      Es muy bueno el enfoque que haces con la utilización de C ya que lleva ver el mecanismo involucrado, el cual, muchos programadores pierden de vista al utilizar lenguajes de mas alto nivel.
      Un lenguaje es una herramienta, y en sistemas de alto rendimiento, C es sin duda la herramienta correcta. Enfoques de OOP constituyen una forma muy buena de abordar los problemas.
      Muy buen articulo.

      ResponderEliminar

    Publicar un comentario