clojure - pattern - ¿Cómo implementar el patrón de diseño del observador de una manera puramente funcional?
patrones de diseño (4)
Digamos que quiero implementar un bus de eventos usando un lenguaje de programación OO. Podría hacer esto (pseudocódigo):
class EventBus
listeners = []
public register(listener):
listeners.add(listener)
public unregister(listener):
listeners.remove(listener)
public fireEvent(event):
for (listener in listeners):
listener.on(event)
Este es en realidad el patrón del observador, pero se usa para el flujo de control controlado por eventos de una aplicación.
¿Cómo implementaría este patrón utilizando un lenguaje de programación funcional (como uno de los sabores lisp)?
Le pregunto esto porque si uno no usa objetos, todavía necesitaría algún tipo de estado para mantener una colección de todos los oyentes. Más aún, ya que la colección de oyentes cambia con el tiempo, no sería posible crear una solución funcional pura, ¿verdad?
Más aún, ya que la colección de oyentes cambia con el tiempo, no sería posible crear una solución funcional pura, ¿verdad?
Este es un problema menor: en general, siempre que modifique el atributo de un objeto en una solución imperativa, puede calcular un nuevo objeto con el nuevo valor en una solución funcional pura. Creo que la propagación real del evento es un poco más problemática: tendría que ser implementada por una función que tome el evento, todo el conjunto de observadores potenciales más EventBus
, luego filtre los observadores reales y devuelva un conjunto completamente nuevo de Objetos con los nuevos estados de observadores computados por sus funciones de procesamiento de eventos. Los no observadores, por supuesto, serían iguales en los conjuntos de entrada y salida.
Se vuelve interesante si los observadores generan nuevos eventos en respuesta a sus métodos on
(aquí: funciones) que se llaman, en este caso, debe aplicar la función de forma recursiva (tal vez permitiendo que tome más de un evento) hasta que no produzca más eventos. para procesar.
En general, la función tomaría un evento y un conjunto de objetos y devolvería el nuevo conjunto de objetos con nuevos estados que representan todas las modificaciones resultantes de la propagación del evento.
TL; DR: Creo que modelar la propagación de eventos de manera puramente funcional es complicado.
Algunos comentarios sobre esto:
No estoy seguro de cómo se hace, pero hay algo llamado " programación reactiva funcional " que está disponible como una biblioteca para muchos lenguajes funcionales. Esto es en realidad más o menos el patrón observador hecho correctamente.
Además, el patrón de observador se usa generalmente para notificar cambios en el estado, como en las diversas implementaciones de MVC. Sin embargo, en un lenguaje funcional no hay una forma directa de hacer cambios de estado, a menos que use algunos trucos como las mónadas para simular el estado. Sin embargo, si simula los cambios de estado utilizando las mónadas, también obtendrá puntos en los que puede agregar el mecanismo de observación dentro de la mónada.
A juzgar por el código que publicaste, parece que en realidad estás haciendo programación dirigida por eventos. Por lo tanto, el patrón de observador es una forma típica de obtener programación dirigida por eventos en lenguajes orientados a objetos. Así que tienes un objetivo (programación dirigida por eventos) y una herramienta en el mundo orientado a objetos (patrón de observador). Si desea utilizar toda la potencia de la programación funcional, debe verificar qué otros métodos están disponibles para lograr este objetivo en lugar de portar directamente la herramienta desde el mundo orientado a objetos (puede que no sea la mejor opción para un lenguaje funcional). Simplemente verifique qué otras herramientas están disponibles aquí y probablemente encontrará algo que se ajuste mucho mejor a sus objetivos.
Si el patrón de Observer es esencialmente sobre editores y suscriptores, Clojure tiene un par de funciones que podría usar:
La función add-watch toma tres argumentos: una referencia, una tecla de función de observación y una función de observación que se llama cuando la referencia cambia de estado.
Claramente, debido a los cambios en el estado mutable, esto no es puramente funcional (como usted lo solicitó claramente), pero add-watcher
le dará una manera de reaccionar a los eventos, si ese es el efecto que estaba buscando, así:
(def number-cats (ref 3))
(defn updated-cat-count [k r o n]
;; Takes a function key, reference, old value and new value
(println (str "Number of cats was " o))
(println (str "Number of cats is now " n)))
(add-watch number-cats :cat-count-watcher updated-cat-count)
(dosync (alter number-cats inc))
Salida:
Number of cats was 3
Number of cats is now 4
4
Sugiero crear una referencia que contenga un conjunto de oyentes, cada uno de los cuales es una función que actúa en un evento.
Algo como:
(def listeners (ref #{}))
(defn register-listener [listener]
(dosync
(alter listeners conj listener)))
(defn unregister-listener [listener]
(dosync
(alter listeners disj listener)))
(defn fire-event [event]
(doall
(map #(% event) @listeners)))
Tenga en cuenta que está utilizando un estado mutable aquí, pero eso está bien porque el problema que está tratando de resolver explícitamente requiere un estado en términos de mantener un registro de un conjunto de oyentes.
Tenga en cuenta gracias al comentario de CAMcCann: Estoy usando un "ref" que almacena el conjunto de oyentes activos que tiene la propiedad de bonificación agradable de que la solución es segura para la concurrencia. Todas las actualizaciones se realizan protegidas por la transacción STM dentro de la construcción (dosync ....). En este caso, es posible que sea excesivo (por ejemplo, un átomo también haría el truco), pero esto puede ser útil en situaciones más complejas, por ejemplo, cuando está registrando / anulando el registro de un conjunto complejo de oyentes y desea que la actualización tenga lugar en una sola. Transation seguro para hilos.