Recently, I read an article (Using the Actor pattern in GUIs) presenting the Actor pattern used in GUI development. The article applies the Actor pattern in a GUI example.
Here, I am going to present a Ruby implementation of the example. For the GUI aspects, I use the Swiby framework based on Java/Swing. Therefore, the GUI part is JRuby specific.
After a quick explanation of the pattern, I am going to present the same example as the Using the Actor pattern in GUIs article written in Ruby, then explain how I implement the Actor pattern and how I build the GUI.
The principles of the Actor pattern, also Active object pattern, is to call a method that delegates the actual execution to another thread.
This pattern is useful in GUI applications because GUIs, like Java/Swing, expect udpates to execute from the GUI thread. A long running task executes in its own thread, to prevent non responsive GUI behavior, and uses this pattern to update the GUI.
The example shows a tea maker, making tea is a long running task. The tea maker task that needs time to heat water, notifies the registered listener when the water is ready by calling its handle_brewing_complete method. We can implement the tea maker as follows:
class TeaMaker attr_accessor :listener def brew() sleep 5 # the long running task @listener.handle_brewing_complete end end
The user interface presents a button to start heating the water (see the brew button in the GUI, in the following figure).
The code that executes, when the user clicks on the brew button, is running in the GUI thread. It starts the brewing task in a new thread.
button('Brew', :name => :brew) { context[:brew].enabled = false context[:state].text = 'Brewing started' Thread.new do tea_maker.brew end }
The button call, in the above listing, adds a button named brew, with Brew as text, to the window.
Everything between the opening and closing curly braces is the action handler code:
Next picture shows the GUI after the user clicked the brew button.
Once the water is ready, the brewing task must update the GUI. It does so by calling the registered listener:
@listener.handle_brewing_complete
The listener updates the GUI, in its handle_brewing_complete implementation:
def f.handle_brewing_complete() context[:state].text = 'Brewing complete' context[:brew].enabled = true end
The listener method is added to the window object stored in the f variable (more on this later), it
Next figure shows the GUI when water is ready.
Because the GUI update should not run from the brew task thread, the listener registration wraps the implementation inside an Actor object:
tea_maker.listener = Actor.new(f)
The Actor class implements the actor pattern, it delegates the execution to the GUI thread.
The code that implements the listener (brewing completion) is wrapped in an Actor instance. The actor acts as a listener but does not implement the handle_brewing_complete method. It implements the method_missing, called when a method call cannot be resolved.
def method_missing method, *args @method = method @args = args EventQueue.invokeLater self end
It stores the missing method name (symbol) and the arguments. Then calls the static method method_missing passing it-self as argument.
The EventQueue.invokeLater method takes a Runnable object and executes (later) the run method from the GUI thread. Therefore, The actor implements the Runnable interface.
The run method, here, calls a method, using the missing name, on the wrapped object.
def run @delegate.send(@method, *@args) end
Here is the Actor class:
class Actor include java.lang.Runnable def initialize delegate @delegate = delegate end def run @delegate.send(@method, *@args) end def method_missing method, *args @method = method @args = args EventQueue.invokeLater self end end
To declare that the class implements the Runnable interface use include method (in JRuby). Creating an Actor object needs the object instance to wrap as argument (see the initialize method).
The following code is the one that builds the window:
f = form { title 'Tee Maker' width 200 height 100 content { label ' ', :name => :state button('Brew', :name => :brew) { context[:brew].enabled = false context[:state].text = 'Brewing started' Thread.new do tea_maker.brew end } button('Quit') { exit } } visible true }
The code is easy to read. form creates a window, with the given title and size. It adds one label and two buttons. The handler for the brew button was presented above. The handler for the quit button exits the application. The code assigns the window to a variable named f.
Next, we use the f variable to extend the object with a new method, as described earlier, named handle_brewing_complete to register as a listener to the tea maker.
def f.handle_brewing_complete() context[:state].text = 'Brewing complete' context[:brew].enabled = true end tea_maker.listener = Actor.new(f)
Here is the whole code for the tea maker.
Run the code by executing: jruby -Iswiby/lib tee_maker.rb
.
To run the code you need to install the Swiby library (for the GUI part).