module Athena::EventDispatcher

Overview

A Mediator and Observer pattern event library.

Athena::EventDispatcher or, AED for short, allows defining instance methods on EventListenerInterface types (observers) that will be executed when an Event is dispatched via the EventDispatcher (mediator).

All events are registered with an EventDispatcher at compile time. While the recommended usage involves using listener structs, it is also possible to add/remove event handlers dynamically at runtime. The EventDispatcher has two constructors; one that supports manual or DI initialization, while the other auto registers listeners at compile time via macros.

An event is nothing more than a class that, optionally, contains stateful information about the event. For example a HttpOnRequest event would contain a reference to the HTTP::Request object so that the listeners have access to request data. Similarly, a HttpOnResponse event would contain a reference to the HTTP::Server::Response object so that the response body/headers/status can be mutated by the listeners.

Since events and listeners are registered at compile time (via macros or DI), listeners can be added to a project seamlessly without updating any configuration, or having to instantiate a HTTP::Handler object and add it to an array for example. The main benefit of this is that an external shard that defines a listener could be installed and would inherently be picked up and used by Athena::EventDispatcher; thus making an application easily extendable.

Example

# Create a custom event.
class ExceptionEvent < AED::Event
  property? handled : Bool = false

  getter exception : Exception

  # Events can contain stateful information related to the event.
  def initialize(@exception : Exception); end
end

# Create a listener.
struct ExceptionListener
  include AED::EventListenerInterface

  # Define what events `self` is listening on as well as their priorities.
  #
  # The higher the priority the sooner that specific listener is executed.
  def self.subscribed_events : AED::SubscribedEvents
    AED::SubscribedEvents{
      ExceptionEvent => 0,
    }
  end

  # Listener handler's are `#call` instance methods restricted to the type of event it should handle.
  #
  # Multiple methods can be defined to handle multiple events within the same listener.
  #
  # Event handler's also have access to the dispatcher instance itself.
  def call(event : ExceptionEvent, dispatcher : AED::EventDispatcherInterface) : Nil
    # Do something with the `ExceptionEvent` and/or dispatcher
    event.handled = true
  end
end

# New up an `AED::EventDispatcher`, using `AED::EventDispatcher#new`.
# This overload automatically registers listeners using macros.
#
# See also `AED::EventDispatcher#new(listeners : Array(EventListenerInterface))` for a more manual/DI friendly initializer.
dispatcher = AED::EventDispatcher.new

# Instantiate our custom event.
event = ExceptionEvent.new ArgumentError.new("Test exception")

# All events are dispatched via an `AED::EventDispatcher` instance.
#
# Similarly, all listeners are registered with it.
dispatcher.dispatch event

event.handled # => true

# Additional methods also exist on the dispatcher, such as:
# * Adding/removing listeners at runtime
# * Checking the priority of a listener
# * Getting an array of listeners for a given event
# * Checking if there is a listener(s) listening on a given `AED::Event`
dispatcher.has_listeners? ExceptionEvent # => true

Getting Started

If using this component within the [Athena Framework][Athena::Framework], it is already installed and required for you. Checkout the manual for some additional information on how to use it within the framework.

If using it outside of the framework, you will first need to add it as a dependency:

dependencies:
  athena-event_dispatcher:
    github: athena-framework/event-dispatcher
    version: ~> 0.1.0

Then run shards install, being sure to require it via require "athena-event_dispatcher".

From here you will want to define any AED::EventListenerInterface and/or custom AED::Events as required by your application. You will then need a way to create/register the listeners with an AED::EventDispatcherInterface. If none of your listeners have any constructor arguments, you can most likely just call .new on the default implementation, otherwise you will need to pass it an array of the instantiated listener types.

The dispatcher should be created in a way that allows it to be used throughout the application such that any mutations that happen to the listeners are reflected on subsequent dispatches.

WARNING If using this component within the context of something that handles independent execution flows, such as a web framework, you will want there to be a dedicated dispatcher instance for each path. This ensures that one flow will not leak state to any other flow, while still allowing flow specific mutations to be used. Consider pairing this component with the [Athena::DependencyInjection][Athena::DependencyInjection--getting-started] component as a way to handle this.

Defined in:

athena-event_dispatcher.cr
event_dispatcher_interface.cr

Constant Summary

VERSION = "0.1.4"

Macro Summary

Macro Detail

macro create_listener(event, &) #

Creates a listener for the provided event. The macro's block is used as the listener.

The macro block implicitly yields event and dispatcher.

listener = AED.create_listener(SampleEvent) do
  # Do something with the event.
  event.some_method

  # A reference to the `AED::EventDispatcherInterface` is also provided.
  dispatcher.dispatch FakeEvent.new
end

[View source]