Observer interface (best practices?)
-
So far always when I used an observer, I followed the API example and created a subclass of one of SketchUp's observer classes. This poses some challenges for delegating an observer event to a private method in an instance of my main class.
Very simple example:<span class="syntaxdefault"><br />class MyTool<br /> def initialize<br /> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">webdialog </span><span class="syntaxkeyword">=</span><span class="syntaxdefault"> UI</span><span class="syntaxkeyword">;;</span><span class="syntaxdefault">WebDialog</span><span class="syntaxkeyword">.new()<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># …<br /></span><span class="syntaxdefault"> Sketchup</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">active_model</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">selection</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">add_observer</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">MySelectionObserver</span><span class="syntaxkeyword">.new)<br /></span><span class="syntaxdefault"> end<br /><br /> def updateSelectionInWebDialog</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">entities</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># passes entities to webdialog…<br /></span><span class="syntaxdefault"> end<br />end<br /><br />class MySelectionObserver </span><span class="syntaxkeyword"><</span><span class="syntaxdefault"> Sketchup</span><span class="syntaxkeyword">;;</span><span class="syntaxdefault">SelectionObserver<br /> def onSelectionBulkChange</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">selection</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># needs to call instance method of MyTool instance<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># * either I keep a public globally accessible variable/constant/method to get the current instance of MyTool<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># * or I pass the MyTool instance (as self) to the initialize method of this observer instance<br /></span><span class="syntaxdefault"> end<br />end<br /></span>
Now seeing that an observer does not need to be its own class, but rather an arbitrary class (like
MyTool
) that can behave like the specific observer should = "implements the interface" (or in Ruby: "responds to"onSelectionBulkChange
), I could simplify it like this:<span class="syntaxdefault"><br /></span><span class="syntaxcomment"># implements onSelectionBulkChange from Sketchup;;SelectionObserver<br /></span><span class="syntaxdefault">class MyTool<br /> def initialize<br /> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">webdialog </span><span class="syntaxkeyword">=</span><span class="syntaxdefault"> UI</span><span class="syntaxkeyword">;;</span><span class="syntaxdefault">WebDialog</span><span class="syntaxkeyword">.new()<br /></span><span class="syntaxdefault"> Sketchup</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">active_model</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">selection</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">add_observer</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">self</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> end<br /><br /> def updateSelectionInWebDialog</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">entities</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxcomment"># passes entities to webdialog…<br /></span><span class="syntaxdefault"> end<br /><br /> def onSelectionBulkChange</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">selection</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> updateSelectionInWebDialog</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">selection</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">entities</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> end<br />end<br /></span>
This does not follow the documentation. There are also some differences:
- I could use one class (MyTool) to act as observer for different SketchUp entities (
onSelectionBulkChange
,onViewChanged
,onLayerAdded
) - SketchUp's observer classes define all callback methods with empty implementation. Thus a subclass inherits them (and you can call them but nothing happens) where as my not-subclass does not respond to all callbacks.
SketchUp currently does not raise aNotImplementedError
. But theoretically it could do so (and it's not specified by the documentation). - Once more it becomes clear that many object oriented concepts (interfaces, abstract classes/methods) don't directly apply to Ruby (which has no more than plain classes/modules). Yet Ruby programming can be very object-oriented (we would include mixin modules or extend objects with mixin modules).
How do you use observers, as subclass of a Sketchup observer class or as your own class that implements that interface?
Would it be advisable what I showed above? Is it guaranteed that SketchUp will not raise errors in future if some methods are not implemented?
- I could use one class (MyTool) to act as observer for different SketchUp entities (
-
@aerilius said:
Would it be advisable what I showed above?
NO. It is poor practice.
Observer classes should at least be a subclass of one of the API superobservers.
I WOULD like a masterSketchup::Observer
superclass (which would actually be implemented as a mixin module so it could not be instantiated,) for ALL observer superclassses.The idea of a MetaObserver is not new. Jim Foltz has been using them for years.
IF there was a master mixin superclass module, one could define a metaobserver like:module Author module Plugin class MetaObserver include(Sketchup;;Observer) # callback overrides, etc. end end end
In this way the author's class inherits the common functionality, can be listed along with ALL other observer classes as a descendant of
Sketchup::Observer
(because it is now the direct ancestor via inclusion, and appears as the first parent in the ancestor list.)@aerilius said:
Is it guaranteed that SketchUp will not raise errors in future if some methods are not implemented?
NO
I have long been pushing the idea that tool classes need a proper superclass (either
Sketchup::Tool
orUI::Tool
,) from which standard behavior and instance methods would be inherited. (There is an example I posted in the SKX Extension forum.)Ya'll may be forgetting that each
model
(of which multiple might exist on the Mac,) has it's OWNselection
collection instance object, it's OWNtools
collection instance object (which holds a set of references to model specifictool
instance objects.)(These facts negate Anton's whimsical idea that tools objects may not have to be instances. Even though technically, a module is actually an instance of class
Module
.) -
@anton_s said:
This is how I think Sketchup Observers work.
They cannot. The list (array) of observer instances would be wiped out each time a new instance was initialized, in your example.
-
@Aerilius : a few points.
Selection observers are not added via a method call upon the model, ... there is an add method upon the model's selection collection instance.
It was allowable for classes to define nested classes. Did this change in Ruby 2.0 ?
-
@anton_s said:
This is how I think Sketchup Observers work.
Then it's probably how the
Sketchup::Selection
class works, to which selection observers are added. -
@dan rathbun said:
… there is an add method upon the model's selection collection instance.
I fixed the mistake. Initially I had changed the example (model → selection) of how I wanted to illustrate the question, but it changes nothing on the core topic in question.
-
@aerilius said:
How do you use observers, as subclass of a Sketchup observer class or as your own class that implements that interface?
I do what the API docs suggest.
ONE application observer that watches the application, and attaches a model observer subclass instance to models when they are opened or created.
INSIDE the model observer instance, are instance variables that hold references to any other model specific observer instances. IE, the model observer instantiates and attaches any subordinate observers needed. The "handle" to the model and / or the model observer can be passed down into any or all of the subordinate observers.
Lastly.. some problems with private methods can be overcome by using protected methods instead.
-
@dan rathbun said:
@aerilius said:
Would it be advisable what I showed above?
NO. It is poor practice.
Observer classes should at least be a subclass of one of the API superobservers.This is likely to have proper types (
is_a?(Sketchup::Observer)
) and avoid unimplemented methods.But the current solution in the first example is overcomplicated and distracts from the idea of what an observer is used for. An observer is an object that is notified about a certain events, and in the above case the "tool" class needs to be notified somehow. I agree that this observer should implement the interface of the specific observer (in order to have the behavior of a SelectionObserver, it must implement all its methods etc.).
But (in other ooo programming languages) it is legitimate to implement multiple interfaces, and my class would be able to observe multiple API objects. In Ruby, subclass inheritance does not allow this:
class MyTool < Sketchup::SelectionObserver, Sketchup::MaterialsObserver
In Ruby something like multiple inheritance can rather be achieved by mixin modules, and interfaces probably should be done as mixin modules(?).
class MyTool include(Sketchup::SelectionObserver, Sketchup::MaterialsObserver)
So I agree totally that the API should provide the observers as modules.
@aerilius said:
Is it guaranteed that SketchUp will not raise errors in future if some methods are not implemented?
We can so far only adhere to the documentation (and everything not mentioned will break in future), but considering the limited examples, I don't see that as a source to develop further skills. So this was rather supposed to get more people involved, especially some experienced with the internals or design patterns of the API.
-
Hmm, thats a good example. Now, I won't have to use variables to tranfer data between two different class instances. Simply having 'em all in one class is good implementation.
This is how I think Sketchup Observers work.
<span class="syntaxdefault"><br />class Sketchup</span><span class="syntaxkeyword">;;</span><span class="syntaxdefault">Selection<br /><br /> def initialize<br /> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">observers </span><span class="syntaxkeyword">=</span><span class="syntaxdefault"> </span><span class="syntaxkeyword">[]<br /></span><span class="syntaxdefault"> end<br /><br /> def add_observer</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">obj</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">observers </span><span class="syntaxkeyword"><<</span><span class="syntaxdefault"> obj<br /> true<br /> end<br /><br /> def remove_observer</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">obj</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">observers</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">delete</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">obj</span><span class="syntaxkeyword">)</span><span class="syntaxdefault"> </span><span class="syntaxkeyword">?</span><span class="syntaxdefault"> true </span><span class="syntaxkeyword">;</span><span class="syntaxdefault"> false<br /> end<br /><br /> </span><span class="syntaxcomment"># Some method triggered by SketchUp private API.<br /></span><span class="syntaxdefault"> def _trigger_selection_added</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">sel</span><span class="syntaxkeyword">,</span><span class="syntaxdefault"> element</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">observers</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">each </span><span class="syntaxkeyword">{</span><span class="syntaxdefault"> </span><span class="syntaxkeyword">|</span><span class="syntaxdefault">obj</span><span class="syntaxkeyword">|<br /></span><span class="syntaxdefault"> next unless obj</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">respond_to</span><span class="syntaxkeyword">?(;</span><span class="syntaxdefault">onSelectionAdded</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> begin<br /> </span><span class="syntaxkeyword">@</span><span class="syntaxdefault">obj</span><span class="syntaxkeyword">.</span><span class="syntaxdefault">onSelectionAdded</span><span class="syntaxkeyword">(</span><span class="syntaxdefault">sel</span><span class="syntaxkeyword">,</span><span class="syntaxdefault"> element</span><span class="syntaxkeyword">)<br /></span><span class="syntaxdefault"> rescue Exception </span><span class="syntaxkeyword">=></span><span class="syntaxdefault"> e<br /> puts </span><span class="syntaxstring">"#{e}\n#{e.backtrace.first}"<br /></span><span class="syntaxdefault"> end<br /> </span><span class="syntaxkeyword">}<br /></span><span class="syntaxdefault"> end<br /><br /> </span><span class="syntaxcomment"># ...<br /></span><span class="syntaxdefault">end<br /></span>
I think the event is called only if the tool contains that method.
This might tell us that you tool isn't event supposed to be an instance. It may be a module as well. As long as the methods are accessible (self.onSelectionAdded
) your good to go.
Advertisement