Presentación de los principios del diseño orientado a objetos (OOD) aplicados a la programación en lenguaje C, como una manera disciplinada de organizar, diseñar y codificar software, demistificando su uso exclusivo en lenguajes o herramientas particulares. Su aplicación en sistemas de software, incluyendo los embedded systems, tienen grandes beneficios, ya que fomentan la producción de software flexible, reutilizable, transportable, legible, fácil de probar y mantener, promoviendo lo que se conoce como código limpio. A su vez, se presentarán los principios básicos para conseguir esto último, y comprender el diseño ágil de software. Finalmente, se presentarán diferentes técnicas de programación en C para lograr su implementación.
Principios del Diseño Orientado a Objetos (OOD) Aplicados en C
1. Principios del diseño
orientado a objetos (OOD)
aplicados en C
Ing. Leandro Francucci (francuccilea@gmail.com)
Vortex
10 de agosto de 2016
1
2. Objetivo
Comprender los principios fundamentales del diseño orientado a
objetos (OOD) aplicados al desarrollo de software en lenguaje C.
Y por medio de estos:
Fomentar la producción de software flexible, fácil de mantener
y reutilizable.
Lograr un diseño de software adaptable a los cambios, hoy
conocido como diseño ágil.
Disminuir el tiempo, complejidad y costo del desarrollo.
2
3. Agenda
Comprender y aplicar el encapsulamiento y la modularidad en
lenguaje C
Aplicación del proceso de análisis, generalización, parametrización e
instanciación de una solución general a un problema específico.
Identificación de instancias por referencia y por descriptores.
Creación de instancias mediante los patrones: asignación dinámica de
memoria, asignación estática de memoria, asignación desde un pool.
Abstracción mediante punteros opacos, aplicando los métodos por
definición privada y definición privada parcial.
Utilizar abstracciones, mediante la herencia y el polimorfismo en
lenguaje C
Abstracciones. ¿Porqué y cuándo aplicarlas?. Definición y aplicación
de interfaces y operaciones polimórficas.
Selección de implementación de interfaces en tiempo de compilación
y en tiempo de ejecución.
3
5. Instancia única o singleton
Supongamos que un determinado sistema procesa comandos
recibidos asincrónicamente desde un único canal de
comunicaciones.
Los cuales se constituyen por cadenas de caracteres ASCII.
La entidad que los interpreta y procesa, se ejecuta de manera
independiente, en contexto y tiempo, a la recepción de los mismos.
Por tal motivo, el sistema utiliza una estructura de datos cola, ya que la
misma permite almacenar en forma ordenada una cierta cantidad de
elementos, en este caso caracteres ASCII del tipo char, para luego, en
otro contexto y tiempo, ser extraídos y procesados acordemente,
desacoplando así, la recepción del procesamiento de los datos.
Particularmente, en esta estructura, los elementos se extraen en orden
cronológico, desde el más antiguo al más reciente, esto determina
que la cola sea una estructura de datos FIFO, en la cual el primer
elemento agregado será el primero en extraerse.
Generalmente, se implementa como un buffer circular o una lista
enlazada.
5
6. Módulo ChQueue
Archivo de inclusión, ChQueue.h.
Define sus características (atributos
y operaciones) públicas.
6
Parte del archivo de
implementación, ChQueue.c
Define las características privadas
7. Observaciones
Tanto las operaciones como los atributos de la cola de caracteres
se concentran (encapsulan) en un único par de archivos, donde el
archivo ChQueue.h hace visibles las variables y operaciones
públicas, mientras que el archivo de implementación ChQueue.c
mantiene el cuerpo de las funciones públicas y privadas, al igual
que sus variables.
Por lo general, el nombre de las operaciones públicas de un
módulo con funciones específicas, contiene un prefijo, que se
utiliza para identificar claramente a qué módulo pertenece y
además evitar que se solapen nombres entre módulos del sistema.
7
8. Módulo BitPattQueue y el
código duplicado
Ahora, el sistema requiere una nueva cola, esta vez para almacenar
patrones de datos de 48-bits, typedef unsigned char
BITPAT_T[NUM_BYTE_PAT], donde NUM_BYTE_PAT es igual a 6.
Dado que el sistema ya cuenta con una implementación probada y
funcionando de una cola (ChQueue), la solución inmediata es tan
simple como copiar (duplicar) y adaptar esta última, cambiando
nombres, tipos de datos y funciones, creando así la cola BitPattQueue.
8
¿Qué desventajas
ocasiona esta solución?
Archivo de inclusión, BitPattQueue.h
9. Generalización
Evitando la duplicación
Para evitar los potenciales inconvenientes de la solución anterior
(código duplicado), necesitamos un único módulo capaz de
administrar múltiples colas, que pueda adaptarse a las
necesidades funcionales de cada cola en particular. Por lo tanto:
Partimos de ChQueueue, analizamos los requerimientos de
ambas colas, las comparamos y encontramos sus diferencias.
Estas definirán los parámetros del nuevo módulo.
9
10. Generalización
Partimos del módulo ChQueue, probado y en pleno funcionamiento.
Generalizamos ChQueue, de modo que pueda manipular múltiples
colas, concentrando los datos de cada cola en una única y nueva
entidad, Queue.
Esto implica que exista una instancia de Queue por cada cola que se
requiera.
En este contexto, una instancia es simplemente un objeto de tipo
Queue ubicado en memoria.
Luego, Queue debe proveer al menos las operaciones básicas para
manipular una cola o instancia de Queue.
10
Generalización mediante estructura Queue
11. Abstracción
El proceso anterior genera la abstracción Queue.
La mejor manera de eliminar el código redundante es crear
abstracciones.
Al eliminarlo, tiende a lograr sistemas más fáciles de entender y
mantener.
Definición de abstracción: “la amplificación de lo esencial y la
eliminación de lo irrelevante”.
11
12. Identificando las instancias
Dado que Queue administra múltiples colas o instancias de
Queue, para asegurar que sus funciones accedan a los datos de
la instancia correcta, necesitamos indicarle de qué instancia se
trata.
Si bien existen diversas maneras de resolver dicha situación, sólo
exploraremos la identificación de instancias por referencia y por
descriptores.
Esto está directamente ligado con el tipo de interfaz que presente
Queue.
12
14. Observaciones
La manera más tradicional de indicarle de qué instancia se trata,
consiste en pasar a cada función (operación) de Queue un
puntero a la instancia en cuestión, generalmente de nombre me.
En este caso, el nombre de las operaciones se forma por
<prefijo>_<operación>(), donde el prefijo proviene del nombre
del módulo.
La palabra const luego del * en la declaración del puntero me,
asegura que las operaciones no puedan cambiar su valor, o sea,
la instancia sobre la que se intenta operar.
Si bien el método presentado se emplea asiduamente en
lenguaje C, manifiesta un inconveniente: la definición de la
estructura Queue queda innecesariamente visible (pública) al
sistema.
¿Qué potenciales problemas puede ocasionar el hecho de
publicar la estructura de datos interna del módulo?
14
16. Creando instancias
Una instancia de Queue es un objeto de tipo Queue asignado en
memoria, el cual representa en tiempo de ejecución una cola
determinada, como ChQueue o BitPattQueue.
Por definición, los objetos existen en tiempo de ejecución y en un
momento determinado, lo que significa que pueden existir y dejar de
hacerlo, a criterio del sistema.
Al instanciar, la memoria asignada se reserva durante la existencia
del objeto, desde su creación hasta su destrucción.
Existen diversas maneras de implementar la asignación de memoria,
aquí exploraremos los patrones de asignación de memoria: dinámico,
estático y pool de objetos.
Por "pool" entendemos una colección o lista de objetos disponibles y pre-
asignados en memoria.
Otro de los métodos muy utilizados en embedded, es el patrón de
bloques de tamaño fijo (no presentado en este tutorial).
Si bien la aplicación de un patrón u otro se determina según las
características particulares del sistema, es común encontrar que un
mismo embedded system de complejidad media o superior, utilice
uno o varios de los patrones listados.
16
17. Patrón de asignación
dinámica de memoria
El hecho que un objeto exista durante un momento determinado,
implica un instante de creación y otro de destrucción.
Por destrucción, entendemos que el objeto en cuestión ya no se
utiliza en el sistema y en consecuencia, es una buena
oportunidad para que el sistema recupere la memoria que le
asignó, de modo tal que, en principio, el sistema pueda re-
utilizarla.
En general, esta mecánica produce sistemas óptimos respecto de
la utilización de memoria.
La manera más tradicional y simple de asignar y liberar memoria
es mediante la asignación dinámica de memoria.
Sin embargo, presenta dos problemas: (1) la temporización no
determinística durante la asignación y liberación de memoria, y
(2) la fragmentación de memoria, esto último puede provocar
una falla fatal en el sistema.
Por dichas razones, el uso de este método debe justificarse
claramente, caso contrario, es preferible aplicar un método
alternativo.
17
18. Patrón de asignación
dinámica de memoria
18
Archivo de inclusión Queue.h
para asignación dinámica de
memoria, con operaciones
“especiales”
El constructor también suele
nombrarse:
Queue_construct(),
Queue_ctor(),
Queue_create() o
Queue_alloc()
El destructor también suele
nombrarse:
Queue_destroy(), Queue_d
tor(), Queue_close() o
Queue_free()
24. Patrón de asignación
estática de memoria
24
Inicializador estático en startup
Uso del inicializador estático en startup
Este patrón puede
aplicarse aún en
sistemas que fueron
concebidos
utilizando el patrón
de asignación
dinámica.
Por ejemplo,
eliminando el uso
del destructor en
todo el sistema.
25. Patrón de asignación de
objetos desde un pool
25
Definición de operaciones especiales identificando la instancia
mediante referencias
Definición de operaciones especiales identificando la instancia
mediante descriptores
26. Patrón de asignación de
objetos desde un pool
26
Implementación de
operaciones
especiales
identificando la
instancia mediante
descriptores
Colección de
objetos o “pool”
27. Patrón de asignación de
objetos desde un pool
27
Inicializador de ChQueue identificando la
instancia mediante descriptores
28. Aún más abstracción:
punteros opacos
28
Declara el tipo Queue como un tipo incompleto
En este contexto, un puntero opaco es uno tal que su tipo no está completo o
definido.
No permite instanciar un objeto de tipo Queue
pero si un puntero a ese tipo. ¿Porqué?
Ahora, completamos el tipo incompleto y
podemos declarar tanto punteros como objetos
de tipo Queue.
29. Método: definición privada29
Este método oculta por
completo la definición de
Queue, por lo tanto sus
atributos son privados y no
visibles al resto del sistema.
La operaciones mantienen la misma forma que con la definición
basada en referencias.
De esta manera se refuerza el encapsulamiento y la abstracción.
30. Método: definición privada30
Este método
impone que la
asignación de
memoria sea
responsabilidad del
módulo Queue
Implementación de
Queue, Queue.c
Uso del método.
31. Método: definición privada
parcial
31
Este método publica parte de la definición de Queue. Utiliza la
herencia simple implementada en lenguaje C.
Definición de la estructura pública, en
Queue.h
Definición de la estructura
privada, en Queue.c. Ver
ubicación y tipo del
miembro base
La definición de las operaciones
mantiene la forma.
33. Método: definición privada
parcial
33
Su uso no tiene cambio alguno respecto del método anterior
Puede accederse a los miembros públicos
El puntero chque es opaco
35. Un simple ejemplo
Supongamos una función, Message_receive(), que recibe
por argumento un mensaje, Z, que transporta cierta info.
El mensaje Z, se representa por:
Message_receive(), recibe y procesa el mensaje Z.
35
Una estructura struct en C es una colección de
una o más variables, agrupadas bajo un mismo
nombre. Estas se denominan miembros o
campos. Frecuentemente, definen los atributos
de la entidad que las agrupa.
36. Un simple ejemplo
Supongamos ahora que se necesitan procesar dos nuevos
mensajes, X e Y.
36
37. Premisas de la solución
Message_receive() debe ser el único acceso para la recepción
de mensajes.
Su prototipo debe mantenerse tal como fue concebido.
Debe ser flexible para soportar la extensión de nuevos mensajes y
modificaciones a los existentes.
Debe minimizar las modificaciones del programa existente.
37
38. Reordenando mensajes
De las estructuras de mensajes X, Y y Z:
Se ordenan sus miembros e identifican sus atributos comunes
Se agrega un campo para identificarlos
Luego, se agrupan los
miembros comunes en una
nueva estructura y se re-
definen los mensajes.
38
39. Mensajes re-definidos
¿Podremos mantener el mismo
prototipo de Message_receive()
para todo mensaje, evitando
modificar el código que la
invoca?
39
timestamp
id
Message
var2
var1
base
MsgX
Base classDerived class
var2
var1
timestamp
id
Storage
La estructura común (base)
se ubica como el primer
miembro de la estructura
derivada. Esto es parte
fundamental de la solución
propuesta
40. Generación de mensajes
setMsg() completa
la estructura común
entre mensajes.
Observar la conversión
de tipo (casting).
40
41. Descripción
La conversión de tipo (casting) efectuada es legal,
transportable y garantizado por el standard de C, ya que el
anidamiento de estructuras propuesto siempre alinea el
primer miembro base, al comienzo de las estructuras, de
acuerdo con IEC/IEC9899:TC2 (informalmente C99), es decir,
no hay rellenos (padding) al comienzo de una estructura.
41
Diferentes alternativas que
realizan la conversión de tipo.
42. Con macros
Para encapsular la conversión de tipo, y así aumentar la
legibilidad y ocultar los detalles innecesarios, puede
recurrirse al uso de macros.
42
43. Recepción de mensajes
Cumple con la premisa de
mantener el prototipo de
Message_receive()
(interface).
Observar la conversión de
tipo
Esta función es polimórfica,
¿porqué?.
En este tipo de función,
¿qué consecuencias tiene el
uso del switch y el miembro
id?
43
44. Descripción
Recibe el mensaje como un puntero a Message, extiende
el soporte de nuevos mensajes manteniendo su prototipo
El campo id permite identificar el tipo de mensaje
Se realiza el casting apropiado de acuerdo al mensaje
recibido.
MSG_CAST() permite tratar el puntero m de tipo Message
como uno de tipo MsgX. Por ejemplo:
44
46. En resumen
El anidamiento de estructuras permite mantener el prototipo
de Message_receive(), extendiendo sus servicios.
El método aplicado, agiliza el soporte de nuevos mensajes y
modificaciones a los atributos asociados.
Message_receive() recibe punteros a Message, aunque
son referencias a instancias de tipos más elaborados, como
MsgX o MsgY.
La generación de mensajes: trata un puntero de tipo MsgX
como uno de tipo Message.
La recepción de mensajes: trata un puntero de tipo
Message como uno de tipo MsgX.
El miembro id, permite identificar el tipo de mensaje.
46
47. Formalizando la práctica
¿Qué hay detrás de estas prácticas de programación?
El método descripto no es simplemente una técnica de
codificación
Es una implementación en C, que emula el concepto de
herencia de clases de la OOP
Una clase es una estructura de C, con una particularidad,
además de contener datos (atributos) contiene
comportamiento (operaciones), a diferencia de las
estructuras en C que sólo contienen 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.
47
48. Herencia
Es una manera de representar clases que son un “tipo
especializado de” otra clase.
La clase especializada (derivada o hija) hereda las
características de la clase más general (base o padre).
Todas las características de la clase padre son también
características de la case derivada.
Aún así, está permitido especializar y/o extender una clase
más general.
Permite la reutilización tanto del diseño como el código,
evitando su duplicación.
48
49. Mensajes derivados
Del ejemplo anterior, la estructura Message, es común a
todos los mensajes, es la estructura base.
Las estructuras MsgX, MsgY y MsgZ son derivadas de
Message.
Las estructuras derivadas extienden los atributos de la
estructura base.
49
50. Especialización
Significa que una clase derivada puede redefinir las
operaciones provistas por la clase base.
Está relacionado con el polimorfismo de lenguajes
orientados a objetos.
Permite a un mismo nombre de función representar diferentes
comportamientos, de acuerdo al contexto.
Por supuesto, C no soporta implícitamente el polimorfismo.
Aunque puede imitarse.
¿Cómo podríamos implementar el polimorfismo en C?
50
51. Extensión
Significa que una clase derivada define nuevas
características, respecto a su clase base.
Una de las maneras de implementarla en C, es incluyendo
la clase base como una estructura dentro de la clase
derivada, ubicada específicamente como primer miembro
de esta última.
Esto se denomina herencia por composición.
51
53. Upcasting
En términos de la OOP, la conversión de tipo de una clase
derivada a una clase más general se denomina
upcasting. Del ejemplo:
De manera equivalente con una macro:
53
54. Downcasting
En términos de la OOP, la conversión de tipo de una clase
base a una clase derivada se denomina downcasting.
Aunque no es una práctica 100% segura. ¿Porqué?. Del
ejemplo:
De manera equivalente con una macro:
54
55. Herencia simple en C
La estructura RKH_SBSC_T desciende de RKH_ST_T y esta de
RKH_BASE_T.
typedef struct RKH_BASE_T
{
ruint type;
const char *name;
} RKH_BASE_T;
typedef struct RKH_ST_T
{
struct RKH_BASE_T base;
RKH_ENT_ACT_T enter;
RKH_EXT_ACT_T exit;
RKHROM struct RKH_ST_T *parent;
} RKH_ST_T;
typedef struct RKH_SCMP_t
{
RKH_ST_T st;
RKHROM struct RKH_TR_T *trtbl;
RKH_PPRO_T prepro;
RKHROM void *defchild;
RKHROM struct RKH_SHIST_T *history;
} RKH_SCMP_T;
Instance of the
base structure
RKH_BASE_T
RKH_SBSC_T
RKH_BASE_T
RKH_ST_T
Members added in
the derived structure
RKH_ST_T
Members added in the
derived structure
RKH_SBSC_T
55
57. Ejemplo con polimorfismo57
ComServer, especializa (cambia el
comportamiento de) las funciones
heredadas post() y dispatch()
Cleaner hereda todas las
características de ActiveObject
ActiveObject como base provee su
propia implementación de las
funciones init(), post() y
dispatch(). Podría denominarse
comportamiento por defecto.
init()
post()
dispatch()
ActiveObject
post()
dispatch()
ComServer Cleaner
58. Ejemplo con polimorfismo58
Command no provee implementación de la función format(),
esta es abstracta.
Cada una de las estructuras BaudRate, Pin y FacilityLock,
proveen su propia implementación de la función format()
De esta manera, Client invoca a format() desconociendo
los detalles de su implementación, es decir, se abstrae de esta.
format()
Command
format()
baudrate: uint32_t
BaudRate
format()
actual: char *
new: char *
Pin
format()
facility: char *
oldPwd: char *
newPwd: char *
FacilityLock
Client
59. ¿Porqué estas prácticas?
Construir software que posea una buena estructura de
modo tal que sea flexible, fácil de mantener y reutilizable.
La aplicación de estos principios, prácticas y patrones para
mejorar la estructura y legibilidad del software, es un
proceso, no un evento, es decir se aplican continuamente,
manteniendo en todo momento el diseño del sistema tan
simple, claro y expresivo como sea posible.
A su vez, si el desarrollo se realiza en pequeños incrementos,
aplicando prácticas que provean la disciplina y la
realimentación necesaria, el software se construye
rápidamente, aún ante cambios vertiginosos de
requerimientos.
Todo esto constituyen el Desarrollo Ágil
59
60. Polimorfismo en C
Una clase derivada puede especializar o redefinir las
operaciones de su clase base. En C++ funciones virtuales.
Una alternativa para lograr el polimorfismo en C es
mediante punteros a función y anidamiento de estructuras.
Aplicando esto último a Message_receive():
60
Compararla contra su versión con switch y el miembro id.
61. Tabla de funciones virtuales
En C se requiere un nivel más de indirección, que permita
acceder a la operación correspondiente según el tipo de
estructura derivada (del ejemplo, mensaje recibido).
Las operaciones (funciones virtuales) se definen en una
tabla, vtbl (tabla de funciones virtuales).
La funciones virtuales definen las operaciones de la clase.
La vtbl es una estructura anidada.
Toda estructura derivada que requiera especializar o
extender las operaciones heredadas, proveerá su propia
vtbl. Referenciada por el puntero vptr.
61
62. Implementación en C62
Ya no se requiere el identificador de tipo
de mensaje, id.
Se agrega el puntero vptr.
Toda estructura derivada que lo
requiera, puede proveer su propia tabla
de funciones.
La tabla de funciones virtuales es
simplemente una estructura, donde
cada miembro es un puntero a función.
La clase derivada no sufre cambio
alguno respecto de la implementación
sin soporte de funciones virtuales.
¿Porqué?.
Regla general: un cambio común a
todos los mensajes, sólo impacta en la
clase base. ¿Porqué?.
¿Cuál es la razón del const y del
puntero *me?.
63. Inicialización de vtbl
Esta práctica requiere que cada instancia inicialice vptr.
Si la clase derivada sólo hereda el comportamiento de su
base, vptr se inicializa con la tabla de funciones virtuales
de su clase base, podríamos decir adopta el
comportamiento “por defecto”. Aún así, las operaciones
pueden especializarse.
Si la clase base define operaciones pero no provee
implementación alguna, esta no es “instanciable”. En cuyo
caso el comportamiento lo provee la instancia de la clase
derivada.
¿Qué ventajas provee el anidamiento de estructuras en la
tabla de funciones virtuales?
63
64. Definición de vtbl
Ya no es necesario el
miembro id de
Message. ¿Porqué?
¿Qué diferencias se
observan entre esta
implementación y la
anterior de
Message_receive()?
64
65. Esquema de memoria
resultante
65
timestamp
vptr receive
Message
var2
var1
base
MsgX MsgXVtbl
control
base
controller
MsgX_receive
MsgVtbl
Base classDerived class
Derived vtbl
class
Base vtbl
class
Virtual
functions
66. Alternativa
Agrega el miembro offset a
Message, de forma tal de
independizar la posición
relativa de esta estructura en
cada una de sus derivadas
66
67. Abstracciones
Supongamos que disponemos de un sistema que registra y
emite la trazabilidad de una aplicación.
67
Client Tracer
Client Tracer FileOut
El módulo Tracer almacena los
registros de la trazabilidad en un
archivo determinado, como una
entidad monolítica.
Ahora, delegamos la emisión de
los registros de Tracer al módulo
FileOut dedicado
exclusivamente a la
manipulación de estos registros
sobre archivos.
68. Abstracciones68
Client Tracer FileOut
StdOut
SerialOut
SocketOut
El sistema evoluciona y ahora
requiere emitir los registros a
diferentes medios.
Por lo tanto, modificamos Tracer
para lograr el soporte necesario.
De ahora en más, un cambio en
los módulos de salida puede
implicar cambios en el módulo
Tracer.
¿Cómo podríamos evitar esto
último?
69. Abstracciones69
La mejor manera de aislar o desacoplar dos o más módulos es
mediante una abstracción.
Si bien las abstracciones están fijas, representan un grupo ilimitado de
posibles comportamientos.
Ahora, Tracer accede a la abstracción, y por lo tanto permanece
“cerrado” para su modificación, ya que depende de una abstracción
que está fija. Sin embargo, su comportamiento puede extenderse
mediante la creación de nuevos módulos derivados de la abstracción.
Esto último conforma el Principio de Abierto/Cerrado (OCP).
Tracer <<interface>>
TracerOut
FileOut StdOut SerialOut SocketOut ...
Client
70. Abstracciones70
El módulo de alto nivel Tracer no depende de los módulos
de bajo nivel, FileOut, StdOut, etc. Ambos dependen de
la abstracción TracerOut.
La abstracción no depende de los detalles de la
implementación. Por el contrario, estos dependen de la
abstracción.
Esto conforma el Principio de Inversión de Dependencias
(DIP)
Tracer <<interface>>
TracerOut
FileOut StdOut SerialOut SocketOut ...
Client
71. Vinculando la implementación
con la abstracción
71
Establecida la abstracción, resta vincularla con las
implementaciones. En lenguaje C, podemos utilizar varios
métodos para lograrlo, básicamente:
Vinculación estática
Se produce al momento de compilar y/o linkear
Mediante el preprocesador o mediante el linker
Vinculación dinámica
Se efectúa en momento de ejecución
Mediante punteros a función
La elección de uno u otro depende de la relación de
compromiso entre sus ventajas y desventajas para una
aplicación determinada.
72. Vinculación preprocesador72
La solución más básica
consiste en una macro
que provea la
abstracción.
Entre otras cuestiones,
todo cambio en la
implementación de la
abstracción provoca
cambios en el código
que utiliza la
abstracción.
73. Vinculación preprocesador73
Si afrontamos un
problema de
incompatibilidad entre
la interfaz que necesita
el cliente y la provista
por el servidor,
necesitamos definir un
adaptador que
interceda entre el
cliente y el servidor.
Difícil determinar que
código se compila
realmente para
diferentes situaciones
(compilación
condicional).
74. Vinculación por linker74
TraceOut.h define la
abstracción
Cada implementación se
define en su propio
archivo, donde se respeta
la firma de la operación
definida en la
abstracción.
Se elije para linkear el
archivo de la
implementación
requerida.
Un cambio en la
implementación no
provoca un cambio en el
código que utiliza la
abstracción.
75. Vinculación dinámica
Singleton
75
La implementación se
vincula en tiempo de
ejecución.
El módulo TraceOut
administra una única
instancia (singleton).
Permite obtener un único
binario ejecutable para
todas las implementaciones.
78. Vinculación dinámica
Instancia múltiple
78
A través de me, cada
operación identifica la
instancia sobre la cual
opera.
Cada módulo se crea a
partir de TraceOut
mediante la herencia
simple.
Dentro de cada operación
se realiza un downcast
para acceder a los datos
de la clase derivada.
79. Principios S.O.L.I.D
Cada uno de los criterios y métodos presentados se basan
fundamentalmente en estos principios.
Estos forman parte de la estrategia del desarrollo ágil y adaptativo
de software.
79
Inicial Acrónimo Concepto
S SRP Principio de responsabilidad única (Single Responsibility Principle). Una clase debería tener
una única razón para cambiar.
O OCP
Principio de abierto/cerrado (Open/Closed Principle). Las entidades de software (clases,
módulos, funciones, etc) deben estar abiertas para su extensión, pero cerradas para su
modificación.
L LSP
Principio de sustitución de Liskov (Liskov Substitution Principle). Los subtipos deben ser
sustituibles por sus tipos bases.
I ISP
Principio de segregación de la interfaz (Interface Segregation Principle). la noción de que
“muchas interfaces cliente específicas son mejores que una interfaz de propósito general”
D DIP
Principio de inversión de la dependencia (Dependency Inversion Principle). Las
abstracciones no deberían depender de los detalles. Los detalles deberían depender de
las abstracciones.
80. Conclusiones
La aplicación de los principios del OOD, como el
encapsulamiento, la abstracción, la herencia, y el
polimorfismo, como fundamentos del diseño de embedded
software en C, no es una imitación caprichosa de lenguajes
orientados a objetos, sino más bien es la aplicación de
ideas y conceptos maduros y formalizados, que bien
empleados y en su “justa medida” permiten no sólo
aumentar la productividad sino también la calidad de
nuestros desarrollos.
Y así lograr software más flexible, fácil de mantener y
reutilizable.
80
82. Referencias
[1] Leandro Francucci, “Principios de OOP aplicados en C para Embedded Systems”,
Febrary, 2014, embedded-exploited.com.ar
[2] Leandro Francucci, “Modularidad, abstracción y múltiples instancias en C para
Embedded Software”, April, 2014, embedded-exploited.com.ar
[3] Dan Saks, "Alternatives Idioms for Inheritance in C", Embedded.com, April 2, 2013.
[4] Robert C. Martin, “Clean Code”, Prentice Hall, 2009
[5] Robert C. Martin, “Agile Principles Patterns and Practices in C#”, Pearson Education, 20
jul. 2006
[6] James W. Grenning, “Test Driven Development for Embedded C”, Pragmatic Bookshelf,
2011
[7] Kernighan & Ritchie, "C Programming Language (2nd Edition)", April 1, 1988.
82