martes, 17 de marzo de 2015

Patrón Observador (Observer pattern)

Fuentes:
Observer pattern: the CakePHP way, por Zen of Coding
Observer Design pattern, por Sourcemaking
Patrones de comportamiento (VI) Patrón Observer, por David García

El patrón Observador es la respuesta a la situación en la que necesitas que ciertos objetos (llamados observers) sean notificados de los cambios en otro (llamado subject). Para esto, se suscriben a los anuncios del objeto observado y actúan cuando reciben una notificación que sepan manejar.
De este modo se logra que el objeto observado no tenga que preocuparse de cuestiones que no tienen que ver con su propia responsabilidad, como podrían ser enviar un email o actualizar datos de otro objeto.
En programación de escritorio, por ejemplo, el uso prototípico es que las vistas de un modelo se actualicen si cambian datos en el mismo. El modelo no tiene que preocuparse de actualizar la vista, sino que es la vista la que "observa" al modelo para ver si ha cambiado y actuar en consecuencia.
En el caso de una aplicación web escrita en PHP con una arquitectura MVC el patrón observador puede ser muy útil para gestionar cuestiones como las notificaciones por email a los usuarios, registro de ciertas actividades, etc, que no encajan en la capa del modelo y que tampoco es que tengan un encaje muy claro en el controlador.

La situación

El patrón Observador se utiliza cuando necesitamos que uno o más objetos sepan que otro objeto ha cambiado de algún modo y actúen en consecuencia. Es adecuado para que la aplicación pueda reaccionar ante ciertos eventos que no se sabe cuándo van a ocurrir.
Por ejemplo, imaginemos una aplicación web de un colegio en la que los visitantes puedan solicitar una plaza para estudiar. Los visitantes cubrirían un formulario y, una vez validado, el sistema podría notificar por email a los propios interesados que la solicitud se ha procesado correctamente y a miembros de la administración del centro para que sepan que se ha recibido una solicitud e iniciar el proceso de trámite de la misma. Este ejemplo puede solventarse perfectamente con el uso del patrón observador.

El patrón

El patrón observer utiliza dos interfaces:
Una es para ser implementada por las clases que pueden vas a ser observadas (el subject) y tiene métodos para que los observadores (observers) se suscriban (attach), se den de baja (detach) y sean notificados por el objeto observado (notify). La Biblioteca Standard de PHP (SPL) dispone de la interfaz SplSubject que encaja perfectamente aquí, sin embargo podrías preferir crear la tuya propia o extenderla.
La otra interfaz es para la clase de observadores (observers) y contiene un método update, que responde a las notificaciones que realiza el Subject y en el que se pasa una instancia del subject. La SPL dispone de la interfaz SplObserver.
La implementación del Subject mantiene una lista de los observadores que se suscriben a sus notificaciones. En este sentido, puede ser buena idea utilizar la clase SplObjectStorage.
El método notify, envía el mensaje update a cada uno de los observadores y estos hacen lo que les corresponda, utilizando los métodos que el Subject les permita.

Beneficios

La aplicación puede reaccionar a la ocurrencia de ciertos eventos sin que las clases de la lógica de dominio tengan que ocuparse de tareas fuera de su responsabilidad, como enviar emails, registrar en logs, generar archivos o cualquier otra.
Al estar las "reacciones" encapsuladas en un observer es fácil cambiarlas o añadir nuevas implementando y registrando nuevos observadores.

Cómo realizarlo

Hay muchas formas de implementar el patrón Observer, aunque lo que se acaba de describir es su versión más esquemática.

Diseño

Lo primero es identificar el subject y las circunstancias que nos interesa que notifique. Después tenemos que pensar en las tareas que van a realizar los observers en función del estado del Subject.

Implementación

El ejemplo de implementación del patrón Observer es un Gestor de Eventos para una aplicación realizada en CakePHP 1.3. Tiene ideas tomadas de Observer pattern: the CakePHP way, por Zen of Coding, así como de la implementación del Event Manager de CakePHP 2 y 3.
En esencia, en lugar de tener muchas clases Subject, tengo una sola (EventManager) la cual utilizan las demás clases de la aplicación cuando quieren anunciar un mensaje. Los Observers se suscriben al EventManager indicando qué mensajes quieren escuchar y con qué métodos los van a atender. Además, EventManager ignora aquellos mensajes para los que no se ha registrado ningún observador.
Crear la interfaz Subject. La interfaz subject será posteriormente implementada por la clase o clases que vayan a ser observadas. Aquí tienes un ejemplo: la clase FiSubject.
<?php
interface FiSubject
{
    public function attach(FiObserver $Observer);
    public function detach(FiObserver $Observer);
    public function notify($Generator, $message);
}

?>
Crear la interfaz Observer. La interfaz observer será la que implementen los Observers. El ejemplo que se muestra aquí (FiObserver) no sigue exactamente el patrón expuesto aquí,  ya que el método update recibe un parámetro $Generator que es el objeto que "genera" la llamada al observer y que no necesariamente va a ser el subject (forma parte de un sistema de manejo de eventos para mi aplicación).
<?php

interface FiObserver{
    /**
     * Run the method registered to proccess the message and get the message Generator
     *
     * @param string $Generator
     * @param string $message
     * @return void
     * @author Fran Iglesias
     */
    public function update($Generator, $message);
    /**
     * Return a list of messages/events supported by the observer
     *
     * @return array
     * @author Fran Iglesias
     */
    public function getEvents();
}

?>
Implementar los métodos necesarios en el Subject. La clase o clases observadas deberán implementar métodos para registrar los objetos. En el ejemplo de EventManager que muestro, se utiliza una clase SplObjectStorage para registrar los observadores (en este caso se inyecta en un método init aunque podría inyectarse perfectamente en el constructor).
<?php

App::import('Lib', 'events/FiSubject');

class EventManager implements FiSubject
{
    private $observers;
    private $messages;

    public function init(SplObjectStorage $Storage)
    {
        $this->observers =& $Storage;
        $this->messages = array();
    }

    public function attach(FiObserver $Observer)
    {
        $this->observers->attach($Observer);
        $eventsSupported = $Observer->getEvents();
        $this->messages = array_merge($this->messages, $eventsSupported);
    }

    public function detach(FiObserver $Observer)
    {
        $this->observers->detach($Observer);
    }

    public function notify($Generator, $message)
    {
        if (!in_array($message, $this->messages)) {
            return false;
        }
        foreach ($this->observers as $observer) {
             $observer->update($Generator, $message);
        }
    }
}

?>
Implementar una clase abstracta AbstractObserver. Muchas veces implemento una clase abstracta a partir de una interface para poder añadir miembros y métodos comunes en las clases finales.
En este caso, la clase Abstracta está hecha a medida para un sistema de eventos global en el que los Observers pueden registrarse para atender a ciertos mensajes, respondiendo con un método que se especifica (método listen). Por otro lado, el método update lo que hace es decidir qué método del Observer debe responder al mensaje recibido. En las clases concretas sólo tengo que implementar los métodos.
<?php

class AbstractObserver implements FiObserver
{
    private $implemented;

    public function listen($message, $method)
    {
        $this->implemented[$message] = $method;
    }

    public function update($Generator, $message)
    {
        if (!array_key_exists($message, $this->implemented)) {
            return false;
        }
        $method = $this->implemented[$message];
        if (!method_exists($this, $method)) {
            throw new BadMethodCallException(get_class().'->'.$method.' Not implemented');
        }
        $this->$method($Generator);
    }

    public function getEvents()
    {
        return array_keys($this->implemented);
    }

    public function log($message, $log = 'debug')
    {
        file_put_contents(LOGS.$log.'.log', date('Y-m-d H:i > ').$message.chr(10), FILE_APPEND);
    }

}

?>
Se puede argumentar que en este caso se rompe el principio de Responsabilidad Única pues una única clase Observer responde a diversos eventos. Esto se puede solucionar creando varios Observers que respondan cada uno a un mensaje o evento o plantear los Observers con un patrón Estrategia, con un Observer Contexto que delega en diferentes Observer Estrategias en función del mensaje recibido. Como siempre, todo depende de las necesidades específicas de la aplicación.
Implementar los Observers. En los ejemplos clásicos los observers implementan un método update que responde a los cambios del Subject. Como se comenta en el párrafo anterior, en esta implementación, update decide qué método del Observer responde a qué mensaje del sistema de eventos. He aquí un ejemplo, UserObserver.
<?php

App::import('Lib', 'events/FiObserver');
App::import('Lib', 'events/AbstractObserver');
App::import('Lib', 'fi_mailer/CakeMailer');
/**
*
*/
class UserObserver extends AbstractObserver
{
    protected $Mailer;

    public function __construct()
    {
        $this->Mailer = new CakeMailer();
    }

    public function login($User)
    {
        $data = $User->read(null);
        $this->log(sprintf(__d('access','User %s login.',true), $data['User']['realname']), 'access');
    }

    public function logout($User)
    {
        $data = $User->read(null);
        $this->log(sprintf(__d('access','User %s logout.',true), $data['User']['realname']), 'access');
    }

    public function register($User)
    {
        $data = $User->read(null);
        $this->log(sprintf(__d('access','User %s registered with account %s.',true), $data['User']['realname'], $data['User']['username']), 'access');
    }

}

?>
Registrar los Observers. Aquí puedes ver un ejemplo de un archivo events.php en el que los Observers se registran en el EventManager. Este archivo se incluye desde el bootstrap.
<?php
# Load the EventManager
App::import('Lib', 'events/EventManager');

$EventManager = new EventManager();
$EventManager->init(new SplObjectStorage());
ClassRegistry::addObject('EventManager', $EventManager);

# Init and register Observers


App::import('Lib', 'School.ApplicationObserver');
$AO = new ApplicationObserver();
$AO->useMailer($Mailer);
$AO->listen('school.application.new', 'received');
// $AO->listen('school.application.opened', 'opened');
$EventManager->attach($AO);

App::import('Lib', 'Resumes.ResumeObserver');
$RO = new ResumeObserver();
$RO->useMailer($Mailer);
$RO->listen('resumes.resume.new', 'received');
$EventManager->attach($RO);

App::import('Lib', 'Access.UserObserver');
$UO = new UserObserver();
$UO->listen('access.user.login', 'login');
$UO->listen('access.user.logout', 'logout');
$UO->listen('access.user.register', 'register');
$EventManager->attach($UO);
?>

No hay comentarios: