Как реализовать шаблон проектирования Observer чисто функциональным способом? - PullRequest
22 голосов
/ 05 августа 2011

Допустим, я хочу реализовать шину событий, используя язык программирования ОО. Я мог бы сделать это (псевдокод):

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)

На самом деле это шаблон наблюдателя, но он используется для управляемого приложением потока управления событиями.

Как бы вы реализовали этот шаблон, используя функциональный язык программирования (например, один из вариантов lisp)?

Я спрашиваю об этом, потому что если человек не использует объекты, ему все равно нужно какое-то состояние, чтобы поддерживать коллекцию всех слушателей. Более того, поскольку коллекция слушателей со временем меняется, невозможно было бы создать чисто функциональное решение, верно?

Ответы [ 4 ]

24 голосов
/ 05 августа 2011

Несколько замечаний по этому поводу:

Я не уверен, как это делается, но есть нечто, называемое « функционально-реактивное программирование », которое доступно в виде библиотеки для многих функциональных языков.На самом деле это более или менее правильно сделанный шаблон наблюдателя.

Также шаблон наблюдателя обычно используется для уведомления об изменениях состояния, как в различных реализациях MVC.Однако в функциональном языке нет прямого способа сделать изменения состояния, если вы не используете некоторые приемы, такие как монады, для симуляции состояния.Однако, если вы смоделируете изменения состояния, используя монады, вы также получите очки, в которые вы можете добавить механизм наблюдения внутри монады.

Судя по опубликованному вами коду, кажется, что вы на самом деле делаете программирование, управляемое событиями.Таким образом, шаблон наблюдателя является типичным способом получения событийно-ориентированного программирования на объектно-ориентированных языках.Таким образом, у вас есть цель (программирование на основе событий) и инструмент в объектно-ориентированном мире (шаблон наблюдателя).Если вы хотите использовать все возможности функционального программирования, вам следует проверить, какие другие методы доступны для достижения этой цели, вместо того, чтобы напрямую переносить инструмент из объектно-ориентированного мира (это может быть не лучшим выбором для функционального языка).Просто проверьте, какие другие инструменты доступны здесь, и вы, вероятно, найдете что-то, что намного лучше соответствует вашим целям.

22 голосов
/ 05 августа 2011

Если шаблон Observer в основном касается издателей и подписчиков, то Clojure имеет несколько функций, которые вы можете использовать:

Функция add-watch принимает три аргумента: ссылку, функциональную клавишу наблюдения и функцию наблюдения, которая вызывается при изменении состояния ссылки.

Очевидно, что из-за изменений в изменяемом состоянии это не является чисто функциональным (как вы четко просили), но add-watcher даст вам возможность реагировать на события, если вы искали тот эффект, например:

(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))

Выход:

Number of cats was 3
Number of cats is now 4
4
6 голосов
/ 05 августа 2011

Я бы предложил создать ссылку, которая содержит набор слушателей, каждый из которых является функцией, которая воздействует на событие.

Что-то вроде:

(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)))

Обратите внимание, что здесь вы используете изменяемое состояние, но это нормально, потому что проблема, которую вы пытаетесь решить, требует состояния в плане отслеживания набора слушателей.

Обратите внимание благодаря комментарию C.A.McCann: Я использую «ref», в котором хранится набор активных слушателей, который обладает приятным бонусным свойством, что решение безопасно для параллелизма. Все обновления выполняются защищенными транзакцией STM в конструкции (dosync ....). В этом случае это, возможно, излишнее (например, атом тоже справится), но это может пригодиться в более сложных ситуациях, например когда вы регистрируете / отменяете регистрацию сложного набора слушателей и хотите, чтобы обновление происходило за один потокобезопасный переход.

6 голосов
/ 05 августа 2011

Более того, поскольку коллекция слушателей со временем меняется, невозможно было бы создать чисто функциональное решение, верно?

Это меньшая проблема - в общем,всякий раз, когда вы изменяете атрибут объекта в императивном решении, вы можете вычислить новый объект с новым значением в чисто функциональном решении.Я полагаю, что фактическое распространение события является немного более проблематичным - это должно быть реализовано функцией, которая принимает событие, весь набор потенциальных наблюдателей плюс EventBus, а затем отфильтровывает фактическоенаблюдатели и возвращает совершенно новый набор объектов с новыми состояниями наблюдателей, вычисленными по их функциям обработки событий.Номера для наблюдателей, конечно, будут одинаковыми во входных и выходных наборах.

Интересно, если эти наблюдатели генерируют новые события в ответ на вызов их методов on (здесь: функции) - в этом случаеВы должны применять функцию рекурсивно (возможно, позволяя ей принимать более одного события), пока она не выдаст больше событий для обработки.

В общем случае функция принимает событие и набор объектов и возвращает новый набор объектов с новыми состояниями, представляющими все модификации, возникающие в результате распространения события.

TL; DR: Iдумаю, что моделирование распространения событий чисто функциональным способом является сложным.

...