Observer Pattern Made Easy with Pharo Lightweight Observer
Lightweight Observer is an alternative to Announcement, the default Pharo implementation of the Observer Design Pattern. I started developing Lightweight Observer because I needed a small framework, with an implementation that can be easily converted to Javascript using PharoJS.
I also wanted to be straight forward to use. I had in mind beginners such as my students who often struggle when faced with many concepts and entities. This is why by default, the Lightweight Observer generates code behind the scene to allow signaling to observers any assignment to an Instance Variable (IV) of a subject. It also allows signaling addition, removal, or replacement to elements of collections referenced by a subject. Other custom events are possible, but then the developer has to add code to create event objects and dispatch them.
Getting Started
In the following we present Lightweight Observer through an example. The framework, including the full code of the example are available on github.
Install
You can install Lightweight Observer on a Pharo image by evaluating the following code snipet in a playground. All tests should be green on Pharo 7.
Metacello new baseline: 'LightweightObserver'; repository: 'github://bouraqadi/PharoMisc'; load
Defining a Subject
We use here a subject class from the example provided with Lightweight Observer. This is LoDice which instances are dice. It has two instance variables. One is a collection of faces, and the other is the current face up. A face is any object, though it is usually an integer.
LoSubject subclass: #LoDice instanceVariableNames: 'faces faceUp' classVariableNames: '' package: 'LightweightObserver-Example'
A dice understands a roll message that randomly chooses a face and make it the new face up.
LoDice>>#roll faceUp := faces atRandom
Observing a Change
Let’s observe a dice and display on the Transcript changes of its face up. First create a dice.
dice := LoDice new.
By default a dice has 6 faces which are integers from 1 to 6. Time to create an observer.
dice afterChangeOf: #faceUp do: [ : newFaceUp | Transcript cr; show: newFaceUp ].
Ensure the Transcript is open and cleared.
Transcript open. Transcript clear.
Roll the dice and watch changes displayed on the Transcript 🙂
dice roll.
API & Examples
Subjects are subclasses of LoSubject
. They inherit methods to register observers (see LoSubject
observing protocol).
Suppose you define the following subject class with accessors.
LoSubject subclass: #MySubject instanceVariableNames: 'a b set collection' classVariableNames: '' package: 'MyPackage' MySubject>>#a: anObject a := anObject MySubject>>#b: anObject b := anObject MySubject:>>#collection: newCollection collection := newCollection
Observe All Changes
First let’s define our observer. This is a counter that is incremented upon each change of an IV, as shown in the following code.
subject := MySubject new. changeCounter := 0. subject afterChangeDo: [ changeCounter := changeCounter + 1 ].
From now on, any IV assignement will lead to the counter being incremented.
subject a: #someValue. "-> changeCounter = 1" subject b: 42. "-> changeCounter = 2" subject collection: #(). "-> changeCounter = 3"
If we have a method that assigns 2 IVs (see example below), then we will have only 1 event fired, leading to the counter incremented only once.
MySubject>>#a: newA b: newB a := newA. b := newB. subject a: 35 b: 99. "-> changeCounter = 4" subject a: 0 b: -2. "-> changeCounter = 5"
Observe Specific IV Changes
We can observe a change of a specif IV. In the following example, the changeCounter is incremented only upon changes of IV a. Other changes are not captured.
subject := MySubject new. changeCounter := 0. subject afterChangeOf: #a do: [changeCounter := changeCounter + 1 ]. subject a: #someValue. "-> changeCounter = 1" subject b: #otherValue. "-> changeCounter = 1 (inchanged!)"
Stop Observing
The observation methods such as #afterChangeOf:do: answer an observer object instance of class LoGenericObserver. It does understand the stopObserving message that makes it unsubscribe from the subject.
subject := MySubject new. changeCounter := 0. observer := subject afterChangeOf: #a do: [ changeCounter := changeCounter + 1]. subject a: #someValue. "-> changeCounter = 1" observer stopObserving. subject a: #otherValue. "-> changeCounter = 1 (inchanged!)"
Observe Inserting an Element into a Collection
Lightweight Observer allows observing changes to collections. For example, you can react to replacing an element in a sequenceable collection such as
subject collection: { 11. 21. 31. }. subject afterReplaceInCollection: #collection do: [ : index : newElement : oldElement | changedIndex := index. insertedElement := newElement. removedElement := oldElement.]. subject collection at: 2 put: 42. "changedIndex = 2." "insertedElement = 42." "removedElement = 21."
The same thing can be done to observe dictionaries. But, instead of indices, we get keys.
subject collection: {#a->11. #b->21. #c->31} asDictionary. subject afterReplaceInCollection: #collection do: [ : key : newValue : oldValue | changedKey := key. insertedValue := newValue. removedValue := oldValue.]. subject collection at: #b put: 42. "changedKey = #b." "insertedValue = 42." "removedValue = 21."
When adding a new association to the dictionary, the oldValue is nil.
subject collection at: #z put: 100. "changedKey = #z." "insertedValue = 100." "removedValue = nil."
Observe Adding/Removing Elements
In collections that support element addition and removal, these event can be observed. The following example uses a Set, though it applies to other collections such as OrdredCollection.
subject := LoSubjectForTest new. subject collection: Set new. subject afterAddToCollection: #collection do: [ : newValue | addedValue := newValue]. subject afterRemoveFromCollection: #collection do: [ : newValue | removedValue := newValue]. subject collection add: 1. "-> addedValue = 1" subject collection add: 2. "-> addedValue = 2" subject collection remove: 1. "-> removedValue = 1" subject collection remove: 2. "-> removedValue = 2"
Subject Sending Custom Events
Custom events can be defined as subclass of LoEvent
. They can be created whenever needed, and then dispatched as in the following example.
Suppose we have defined the following event class.
LoEvent subclass: #MyEvent instanceVariableNames: 'data' classVariableNames: '' package: 'MyPackage' LoEvent>>#data ^data LoEvent>>#data: anObject data := anObject
Suppose we want instances of MySubject fire a MyEvent upon performing a doSomethingSpecial: message. We need to create an instance of MyEvent, set it up, and dispatch it.
MySubject>>#doSomethingSpecial: aNumber |event| event := MyEvent new. event data: aNumber * 2. self dispatch: event. ^aNumber even
Now, we can observe changes as following.
subject := MySubject new. subject when: MyEvent do: [: event | observedData := event data ]. subject doSomethingSpecial: 100. "-> observedData = 200."
Implementation
Defining Subject Classes
Subject classes inherit from LoSubject
superclass. This introduces support for code transformations that allow emitting events without requiring the developer to introduce boiler plate code. Traits would be a more flexible way of defining traits. But, there is currently a bug in Pharo 7 traits implementation that forbids doing so.
Method Wrappers to Capture IV Changes
Event generation relies on method wrappers created automatically behind the scenes upon compilation for methods that change IVs. Wrappers methods take care of emitting events dispatched to observers. Warpper methods are also deleted automatically in case developers change the code and remove statements that lead to events (e.g. IV assignments or collection changes).
Wrapper methods are installed instead of the original methods. They display the original code typed in by the developer, but their actual byte-code takes care of creating event objects and dispatching them. Wrapper methods also call basic methods. Basic methods are the ones that do the actual computations defined by the developers.
Basic methods are classified under the “private-generated” protocol. Their selectors are prefixed with basicSubjectMethod_
. They show the original code typed in by the developer.
Collection Replacement to Capture Element Change Events
Capturing element additions, removal or insertions is done dynamically. We have introduced collection subclasses that fire events to signal these changes. Upon assigning an IV with observers that watch collection events, the targeted collection is replaced by an instance of the appropriate subclass (see makeSubject:
method).
Photo by Maarten van den Heuvel on Unsplash
Leave a Reply