El código espagueti no es realmente un patrón de programación, más bien un «anti patrón», por eso se debe tener en cuenta por lo menos algunos patrones avanzados de programación. Muchos estudiantes o graduados de electrónica que trabajan con sistemas embebidos desconocen los patrones o paradigmas de programación avanzados. Aunque algunos están familiarizados con el patrón Plano Secundario/Plano Principal. Todavía hay muchos que escriben «Código Espagueti» y desconocen los patrones más sofisticados como las Máquinas de Estado Finito, los Planificadores de Tareas y los Sistemas Operativos de Tiempo Real.
Lista de antipatrones de diseño
Para escribir un código más profesional y de calidad, un desarrollador de software para sistemas embebidos debe conocer y dominar gradualmente los diferentes patrones o métodos de programación más utilizados. A continuación, explicaremos brevemente cada uno de ellos. Es la inauguración de la sección de programación y es teoría pura.
Código Espagueti
El código espagueti es un término peyorativo para los programas de computación que tienen una estructura de control de flujo compleja e incomprensible. Su nombre deriva del hecho que este tipo de código parece asemejarse a un plato de espaguetis, es decir, un montón de hilos intrincados y anudados.
Tradicionalmente suele asociarse este estilo de programación con lenguajes básicos y antiguos, donde el flujo se controlaba mediante sentencias de control muy primitivas como goto y utilizando números de línea. Se caracteriza por el uso excesivo de sentencias condicionales anidadas y falta de estructura y modularidad. Esto dificulta la comprensión, el mantenimiento y la expansión del código. Además, puede llevar a errores y problemas de programación.
Ejemplo:
/* Ejemplo de Código Espagueti*/
// Controlar que la puerta esté cerrada
main() {
while(TRUE) {
while (!(puerta_cerrada()))
;
// Empezar con el llenado de agua
while (!(NIVEL_AGUA_OK)) {
if (puerta_cerrada()) {
valvula_agua(ABRIR);
compartimento_detergente(ABRIR);
}
else
valvula_agua(CERRAR);
}
//Calentar el agua
if (SELECCION_AGUA_CALIENTE) {
while (tiempo <= TIEMPO_CALENTAMIENTO) {
if (puerta_cerrada())
calentador(ON);
else
calentador(OFF);
++tiempo;
}
}
// Empezar ciclo de lavado
tiempo = 0;
while (tiempo <= TIEMPO_LAVADO) {
if (puerta_cerrada())
ciclo_lavado(ON);
else
ciclo_lavado(OFF);
++tiempo;
}
}
}
El análisis del código revela lo siguiente:
- Uso recurrente de sentencias condicionales anidadas (if – then – else, while, etc.), conocido como «Código Convolucionado», lo cual dificulta la comprensión del algoritmo y aumenta la posibilidad de errores de programación.
- Importancia de verificar si la puerta de la lavadora está cerrada en cada sección principal del programa, lo cual requiere repetir código de comprobación y aumenta la complejidad del programa. Añadir más funciones complicaría aún más el seguimiento del programa.
- Presencia de «Código de Bloqueo» en las tres funciones implementadas, donde una instrucción condicional bloquea al procesador hasta que se cumpla una condición. Esto puede afectar la ejecución de tareas críticas y dificultar la adición de funciones paralelas. Existe el riesgo de quedar atrapado en un ciclo permanente si ocurre una falla en un sensor, a menos que se implementen medidas adicionales, como una función de temporización o un «watchdog timer».
- Falta de estructura y modularidad en el Código Espagueti, lo que dificulta la expansión, comprensión, mantenimiento, depuración y mejora del sistema, tanto para el autor original como para otros programadores que puedan hacerse cargo en el futuro.
El código analizado presenta problemas de complejidad, repetición de código, bloqueo del procesador y falta de estructura, lo cual dificulta su comprensión, mantenimiento y expansión. Es necesario aplicar técnicas de programación más organizadas y modulares para mejorar la calidad del código.
Patrones de diseño
Los patrones de diseño son técnicas para resolver problemas comunes en el desarrollo de software y otros ámbitos de diseño. Surgieron a partir de la arquitectura y se popularizaron en la informática con el libro «Design Patterns» del grupo Gang of Four. Como se ha mencionado, los patrones de diseño tienen como objetivo proporcionar soluciones reutilizables, estandarizar el diseño y facilitar el aprendizaje.
Se clasifican en patrones de arquitectura, patrones de diseño y dialectos. Los antipatrones de diseño se deben tener en cuenta también, ya que previenen errores comunes. La descripción de un patrón sigue una plantilla estándar que incluye su nombre, clasificación, intención, estructura, participantes, consecuencias, implementación y ejemplos de uso.
Algunos patrones por categorías:
- Patrones creacionales – Corresponden a patrones de diseño de software que solucionan problemas de creación de instancias. Nos ayudan a encapsular y abstraer dicha creación:
- Patrones estructurales – Son los patrones de diseño software que solucionan problemas de composición (agregación) de clases y objetos
- Patrones de comportamiento – Se definen como patrones de diseño software que ofrecen soluciones respecto a la interacción y responsabilidades entre clases y objetos, así como los algoritmos que encapsulan.
También se puede categorizar según la escala o nivel de abstracción:
- Patrones de arquitectura: Aquellos que expresan un esquema organizativo estructural fundamental para sistemas de software.
- Patrones de diseño: Aquellos que expresan esquemas para definir estructuras de diseño (o sus relaciones) con las que construir sistemas de software.
- Dialectos: Patrones de bajo nivel específicos para un lenguaje de programación o entorno concreto.
Patrón «Super Lazo» (Super Loop)
El patrón Super Lazo consiste en un gran lazo cerrado que se repite indefinidamente y contiene muchas instrucciones. Este patrón puede ser válido para aplicaciones simples, pero requiere una buena estructura y claridad para evitar problemas. Una forma de mejorar la estructura del Super Lazo es agrupar el código en funciones que encapsulen el comportamiento del sistema.
Ejemplo:
/* Ejemplo de un Super Loop */
main() {
while (TRUE) {
// Muchas y muchas instrucciones aquí
// ..........
}
}
El ejemplo anteriormente mencionado como «Código Espagueti» en realidad también es un Super Lazo. No hay nada inherentemente malo en el uso del Super Lazo, ya que si se le proporciona cierta estructura, el código puede volverse más claro. Para mejorar la estructura y claridad del Super Lazo, se puede comenzar agrupando el código en funciones que encapsulen adecuadamente el comportamiento del mismo:
/* Ejemplo de un Super Loop estructurado */
main() {
while (TRUE) {
inicialización();
cargar_agua();
calentar_agua();
ciclo_lavado_1();
ciclo_lavado_2();
centrifugado();
}
}
A pesar de que en el ejemplo anterior el código parece estar mejor estructurado, todavía se requiere la presencia de código para verificar si la puerta está cerrada y retardos de tiempo en la mayoría, si no en todas, las funciones que lo necesiten.
Patrón Plano Secundario/Plano Principal (Background/Foreground)
Este patrón se basa en el Super Lazo pero incorpora el uso de interrupciones. El código se divide en dos secciones principales: el Lazo Principal (Background) y la Sección de Rutina/s de Interrupción (Foreground). El Lazo Principal contiene operaciones que no requieren un tratamiento en tiempo real, mientras que las Rutinas de Interrupción se encargan de operaciones que necesitan una respuesta inmediata.
Ejemplo:
/* Ejemplo de Patrón Background/Foreground */
/* Sección Lazo Principal (Background) - En esta sección se ubican
todas las operaciones del sistema que no necesitan tratamiento
crítico inmediato, que duran mucho tiempo y son generalmente
sincrónicas */
main() {
while (TRUE) {
// Funciones principales sin código de comprobación
// y retardos de tiempo
inicialización();
cargar_agua();
calentar_agua();
ciclo_lavado_1();
ciclo_lavado_2();
centrifugado();
}
}
/* Sección de Rutina(s) de Interrupción - En esta sección se ubican
las operaciones en el sistema que requieren tratamiento crítico inmediato, son de comparativamente corta duración y generalmente
asíncronas (pueden suceder de manera impredecible) */
void interrupt isr(void) {
/* Código que se ejecuta si la puerta de la lavadora
se abre por algún motivo */
if (pin_puerta_lavadora == OPEN)
// Bandera para comunicar a todas las funciones que
// la puerta está abierta
puerta_lavadora = OPEN;
/* Código único para generar retardos de tiempo a ser
usados por las distintas funciones del sistema */
if (timer_interrupt_flag)
// Bandera para comunicar a la función en turno
// que el retardo de tiempo ha concluido
timer_over = TRUE;
}
Máquinas de Estado Finito (Finite State Machines)
Las Máquinas de Estado Finito (MEFs) son una forma de programación que describe un sistema a través de una abstracción con características específicas:
- El sistema tiene un número finito de estados o condiciones de existencia.
- Cada estado realiza acciones definidas o muestra un comportamiento específico.
- Se conocen los eventos que causan transiciones entre estados según reglas específicas.
Ejemplo MEF en lenguaje C es mediante bloques switch – case:
/* Ejemplo de Patrón con Máquina de Estado Finito */
enum eEstados {
INICIALIZACION,
CARGAR_AGUA,
CALENTAR_AGUA,
CICLO_LAVADO_1,
CICLO_LAVADO_2,
CENTRIFUGADO } estado;
main() {
estado = INICIALIZACION;
while (TRUE) {
switch (estado) {
case INICIALIZACION:{
if (inicialización())
estado = CARGAR_AGUA;
} break;
case CARGAR_AGUA:{
if (cargar_agua())
estado = CALENTAR_AGUA;
} break;
case CALENTAR_AGUA:{
if (calentar_agua())
estado = CICLO_LAVADO_1;
} break;
case CICLO_LAVADO_1:{
if (ciclo_lavado_1())
estado = CICLO_LAVADO_2;
} break;
case CICLO_LAVADO_2:{
if (ciclo_lavado_2())
estado = CENTRIFUGADO;
} break;
case CENTRIFUGADO:{
if (centrifugado())
estado = INICIALIZACION;
} break;
}
}
}
Las MEFs definen el comportamiento del sistema. En la implementación en lenguaje C, se utilizan bloques switch-case. Algunas ventajas del uso de MEFs incluyen:
- Proporcionan abstracción y estructura al sistema, lo que facilita la comprensión del código y ayuda a evitar el código espagueti.
- Cada estado guarda información sobre eventos y estados pasados, lo que ayuda a reducir el uso de variables globales y banderas en la aplicación.
Si bien las MEFs no son adecuadas para todas las aplicaciones, se recomienda su uso en aquellas que se ajusten a este modelo.
Patrón con Planificadores de Tarea (Task Schedulers)
Un planificador de tareas decide qué tarea se ejecutará a continuación en un sistema operativo multitarea. Utiliza el estado actual de la tarea, las prioridades y un algoritmo específico para tomar esta decisión. Aunque solo se ejecuta una tarea a la vez en un procesador, el planificador permite simular la ejecución simultánea al alternar rápidamente entre las tareas. Su objetivo es optimizar el uso de recursos y garantizar una distribución equitativa del tiempo de procesamiento.
Los algoritmos de planificación más populares son:
El Planificador Equitativo asigna a cada tarea un tiempo constante llamado «rebanada» o «time slice». Este algoritmo distribuye de manera equitativa el tiempo del procesador entre todas las tareas.
Planificador Cooperativo (Cooperative Scheduler)
El Planificador Cooperativo asigna un orden de ejecución y se basa en el principio de «Correr Hasta Terminar». Las tareas cooperan y ceden el control al planificador cuando han terminado, evitando bloqueos prolongados.
Planificador Preemptivo (Preemptive Scheduler)
Los Planificadores de Tarea son sistemas operativos de tiempo real simplificados, dependiendo de la complejidad de sus algoritmos y los servicios adicionales que ofrecen. También se les conoce como Sistemas Operativos «A Medida».
Los planificadores de tareas pueden ser vistos como sistemas operativos de tiempo real simplificados. Su complejidad de implementación y los servicios adicionales que ofrecen varían. También se les conoce como sistemas operativos «a medida».
Conclusiones
En resumen, el «Código Espagueti» es un estilo de programación desorganizado y difícil de mantener, que carece de estructura y modularidad. A medida que los desarrolladores de software para sistemas embebidos se adentran en patrones de programación más avanzados, como el Super Lazo, el Patrón Plano Secundario / Plano Principal y los Sistemas Conducidos Por Eventos, pueden mejorar la calidad, seguridad y confiabilidad del código.
Estos patrones permiten una mejor organización y claridad del código, facilitan la expansión y modificación del sistema, y permiten manejar eventos en tiempo real de manera eficiente. En particular, los Sistemas de Tiempo Real y los Sistemas Conducidos Por Eventos son especialmente relevantes en aplicaciones críticas y reactivas.
Conocer y dominar estos patrones y paradigmas de programación avanzados es fundamental para los desarrolladores de software que trabajan en sistemas embebidos, ya que les permite escribir código más profesional y acorde con los estándares de calidad y seguridad requeridos en estas aplicaciones.