Observer Pattern Made Easy with Pharo Lightweight Observer

Observer Design Pattern implementation in Pharo

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

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.