Sounds good to me -- but I'm not much of an expert on UI libraries. Can you provide a sketch of the intended API before you do the work? I don't know whether there are design decisions to be made -- e.g. about how to chain listeners, what happens when a listener throws an exception -- but it would help if you state 'I'm aiming to make this look like AWT listeners' (choose a UI library that you think does the job well).
From: Luc Boudreau [mailto:lucbo...@gmail.com]
Sent: Wednesday, June 10, 2009 1:36 PM
To: olap4j...@lists.sourceforge.net
Subject: [Olap4j-devel] Listener pattern implementation for Query selections
We've had a few requests to implement the listener pattern for the Query Model's selections. This is due to users developing thick clients over olap4j's Query Model. This issue was not raised previously, simply because most of our users were developing thin web clients.
So I started browsing the code and analysing the situation. Here are my findings.
The current Query Model objects require the calling code to :
There really is no point in expecting the client code to perform all those operations. Not only is it tedious work that could be pushed down inside the QueryDimension code, it is a terrible violation of the encapsulation principle. Add to this the fact that implementing listeners with such a flawed design would be pretty much useless and would not be triggered half of the time.
- Obtain a Member object from whatever means. This part is not done by the Query Model. The usual way of doing this is to use a Cube object and invoke it's lookupMember() method.
- Take the Query object, find in it's collection of QueryDimension objects the proper dimension that contains the member we searched for in the earlier step, and invoke it's createSelection() method that returns a Selection object.
- Access directly the QueryDimension selections collection (absolute yuck...) and add the Selection object we obtained at the previous step.
My proposition is this.
Insights?
- I will refactor and encapsulate properly all operations required to perform selections and push them down into the QueryDimension object; where it belongs.
- Create an enumeration of events that describe the lifecycle of the selection mechanism.
- Add method hooks for potential listeners.
- Refector the selection code and notify the registered listeners.
_____________________________
Luc Boudreau
Hi Luc,
Thanks for putting the thought and effort into this problem. It's
definitely something that will make "thick client" tools like Wabit
easier to write. Our current use of the olap4j query package in Wabit
essentially adds a wrapper around the Query object that adds event
functionality much like you've described here (also hiding the mutable
list of children!).
Here's my feedback on your proposal:
> The current Query Model is composed of three hierarchically organized
> classes: Query, QueryAxis and QueryDimension. All these classes will extend
> a newly created abstract superclass, QueryNode. A QueryNode is basically
> this :
>
> class QueryNode extends java.util.Observable
>
> It might seem redundant at first, but I expect we'll need to override some
> functions of the Observable class to support properly concurrent access to
> the list of observers.
Giving these three classes a common interface or base class does make
sense to me too, but actual use of the Observable/Observer interfaces
has been out of fashion for a number of years now. Following the newer
idiom, the QueryNode interface would look something like this:
abstract class QueryNode {
public abstract String getName();
// this can be in addition to the more type-specific
// child list accessors in subclasses
public abstract List<QueryNode> getChildNodes();
public void addQueryNodeListener(QueryNodeListener l);
public void removeQueryNodeListener(QueryNodeListener l);
protected final void fireChildNodesAdded(QueryNode addedNode) {
// create a QueryEvent object and deliver it to all listeners...
}
protected final void fireChildNodesRemoved(QueryNode removedNode) { ... }
}
There's a bit of a wrinkle here if Selection isn't a QueryNode, since
Selections can be the children in these events. I don't see an issue
with making Selection a QueryNode, though.
Listeners (observers) would implement the QueryListener interface:
interface QueryListener {
void childNodeAdded(QueryEvent e);
void childNodeRemoved(QueryEvent e);
}
The actual event object should be immutable. It would look like this:
class QueryEvent {
private final QueryNode source;
private final QueryEventType eventType;
private final QueryNode childAddedOrRemoved;
// constructor and public getters
}
The key here is that listeners know *which* child(ren) were just added
to or removed from the QueryNode. Otherwise, listeners would have to
contain error-prone code that caches the previous state of each object
they're listening to, and then perform diffs against that state to
recover the changes. Not a good situation.
Further to that, it's often essential to know *where* the children
were added within the child list. This information can be recovered in
a listener by looking for the index of each added child, but it's
harder to recover the indices of removed objects. This leads back to
the situation of error-prone code in listeners.
To me, there's an important decision to make about the event
structure: either include the child objects and their position,
issuing an insert or remove event for each child, OR provide an API
where multiple insertions or deletions can be reported in a single
event. I think the former is the better choice.
It is possible to combine the two (provide the child objects and their
indices), but in my experience this type of API is sufficiently
complicated that it invites junior programmers to cultivate bugs. I'd
only take this approach if the individual notifications became a
noticeable (and then measured and proven) performance issue.
> At first I was considering event-driven implementations, but they are all
> dependent on their respective technologies. We don't want olap4j to depend
> on AWT, SWT, Swing, or any other GUI technology. We cannot assume that users
> of the Query Model will use any of these. Even if they are all included in
> the basic JDK distributions, choosing AWT events would create lots of
> problems for Swing applications and vice-versa. The Observer class is the
> foundation of the most popular GUI libraries event handling classes, so it
> is a safe choice. The calling code will be responsible for bridging his GUI
> libraries with the Query Model's events by implementing the
> java.util.Observer interface.
I agree 100% that pulling in pieces of AWT and/or Swing would be a
mistake. Even something as seemingly-harmless as using
java.awt.Dimension (a simple (width,height) tuple) has caused us pain
on headless production servers. Its constructors secretly load native
GUI libraries!
> The java.util.Observable class is very straight forward and is used with the
> java.util.Observer interface. The Observer interface is described here :
> http://java.sun.com/javase/6/docs/api/java/util/Observer.html
I think there are two main things that have led people to abandon Observable:
1. Your object has to extend Observable to use it
2. It doesn't provide type safety in its notifications (no prescribed
type of event object)
The reusable event system which has stood the test of time is the one
in java.beans (PropertyChangeListener and PropertyChangeEvent). This
system is certainly not without its drawbacks, but its pattern is
familiar to AWT and Swing developers, since all the events in those
packages follow its example.
The showstopper for PropertyChangeEvents from the olap4j point of view
is that they don't work very well for "compound" properties (like
lists and sets), which is really the only thing olap4j wants from an
event system.
> One last thing is uncertain. If an event is triggered in one of the lower
> objects of the QueryModel hierarchy, let's say a member selection was
> performed on a QueryDimension object, the parent classes would also trigger
> an event? Simply put, should the parent classes also trigger an event if one
> of their child does? This could facilitate a lot the usage, but trigger many
> redundant events. We can't really expect calling code to register as a
> listener on every dimension of the query, right? Yet we need to be able to
> observe a given QueryDimension if this is really what we want. Some GUI
> would rather observe individual QueryDimension and some would be simpler if
> they observed the query as a whole. As long as the QueryEvent object passed
> as a parameter reflects the actual operation and can easily point to the
> originating context, it would be very easy for observers to ignore
> irrelevant events.
We've had to make the same decision over and over. On each occasion,
we've chosen not to propagate the events up the tree. I think we've
been making the right call. I can provide supporting arguments if
necessary. :)
> This solution is the least intrusive and more flexible I could think of, and
> it should fulfill it's role properly.
>
> Insights?
I think the best way to check if the system serves its purpose is to
try to use it. In this case, since the query model is a tree-like
structure, making an implementation of javax.swing.tree.TreeModel that
properly translate QueryNode events into TreeModelEvents would be a
valuable exercise. Similarly, representing the current contents of a
QueryAxis in a ListModel (again, propagating changes as
ListDataEvents) would be worth the time. We will also refactor Wabit
to use the new features of the query API, and provide further
feedback.
Thanks again for taking a crack at this! I see you've posted a new
message that the Observer-based implementation is ready since I
started working on this response. I'll take a look.
-Jonathan