Aspect...tenable(3pm)
π NAME π
Aspect::Library::Listenable β the Observer pattern, but make it events. π
(Straight from the User Contributed Perl Documentation, bestie. Manual page Aspect::Library::Listenable(3pm), and yes we're doing the whole thing, no skimming. β¨)
π SYNOPSIS π
# The class that we will make listenable
package Point;
sub new {
bless { color => 'blue' }, shift;
}
sub erase {
print 'erased!';
}
sub get_color {
$_[0]->{color};
}
sub set_color {
$_[0]->{color} = $_[1];
}
package main;
use Aspect;
use Aspect::Library::Listenable;
# Setup the simplest listenable relationship: a signal
# Define the aspect for the listenable relationship
aspect Listenable => ( Erase => call 'Point::erase' );
# Now add a listener
my $erase_listener = sub { print shift->as_string };
add_listener $point, Erase => $erase_listener;
my $point = Point->new;
$point->erase;
# prints: "erased! name:Erase, source:Point"
remove_listener $point, Erase => $erase_listener;
$point->erase;
# prints: "erased!"
# A more complex relationship: listeners get old and new color values
# and will only be notified if these values are not equal
aspect Listenable =>
(Color => call 'Point::set_color', color => 'get_color');
add_listener $point, Color =>
my $color_listener = sub { print shift->as_string };
$point->set_color('red');
# prints: "name:Color, source:Point, color:red, old_color:blue, params:red"
$point->set_color('red'); # does not print anything, color unchanged
remove_listener $point, Color => $color_listener;
# listeners can be callback, as above, or they can be objects
package ColorListener;
sub new { bless {}, shift }
sub handle_event_Color { print "new color: ". shift->color };
package main;
add_listener $point, Color => my $object_listener = ColorListener->new;
$point->set_color('green');
# prints: "new color: green"
remove_listener $point, Color => $object_listener;
# listeners can also be specific methods on objects
package EraseListener;
sub new { bless {}, shift }
sub my_erase_handler { print 'heard an erase event!' }
package main;
add_listener $point, Color =>
[my_erase_handler => my $method_listener = EraseListener->new]
$point->erase;
# prints: "heard an erase event!"
remove_listener $point, Color => $method_listener;
π¦ DESCRIPTION π¦
Okay so this is a reusable aspect for implementing the Listenable design pattern. It lets you define your listenables and the events they fire, and then you add and remove listeners on those listenables. The moment a specific method on the listenable gets called? Boom, every registered listener gets notified. It's giving group chat notifications, but for objects. π
Some examples of use are:
- A timer that allows registration of listeners. They will receive events when the timer fires. β°
- In an MVC application, as a mechanism for registering views as listeners of models. Then when models change, views receive events, which they handle by updating the display. Several views can be set as listeners for any event of any model. π¨
The Listenable pattern is a variation of the basic Observer pattern:
- Listeners can be attached to specific events fired by a listenable. Listenables can fire several types of events. In the basic Observer pattern, observers are attached to entire observables. (So it's like muting one chat instead of leaving the whole server, iykyk.)
- Listeners receive an event as their only parameter. From this event, they can get its name, source, old/new states of the listenable, and any parameters that were sent to the listenable method that fired the event. π¬
And the best part: because it's implemented using aspects, there is no change required to the listenable or listener classes. You are not required to fire events after performing interesting state changes in the listenable. The aspect will do this for you. Literally doing the work so you don't have to. π
π USING π
Creating listenable relationships between objects is done in two steps. First you must define the relationship between the classes, then you can instantiate the defined relationship between instances of these classes.
β¨ DEFINING β¨
Defining the relationships between classes is done once per program run. This is similar to how methods and classes are defined only once.
Each listenable relationship between classes is defined by one aspect, answering 3 questions:
- What is the name of the event being fired?
- What methods on what listenable objects cause events to be fired?
- What data will be present in the event object, so that listeners can gather information about the change to the listenable that caused the event to fire? This is optional. The event could carry no data at all, except its name and source.
You create a listenable aspect so:
aspect Listenable => (EVENT_NAME => POINTCUT, EVENT_DATA)
The "EVENT_DATA" part is optional. The three parameters are your answers to the questions above:
- EVENT_NAME β The string event name. A listenable can participate in several listenable aspects, each with a different event name. Another way to describe it, is that a listenable can fire several types of events.
- POINTCUT β A pointcut object (
Aspect::Pointcut) that selects "hot" methods. After these methods are run, an event will be fired. π₯ - EVENT_DATA β Optional hash of keys and values you want to add to the event before it is fired. They key is the string name of the property that will be given to the event, and the value is a string name of a method, on the listenable, that will be called to get the property value. The getter method on the listenable must exist for this to work. If you set
"EVENT_DATA", then change checking will be performed before firing. The event will only be fired, if the event data has changed. If there is no"EVENT_DATA", the event will always be fired. The"EVENT_DATA"feature is useful for providing listeners with more information about the event. Example: when listening to a selection widget, it may by used for informing listeners of the item selected.
Here is an example of transforming a selector widget, so that it will fire an event, right after it has received a click from the user. Listeners can get the selected index from the event they receive:
aspect Listenable => (
ItemSelected => call 'SelectorWidget::click',
selected_index => 'selected_index',
);
This assumes that there exists a method "SelectorWidget::selected_index", that will return the currently selected item, and a method "click", called whenever the user clicks the widget. The event will only be fired if the "selected_index" has changed. π―
Because the aspect should be created only Once during a program run, for each listenable relationship type, there are several options for choosing the place to actually create it:
- In the listenable, outside any methods or in some static initializer
- In the top level program unit
- In a Facade over some framework
- In a new class you create, which must be used by the code adding/removing listeners
Now all that is needed is some way to add and remove listener objects, from a specific listenable, so that the event will actually be handled by someone, and not just fired into the void. π³οΈ (No listeners? No witnesses. Sad.)
π ADDING AND REMOVING LISTENERS π
The simplest listener is a "CODE" ref. It can added and removed so:
use Aspect::Library::Listenable;
my $code = sub { print "event!" }
add_listener $point, Color => $code; # add
$point->set_color('red'); # $code will be run
remove_listener $point, Color => $code; # remove
$point->set_color('yellow'); # event will not fire
The event object is the only parameter received by the callback.
The other two types of listeners are object, and method:
- Object - the method
"handle_event_EVENT_NAME"will be called. - Array ref with two elements- scalar method name and listener object.
When the listener is an object, the method name to be called is computed from the event name by adding "handle_event_" in front of the event name. For example: a car object will call the method "handle_event_FrontLeftDoorOpened" on its listeners that are objects. π
When the listener is an array ref (method listener), the method name (1st element) is called on the object (2nd element). When removing this type of listener, you do not remove the array ref but the listener object, i.e. exactly like you remove an object listener.
For method listeners, you can also change the parameter list of the method. Usually, the event is the only parameter to the listener method. By changing the parameter list, you can turn any existing method into a listener method, without changing it. That's the glow-up. β¨
You change the parameter list, by providing a list of event properties, whose values will become the new parameter list. Here is how to make a "Family::set_father_name" run each time "Person::set_name" is called on the father object:
aspect Listenable => (
NameChange => call 'Person::set_name',
name => 'name',
);
$father = Person->new;
$family = Family->new;
add_listener $father, NameChange =>
[set_father_name => $family, [qw(name)]];
$father->set_name('dan'); # $family->set_father_name('dan') will be called
πΈ HANDLING EVENTS πΈ
Listener code is called with one parameter: the event. Its class is "Aspect::Listenable::Event". All events have at least these properties:
"name"β The name of the event as defined in the aspect."source"β The listenable object."params"β The event was fired because a method was called. In this property you will find an array ref of the parameters sent to that method.
Besides these properties, you can also access any properties that were defined to be in the event state, when the listenable aspect was created. For each such property, there is another, with "old_" prefixed, which holds the value of the property on the listenable, before the event was fired. π°οΈ
You access properties on the event using getters. To get the new color of a point after a "Color" event:
sub handle_event_Color {
my $event = shift;
print $event->color;
}
π CAVEATS π
Bestie, listen, the fine print is fine print for a reason. Read it. π
- Only works with hash based objects. May use
"Scalar-Footnote"in the future to get around this, or try to keep listeners in the aspect, not the listenable. - Supports removing listeners, but not aspects. Aspects will be removed and event will stop firing, but listeners will not be cleaned up from listenables. Setup your aspect only once per relationship type, and call
"aspect Listenable..."in a void context. π«Ά
π SEE ALSO
okay bestie, so if this whole aspect thing isn't giving what it needs to give, there are other options out there and we're gonna be honest about them:
Class::Listener,Class::Observableβ these two are the object-oriented girlies solving the exact same problem as this module. Same vibe, different font. BUT β and this is the tea β both of them force you to go in and change the listenable class itself, by adding the code that fires events inside your "hot" methods. So yeah, they work, but they're making you renovate your whole house just to hang a picture. π
π AUTHORS
these iconic humans built this, show some respect:
- Adam Kennedy
<adamk@cpan.org> - Marcel GrΓΌnauer
<marcel@cpan.org> - Ran Eilam
<eilara@cpan.org>
three legends, one module π β¨
Β©οΈ COPYRIGHT
the legal bit, but make it cute:
- Copyright 2001 by Marcel GrΓΌnauer
- Some parts copyright 2009 - 2013 Adam Kennedy
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. so yes bestie, go forth and remix π«Ά
ngl that's the whole page. you now know everything this manpage knows. no gatekeeping, no cap. π
and that's a wrap on Aspect...tenable(3pm), from the User Contributed Perl Documentation, living its best life at perl v5.36.0 and last touched on 2023-07-02. periodt. πβ¨
π¦ where this page comes from
- debian trixie: libaspect-perl 1.04-3 Β· Artistic-1.0-Perl OR GPL-1.0-or-later
- ubuntu noble: libaspect-perl 1.04-3 Β· Artistic-1.0-Perl OR GPL-1.0-or-later
- ubuntu resolute: libaspect-perl 1.04-3 Β· Artistic-1.0-Perl OR GPL-1.0-or-later
- ubuntu stonking: libaspect-perl 1.04-3 Β· Artistic-1.0-Perl OR GPL-1.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms