Patrón Observador para Sistemas Embebidos

En esta ocasión presentaré una implementación de dicho patrón para un sistema embebido. Aunque el ejemplo y la explicación están enfocados a un micro en particular, servirán para cualquier micro/plataforma sin mayores contratiempos. Lo que sí es importante es que el código está desarrollado a partir de C++ 11.

Para mantener el formato de texto del documento original he preferido insertar el documento original para que el lector lo visualice correctamente. Dentro de dicho PDF están los enlaces al código fuente utilizado. De todos modos he incluído aquí en la entrada el texto “raw” por si alguien lo prefiere de esta manera.

Si el lector gusta puede ir directamente a la Sección 2.2 del PDF que es donde hablo de la implementación del patrón.

Háganme saber si les gustaría que implementara algún otro patrón.

 

Patrón Observador para Sistemas Embebidos

Elaboró: M. en I. Fco. Javier Rodríguez G.
Para: La comunidad hispano-hablante de desarrolladores de sistemas embebidos.

1. Introducción

Una sobre-simplificación del patrón Observador es decir que recorre una lista, elemento por elemento, llevando a cabo, de ser necesario, una acción asociada a dicho elemento. A los elementos de la lista se les dice observadores, y a aquel código que procesa la lista, se le dice el sujeto (observado). Cada elemento debe suscribirse a la lista para que pueda ser tomado en cuenta.

Este patrón es muy utilizado sistemas del tipo event-driven. Esto es, sistemas que están a la espera de que un evento suceda para realizar una o más tareas. De más está decir que los sistemas embebidos caen dentro de esta categoría. Así que usar a este patrón resultará muy conveniente, además que es fácil de implementar. Es conveniente porque un mismo evento puede disparar diversas y diferentes respuestas de una manera bastante clara y limpia. Y es de fácil implementación porque la lista de observadores puede ser un simple arreglo. De hecho, una regla de los sistemas embebidos es NO utilizar memoria dinámica, por lo que usar arreglos simples cae como anillo al dedo.

Un ejemplo de este patrón es cuando queremos hacer cosas diferentes y descorrelacionadas una vez cada segundo: leer un puerto, encender un led, actualizar el display, etc. Cada una de estas cosas podrá ser un observador. Por supuesto, los ejemplos presentados podrán ser extendidos y diversificados a diferentes disparadores, no necesariamente interrupciones.

Es importante indicar que si el sujeto observado va a estar dentro de una ISR, entonces se deben tomar precauciones adicionales para que el sistema no se “cuelgue”. Esto podría suceder si la acción de un observador es esperar por otro evento. Así, las acciones de nuestros observadores deberán ser mínimas y rápidas, y en ningún caso, esperar por otros eventos.

2. Ejemplo genérico

Antes de ver un ejemplo real de este patrón he creído conveniente mostrar un ejemplo genérico para que el lector se familiarice con las diferentes partes de las cuales se compone éste. Es importante notar que la operación Detach() no está implementada, y esto es porque una vez que el observador ha sido incluído en la lista de observadores, no esperamos sacarlo de ella. No obstante, en sistemas con memoria dinámica, como las PCs, o que corren Linux completo (Raspberry Pi), entonces sí que deberíamos implementar a Detach(). También ha de notarse que el código debería ser modularizado (archivos .cpp y .hpp para cada clase) pero para efectos de demostración, estudio y posiblemente, impresión en papel, lo he dejado todo en un mismo archivo. Recomiendo comenzar a estudiar el código a partir de la función main().

El código, correctamente formateado y coloreado, se puede ver aquí, y debe ser compilado con C++ 11 o posterior.
https://pastebin.com/embed_js/0sZrnium

/*Copyright (C) 
 * 2017 - fjrg76 at hotmail dot com
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */


// Compilar con C++ 11 o posterior

#include 

using namespace std;
// para efectos didácticos y de depuración

const int MAX_OBSERVERS = 3;


//----------------------------------------------------------------------
//  Clase base abstracta: Observador
//----------------------------------------------------------------------

struct Observador
{
	virtual void Update() = 0;
};


//----------------------------------------------------------------------
//  Clase concreta: Observador1
//----------------------------------------------------------------------
class Observador1 : public Observador
{
public:
	explicit Observador1( int _someValue );

	void Update() override;

private:
	int val;
};

Observador1::Observador1( int _someValue) : val{ _someValue }
{
	// nada
}

void Observador1::Update()
{
	cout << "El observador concreto para Observer1 ha sido notificado: ";
	cout << val << endl;
}

//----------------------------------------------------------------------
//  Clase concreta: Observador2
//----------------------------------------------------------------------
class Observador2 : public Observador
{
public:
	explicit Observador2( double _someValue );

	void Update() override;

private:
	double val;
};

Observador2::Observador2( double _someValue) : val{ _someValue }
{
	// nada
}

void Observador2::Update()
{
	cout << "El observador concreto para Observer2 ha sido notificado: ";
	cout << val << endl; } //---------------------------------------------------------------------- //  Class: Sujeto //---------------------------------------------------------------------- struct Sujeto { 	Sujeto(); 	void Attach(Observador* _newObserver); 	void Notify(); 	//void Detach(); private: 	Observador* observadores[MAX_OBSERVERS]; 	// es nuestra lista de observadores 	size_t index; 	// cantidad actual de observadores. No puede ser mayor que MAX_OBSERVERS }; Sujeto::Sujeto() : index{ 0 } { 	for( auto o : this->observadores) { o = nullptr; }
}

void Sujeto::Attach( Observador* _newObserver )
{
	if( this->index < MAX_OBSERVERS ) { 		this->observadores[ this->index ] = _newObserver;
		++this->index;
	}
	// else {
	//	manejar la condición de error
	// }
}

void Sujeto::Notify()
{
	for( auto o : this->observadores) {
		if( o != nullptr) { o->Update(); }
	}
}


//----------------------------------------------------------------------
//  Driver program
//----------------------------------------------------------------------
int main(void)
{
	Sujeto sujeto;

	Observador1 observador1( 8 );
	Observador2 observador2( 3.14 );
	Observador1 observador3( -1 );

	sujeto.Attach( &observador3 );
	sujeto.Attach( &observador2 );
	sujeto.Attach( &observador1 );

	// el evento disparador será cualquier número múltiplo de 3:

	for( size_t cont{ 1 }; cont < 20; ++cont) {
		if( cont % 3 == 0) {
			cout << "NOTIFICANDO A LOS OBSERVADORES! Cuando cont es: ";
			cout << cont << endl;

			sujeto.Notify();
		}
	}
	
	return 0;
}

2.1 Análisis

En la función main() tenemos al sujeto observado, tres observadores (dos de los cuales son del tipo Observador1, y uno del tipo Observador2) y un generador-disparador de eventos. Los observadores deben pedir ser suscritos en la lista de observadores a través de la llamada al método .Attach() del sujeto observado. Y cada vez que se genere un evento se realizará una llamada al método .Notify(), el cual es el responsable de recorrer la lista de observadores. Cada observador en dicha lista llevará a cabo la acción correspondiente.

La clase Sujeto es quien se encarga de la administración de los observadores y la correspondiente notificación, implementando a los métodos .Attach() y .Notify(), respectivamente. Esta es una clase regular, es decir, no necesariamente debe implementar alguna clase base abstracta o interfaz.

Como necesitamos que todos los observadores sean, pues, observadores, entonces tenemos que escribir una clase abstracta de la cual cada posible observador herede las características de un observador, Observador. Las clases Observador1 y Observador2 son clases concretas de la clase abstracta Observador. Éstas deberán implementar al método virtual Update() para que haga lo que se supone debe de hacer cada vez que son “notificados”; en nuestro ejemplo únicamente imprimen un mensaje en la pantalla.

2.2 Ejemplo concreto

Ahora sí, llegó el momento de escribir un ejemplo un poco más real y cercano a nuestros sistemas embebidos. El problema es muy simple: encender y apagar 3 leds de manera completamente independiente, simulando que cada uno de ellos es una sub-tarea. Vamos a necesitar al tick del sistema. El ejemplo está escrito alrededor del microcontrolador LPC812, el IDE LPCXpresso y las bibliotecas asociadas, todo bajo Linux, pero obviamente puede adaptarse para cualquier micro, siempre y cuando se cuente con el compilador C++ 11 o posterior. Le recomiendo al lector descargar e imprimir el código para seguir adecuadamente el siguiente análisis. Me estaré refiriendo a líneas de código específicas como (Lxxx), ó rangos con (Lxxx..yyy), ambas con respecto a dicho código.

2.3 Análisis

(L31..92) La clase TickCounter es una helper-class que simplificará la generación de eventos, pero NO es parte de este patrón, por lo que pueden obviarla por el momento.

(L100) Nuevamente, como nuestra lista de observadores es una lista basada en arreglos estáticos, entonces definimos el número máximo de elementos que podrá almacenar.

(L109) La clase Observador es una clase base abstracta con un único método virtual, Update(). Las clases concretas heredarán de ésta y deberán definir su propia versión de Update().

(L118..144) La clase Observador1 es una clase concreta que hereda de la clase base abstracta Observador. En (L123) y (L137..144) redefine su propia versión del método Update(). Los métodos Update() de los objetos suscritos al sujeto, es decir, aquellos objetos en la lista de observadores, serán los que se ejecuten cada vez que un evento se genere. Por eso es imprescindible que cada clase que herede de Observador implemente en forma correcta al método Update() (es decir, las firmas deben ser idénticas). Para ello le indicamos al compilador que estamos redefiniendo, override (L123), al método Update().

(L149..169) La clase Observador2 es una clase concreta que hereda de la clase base abstracta Observador. En (L154) y (L166..169) redefine su propia versión del método Update(). En nuestro ejemplo, Observador2::Update() cambia de estado al led asociado cada vez que es llamada; en cambio, Observador1::Update() cuenta un número de eventos específico, asociado a cada objeto, antes de cambiar de estado al led correspondiente.

(L174..213) La clase Sujeto es de la cual instanciaremos al sujeto bajo observación (L252). Podrá notarse que es una clase directa, es decir, no hereda de nadie. En (L178) y (L179) podemos ver la declaración de los métodos Attach() y Notify(). Attach() agrega un observador a la lista (L197..206). El constructor de esta clase (L190) establece a nullptr cada entrada de nuestra lista. De esta forma aquellas entradas vacías no serán tomadas en cuenta cada vez que se genere un evento. El método Notify(), donde sucede la magia, (L208..213) recorre la lista de observadores ejecutando el método Update() asociado a cada uno de ellos.

(L230..235) La función Systick_Handler() es la ISR del tick del sistema (para los sistemas que siguen el estándar CMSIS para los micros ARM Cortex). Se ha establecido en (L272) que se genere una interrupción cada milisegundo (no hagan caso a la (L249), es una omisión benigna). Este esquema puede seguirse para otras fuentes de interrupción, como recibir un carácter por la UART, o que una conversión del ADC ya está lista, o cualquier otra cosa que genere una interrupción. El patrón Observador es particularmente útil cuando se usa junto con el tick del sistema porque podemos hacer cosas completamente diferentes sin enredar el código: cambiar el estado de un led, y leer del teclado, y actualizar un display de 7 segmentos, e iniciar una conversión en el ADC, etc. todo a partir de un mismo evento.

En (L252) declaramos una instancia de Sujeto, sujeto. En (L254..261) declaramos tres observadores, dos del tipo Observador1 y una del tipo Observador2. Por supuesto podemos tener más clases que hereden de Observador, y también más objetos de un tipo o de otro, todo dependiendo de las necesidades de nuestra aplicación. En

(L264..267) suscribimos a los observadores a la lista de sujeto. El orden no importa, todos se ejecutarán cada vez que un evento aparezca.
En (L275..283) tenemos al ciclo principal. No hace nada mientras no suceda nada; pero, una vez que un evento se hace presente (L277), se le avisa a la ISR a través de

(L278), y finalmente, aquí está la otra parte de la magia, se ejecuta el método .Notify() del sujeto bajo observación (L280).
En este ejemplo nuestro ciclo principal está dentro de una función, pero con algunos cuidados podríamos hacer la llamada a .Notify() dentro del propio código de la ISR, lo que sea más claro y seguro para la aplicación.

2.3.1 Contra-ejemplo

Se le sugiere al lector que escriba un contra-ejemplo a nuestro ejemplo, es decir, modificar el estado de, al menos, tres LEDs, dejando espacio para otros dos, todo dentro de la ISR. Quizás ya lo ha hecho, porque en algún momento lo hemos tenido que hacer. Pero afortunadamente, ahora que conocemos al patrón Observador, nuestros códigos serán más limpios y escalables.

2.4 Conclusiones

El patrón Observador es uno de los más útiles dentro de nuestro repertorio de patrones para sistemas embebidos. Y como cualquier otro patrón está intimamente relacionado con la programación orientada a objetos y, en nuestro caso, con el lenguaje C++.
He intentado explicar los beneficios de este patrón. En mi vida profesional me he enfrentado muchas veces al hecho de que en cada evento del tick del sistema debo hacer muchas cosas separadas, y sin usar al patrón Observador, el código queda no sólo enredado, sino inseguro.
En apariencia el patrón está enredado, y así son todos los patrones de diseño. Pero con algo de trabajo y estudio el lector podrá ver que no es tan complicado. Tal vez no le quede claro a la primera, ni a la segunda, pero en la tercera será como ver la Matrix. Y una vez que se ha entendido un patrón, el usar y comprender el resto (22 de los 23 originales) será mucho más fácil.
Espero que la explicación y el ejemplo hayan sido claros, y que les sea de utilidad en el futuro.

Bibliografía y referencias

https://en.wikipedia.org/wiki/Observer_pattern
Design Patterns: Elements of Reusable Object-Oriented Software
Programming: Principles and Practice Using C++, Bjarne Stroustrup, 2014.
Memoria dinámica en sistemas embebidos, aquí y aquí.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s