TEMA 6: PROGRAMACIÓN DIRIGIDA POR EVENTOS.
Contenidos
1. Paradigma de programación.
- Se considera un paradigma de programación como un estilo o forma de escribir programas.
- Un paradigma de programación se caracteriza por los conceptos empleados y por la manera de abstraer los elementos utilizados en dar con la solución a un problema haciendo uso de un algoritmo.
- Esto hace que esté limitado por las características del lenguaje de programación empleado.
- Uno de los paradigmas de programación más empleados es el de la programación orientada a objetos, el cual lo vamos a estudiar a lo largo de la asignatura.
- Existen diversos paradigmas de programación y estos no tienen por qué ser excluyentes, se pueden complementar.
- Por todo ello podemos refererirnos a la programación dirigida por eventos (en adelante PDE ) como un nuevo paradigma de programación.
1.1. Otros paradigmas de programación.
- Orientado a objetos.
- Imperativo.
- Declarativo.
- Funcional.
- Lógico.
- …
2. Características de la PDE.
- Una aplicación escrita bajo el paradigma de la PDE se caracteriza por :
- La estructura básica del programa principal cambia.
- Esta pasa a ser un bucle sin fin, del cual sólo se sale para terminar la aplicación.
- Lo que pasa dentro de este bucle está determinado por los sucesos (eventos) que ocurren como consecuencia de la interacción con el mundo exterior, con el entorno en el que se ejecuta la aplicación.
- Un evento representa cualquier cambio significativo en el estado del programa.
- La estructura básica del programa principal cambia.
2.1. Esquema básico de este tipo de aplicaciones I
- Al principio de las mismas realizamos una iniciación de todo el sistema de eventos.
- Para todos los tipos de eventos que puedan ocurrir, especificamos en cuáles estamos interesados.
- Se prepara el generador o generadores de estos eventos.
- Estas tres acciones suelen estar ya implementadas en las bibliotecas destinadas a realizar PDE.
- Para todos los eventos en los que estemos interesados debemos decir qué código se ejecutará en respuesta a los mismos - ejecución diferida de código -.
2.2. Esquema básico de este tipo de aplicaciones II
- Se espera a que se vayan produciendo los eventos.
- Una vez producidos, son detectados y tratados por el dispatcher o planificador de eventos. Este se encarga de invocar el código, que previamente hemos dicho que debía ejecutarse para cada evento (manejador del evento, callback, slot, etc…).
- Todo esto se realiza de forma ininterrumpida hasta que finaliza la aplicación.
- A esta ejecución sin fin es a lo que se conoce como el bucle de espera de eventos.
- Las aplicaciones con un interfaz gráfico de usuario siguen este esquema de programación que acabamos de comentar.
2.3. Esquema básico de este tipo de aplicaciones III
- Un mismo manejador puede estar asociado a diferentes eventos.
- Un evento puede tener asociados 0 o más manejadores.
- Si asociamos más de un manejador a un evento, no debemos confiar en el orden de ejecución de los mismos.
- A la acción de asociar un manejador a un evento se la suele llamar conexión.
2.4. Programación secuencial vs. PDE.
// SECUENCIAL | // DIRIGIDA POR EVENTOS repetir | son_eventos (ev1, ev2, ev3...); presentar_menu (); | ... opc = leer_opcion (); | cuando_ocurra ( ev1, accion1 ); ... | cuando_ocurra ( ev2, accion2 ); si (opc == 1) entonces accion1 (); | repetir si (opc == 2) entonces accion2 (); | ... ... | hasta terminar hasta terminar
2.5. Diagrama
Figure 1: Esquema básico de una aplicación bajo el paradigma de la PDE.
3. PDE y C++
- C++ no dispone de ningún mecanismo en el lenguaje para realizar este tipo de programación.
- Debemos recurrir a soluciones externas para ello.
- Estas soluciones pueden ser de distinto tipo:
Usando un preprocesador especial que añade esta característica nueva al lenguaje. Es el caso de la biblioteca Qt. El preprocesador empleado se llama MOC y la parte de PDE que implementa es la que denomina signal/slot.
Qt emplea el término señal (signal) en lugar de evento y el de slot en lugar de callback.
3.1. Un ejemplo sencillo con Qt.
1: #include <QObject> 2: 3: class Counter : public QObject { 4: Q_OBJECT 5: 6: public: 7: Counter() { m_value = 0; } 8: 9: int value() const { return m_value; } 10: 11: public slots: 12: void setValue(int value); 13: 14: signals: 15: void valueChanged(int newValue); 16: 17: private: 18: int m_value; 19: };
1: void Counter::setValue(int value) { 2: if (value != m_value) { 3: m_value = value; 4: emit valueChanged(value); 5: } 6: } 7: 8: Counter a, b; 9: QObject::connect(&a, &Counter::valueChanged, 10: &b, &Counter::setValue); 11: 12: a.setValue(12); // a.value() == 12, b.value() == 12 13: b.setValue(48); // a.value() == 12, b.value() == 48
3.2. Un ejemplo sencillo con Libsigc++.
Las alternativas basadas en el uso de bibliotecas no requieren de modificaciones al lenguaje.
1: #include <sigc++/sigc++.h> 2: class AlienDetector { 3: public: 4: AlienDetector(); 5: 6: void run(); 7: 8: sigc::signal<void> signal_detected; 9: }; 10: 11: AlienDetector::AlienDetector() {} 12: 13: void AlienDetector::run() { 14: sleep(3); // wait for aliens 15: signal_detected.emit(); 16: }
1: using namespace std; 2: 3: void warn_people() { 4: cout << "There are aliens in the carpark!" << endl; 5: } 6: 7: int main() { 8: AlienDetector mydetector; 9: mydetector.signal_detected.connect( sigc::ptr_fun(warn_people) ); 10: 11: mydetector.run(); 12: 13: return 0; 14: }
4. Boost y C++
- Boost es un conjunto de bibliotecas escritas en C++ con vistas a su portabilidad (compilador, s.o., etc…).
- Cada cierto tiempo se publica una nueva versión, la cual contiene actualizaciones a bibliotecas ya existentes y/o nuevas bibliotecas.
- Cada nueva versión publicada viene acompañada de la documentación pertinente para todas y cada una de las bibliotecas que la componen.
- El proyecto Boost es importante por:
- La altísima calidad del código fuente C++ que tienen sus bibliotecas.
- Algunas de estas bibliotecas pueden pasar a formar parte de la biblioteca estándar de C++ en un futuro.
5. Boost y PDE I
- Boost dispone de una biblioteca de implementación de señales (eventos) que que permite realizar PDE: boost::signals. Nos centraremos concretamente en su versión 2 o boost::signals2.
- Esta biblioteca implementa un marco de PDE basado en la emisión de señales (eventos) y la posterior ejecución del código asociado a los mismos, los llamados slots o callbacks.
- Una señal o evento puede tener asociados 0 o más manejadores o callbacks, mientras que un callback o slot puede estar asociado con más de una señal.
6. Boost y PDE II
- A la asociación de un slot con una señal se le llama conexión.
- Los manejadores o slots se ejecutan cuando la señal se emite (el evento se ha producido).
- Un manejador se puede desconectar de la señal a la que se ha conectado previamente.
- Un manejador puede devolver un valor. En el caso de
boost::signals2
es un dato de tipoboost::optional
, este actúa como un wrapper sobre el valor real devuelto.
6.1. boost::signals2 : ejemplos I
1: // compilar con: g++ signals1.cc -o signals1 2: #include <iostream> 3: #include <boost/signals2.hpp> 4: 5: void my_first_slot () { 6: std::cout << "void my_first_slot()\n"; 7: } 8: 9: class Car { 10: public: 11: void out_of_gas_cb () { std::cout << "Gas needed!\n"; } 12: static void class_method () { std::cout << "Class Method!\n"; } 13: };
1: int main() { 2: boost::signals2::signal<void ()> sig; 3: Car c; 4: 5: sig.connect(my_first_slot); 6: 7: ///////////////////////////////////////////////////// 8: // Caso especial: // 9: // El callback o slot es un método de una clase. // 10: ///////////////////////////////////////////////////// 11: sig.connect( std::bind(&Car::out_of_gas_cb, &c) ); 12: sig.connect( Car::class_method ); 13: 14: std::cout << "Emitting the signal...\n"; 15: sig(); 16: }
6.2. boost::signals2 : ejemplos II
1: // compilar con: g++ signals2.cc -o signals2 2: #include <iostream> 3: #include <boost/signals2.hpp> 4: 5: class Car { 6: public: 7: Car (std::string b) { brand = b; } 8: 9: void out_of_gas_cb () { 10: std::cout << brand << ": Gas needed!\n"; 11: } 12: 13: private: 14: std::string brand; 15: };
1: int main() { 2: boost::signals2::signal<void ()> sig; 3: Car a("audi"); 4: Car s("seat"); 5: 6: //auto cna = sig.connect(boost::bind(&Car::out_of_gas, &a)); 7: boost::signals2::connection cna = sig.connect(std::bind (&Car::out_of_gas_cb, &a)); 8: boost::signals2::connection cns = sig.connect(std::bind (&Car::out_of_gas_cb, &s)); 9: 10: std::cout << "Emitting the signal...\n"; 11: sig(); 12: 13: cna.disconnect (); // disconnect audi signal 14: 15: std::cout << "\nDisconnect & emit the signal...\n\n"; 16: sig(); 17: }
1: //==============// 2: // Uso de bind: // 3: //==============//---------------------------------------------------------// 4: // int sub(int lhs, int rhs); // returns lhs - rhs // 5: // bind(sub, 3, 4); // returns a function object whose // 6: // // operator() returns sub(3, 4) // 7: // 1) bind(sub, 3, 4)(); // 8: // 2) auto functor = bind(sub, 3, 4); // define a variable for the functor // 9: // cout << functor() << '\n'; // call the functor, returning -1. // 10: //-------------------------------------------------------------------------//
6.3. boost::signals2 : ejemplos III
1: // compilar con: g++ signals3.cc -o signals3 2: #include <iostream> 3: #include <boost/signals2.hpp> 4: 5: class Car { 6: public: 7: Car () { temp = 90; } 8: 9: void out_of_gas_cb (int dist) { 10: std::cout << "Gas needed!\nDistance to next petrol station: " 11: << dist << "km\n"; 12: } 13: 14: private: 15: int temp; 16: };
1: Int main() { 2: Car c; 3: 4: boost::signals2::signal<void (void)> sig; 5: boost::signals2::signal<void (Car*, int)> sig2; 6: 7: sig.connect ( std::bind(&Car::out_of_gas_cb, &c, 20) ); 8: sig2.connect ( &Car::out_of_gas_cb ); 9: 10: std::cout << "Emitting the signal...\n"; 11: sig(); 12: sig2(&c, 12); 13: }
6.4. boost::signals2 : ejemplos IV
1: // compilar con: g++ signals4.cc -o signals4 2: #include <iostream> 3: #include <boost/signals2.hpp> 4: 5: class Car { 6: public: 7: Car () { temp = 90; } 8: 9: int out_of_gas_cb (int dist) { 10: std::cout << "Gas needed!\nDistance to next petrol station: " 11: << dist << "km\n"; 12: return temp; 13: } 14: 15: private: 16: int temp; 17: };
1: int main() { 2: Car c; 3: 4: boost::signals2::signal<int (void)> sig; 5: sig.connect ( std::bind(&Car::out_of_gas_cb, &c, 20) ); 6: 7: std::cout << "Emitting the signal...\n"; 8: int t = *sig(); // returns a boost::optional 9: 10: std::cout << "The temp. of car's engine is " << t << "ºC.\n"; 11: }
7. Boost y PDE III
- Si conectas varios slots a una misma señal y estos devuelven valores, no olvides echar un vistazo a Signal Return Values (Advanced).
- Si por algún motivo necesitas tener control sobre el orden de ejecución de los slots conectados a una señal, consulta la documentación sobre los llamados grupos de llamadas: Ordering Slot Call Groups (Intermediate).
8. Boost y PDE IV
- ¿Cómo capturar señales enviadas por el hardware y reconvertirlas en
señales de
boost::signals2
? - Vamos a probarlo provocando una división por 0, capturar la
excepción producida y reconvertirla en una señal de
boost::signals2
.
Para ello empleamos la función signal disponible en la biblioteca estandard de
C
.1: #include <boost/signals2.hpp> 2: #include <iostream> 3: #include <cstdlib> 4: #include <csignal> 5: #include <string> 6: 7: using namespace std; 8: 9: class Hardware; 10: using HWPtr = Hardware *; 11: 12: class Hardware { 13: public: 14: Hardware(string n) { 15: if (SIG_ERR == signal(SIGFPE, fpe)) { 16: cerr << "failure to setup signal."; 17: } 18: name = n; 19: } 20: 21: // Signals //-------------------------------------------------------- 22: boost::signals2::signal<void(HWPtr)> onDivisionByZero; 23: 24: string hw_name() { return name; } 25: private: 26: int data; 27: string name; 28: // Class methods //-------------------------------------------------- 29: static void fpe(int n); 30: }; 31: 32: Hardware gHw("Computer CPU"); 33: 34: void Hardware::fpe(int n) { 35: cerr << "Low level signal caught.\nCalling high-level signal.\n\n"; 36: gHw.data = n; 37: gHw.onDivisionByZero(&gHw); 38: } 39: 40: //-- Main program: Compilar con '-O' ---------------------------------- 41: 42: void myFPEHandler(HWPtr h) { 43: cout << "· " << h->hw_name() << " : Division por cero.\n"; 44: exit(1); 45: } 46: 47: int main(int argc, char *argv[]) { 48: gHw.onDivisionByZero.connect(myFPEHandler); 49: 50: auto n = 3; 51: int den = 0; 52: 53: cout << "Division hecha?: r= " << r << '\n'; 54: 55: auto r = n / den; 56: return 0; 57: } 58:
9. Boost y programación secuencial vs. PDE.
- Del ejemplo del Control de Misión compara las versiones secuencial y dirigida por eventos:
1: // SECUENCIAL 2: void MissionControl::follow_voyagers () { 3: ulong impulses = 0; 4: while ((not v1.is_outof_ss()) or 5: (not v2.is_outof_ss())) { 6: std::cout << "[" << ++impulses 7: << "]-------Following Voyagers-------------\n"; 8: 9: v1.travel (); 10: if (v1.is_outof_ss()) do_something_when_outof_ss(v1); 11: 12: v2.travel (); 13: if (v2.is_outof_ss()) do_something_when_outof_ss(v2); 14: } 15: }
1: // DIRIGIDA POR EVENTOS 2: void MissionControl::follow_voyagers () { 3: ulong impulses = 0; 4: while ((not v1.is_outof_ss()) or 5: (not v2.is_outof_ss())) { 6: std::cout << "[" << ++impulses 7: << "]---------Following Voyagers-----------\n"; 8: v1.travel (); 9: v2.travel (); 10: } 11: }
10. PDE y patrones de diseño. Ampliación de conocimientos.
- La PDE estudiada es una realización concreta de un patrón de diseño software. Concretamente del patrón llamado Observer.
- El libro Design Patterns: Elements of Reusable Object-Oriented Software se considera un libro básico en esta materia.
- En la página de enlaces de la web de la asignatura tienes recogido
un curso en YouTube sobre patrones de diseño en
C++
. Aquí tienes el primero de los vídeos dedicados al patrón Observer.
11. Aclaraciones
- En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.