Example 2: Policy-enforcement modularization
To implement policy enforcement using OOP, you must use a combination of documentation, code reviews, and so on. Since these techniques are manual, it is easy to miss policy violations.
In this section, you'll implement policy enforcement to ensure no duplicate listener objects are added to the models, and listeners do not loiter around when the view they represent becomes usable.
Swing's MVC (Model, View, Controller) pattern includes a few unenforced assumptions. First, models let you add a listener object more than once, which leads to duplicate work if an event-notification method carries an expensive operation. Second, you can easily forget to remove listeners before destroying a view. Consider a view onto a model. You add a listener to the model. The listener typically keeps a reference to a view (as a direct field, as a side effect of the listener being the view class's inner class, or as the view class itself being the listener). Now if a programmer forgets to remove its listener from the model before destroying it, the model still holds onto the listener that holds the view object. Since a garbage collector can trace such a view object to a live model object, it would not collect it. Such a view, then, would simply loiter around consuming memory. For more details and a solution, read Java Tip 79.
You can enforce a policy to avoid both situations. In this section, I present an AOP way to handle such policy enforcement. Simply compiling your sources along with these aspects will handle these policies -- no other changes are needed. Cool, right?
I also show how to use UML (Unified Modeling Language) to describe the aspect structural view. But let's first look at the overall structure. Figure 1 shows the aspect hierarchy for event management. I use an <<aspect>> stereotype to denote aspects and a pointcut qualifier to mark pointcuts.
Base aspect: EventListenerManagement
Since implementing both concerns requires capturing joinpoints that add listeners to models, you share the code by creating a base abstract EventListenerManagement
aspect. This aspect contains an addListenerCall()
pointcut that captures calls to methods adding a listener. It uses the modelAndListenerTypeMatch()
pointcut to let derived aspects restrict methods captured by addListenerCall()
:
// EventListenerManagement.java import java.util.*; public abstract aspect EventListenerManagement { pointcut addListenerCall(Object model, EventListener listener) : call(void *.add*Listener(EventListener+)) && target(model) && args(listener) && modelAndListenerTypeMatch(); abstract pointcut modelAndListenerTypeMatch(); }
Implement the uniqueness concern
At its core, the uniqueness concern, before adding any listener, checks whether that listener was previously added. If that listener is already present, the operation does not proceed; otherwise, it adds the listener.
The EventListenerUniqueness
aspect implements the concern's core functionality, thus ensuring no duplicate listener objects in a model. That abstract aspect declares EventListenerManagement
as its parent. It advises the addListenerCall()
pointcut to check for the listener's uniqueness by looking in a list obtained by invoking getCurrentListeners()
. It proceeds with adding the listener only if the list doesn't include a listener:
// EventListenerUniqueness.java import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.table.*; public abstract aspect EventListenerUniqueness extends EventListenerManagement { void around(Object model, EventListener listener) : addListenerCall(model, listener) { EventListener[] listeners = getCurrentListeners(model); if (!Utils.isInArray(listeners, listener)) { System.out.println("Accepting " + listener); proceed(model, listener); } else { System.out.println("Already listening " + listener); } } public abstract EventListener[] getCurrentListeners(Object model); }
As Figure 2 shows, the concrete aspect TableModelListenerUniqueness
extends EventListenerUniqueness
to apply the aspect to TableModel
and related classes. It provides an implementation for the modelAndListenerTypeMatch()
pointcut to restrict the model type to AbstractTableModel
and the listener type to TableModelListener
. Another concrete aspect, ListDataListenerUniqueness
, does the same for list-related classes. For ListDataListenerUniqueness
's source code, see Resources.
// TableModelListenerUniqueness.java import java.util.EventListener; import javax.swing.event.TableModelListener; import javax.swing.table.*; aspect TableModelListenerUniqueness extends EventListenerUniqueness { pointcut modelAndListenerTypeMatch() : target(AbstractTableModel) && args(TableModelListener); public EventListener[] getCurrentListeners(Object model) { return ((AbstractTableModel)model) .getListeners(TableModelListener.class); } }
Each concrete aspect, TableModelListenerUniqueness
, for example, handles the listener associated with a model type. You can create a simple aspect such as this for other Swing models, as well as your models. Compiling your source code along with these aspects will ensure no duplicate listener is added.
Implement a no loitering-views concern
Next, you implement a no loitering-views concern, as Figure 3 shows. Instead of adding listeners to a model directly, you can wrap it as a referent in a WeakReference
object and add it. Since the added object must be of the correct listener type, use the Decorator pattern in a class extending WeakReference
and implementing the required listener interface. The decorator implements the listener interface by delegating each method to the referent object.
The EventListenerWeakening
aspect implements the concern's core functionality, thus ensuring that no view loiters after its destruction. That abstract aspect also declares EventListenerManagement
as its parent. It advises addListenerCall()
to proceed with the listener obtained by calling the getWeakListener()
method:
// EventListenerWeakening.java import java.lang.ref.*; import java.util.*; import javax.swing.event.*; public abstract aspect EventListenerWeakening extends EventListenerManagement dominates EventListenerUniqueness { void around(Object model, EventListener listener) : addListenerCall(model, listener) { proceed(model, getWeakListener(listener)); } public abstract EventListener getWeakListener(EventListener listener); }
The WeakEventListener
decorates the EventListener
in addition to extending WeakReference
. The nested RemoveGarbageCollectedListeners
aspect removes WeakEventListener
from the model when it detects that the referent is garbage collected. Here you check the collected referent in an event notification method. If you don't wish to wait for the event to happen after the view disappears, you could use an implementation using ReferenceQueues
and a reaper thread:
// EventListenerWeakening.java (contd...) public abstract class WeakEventListener extends WeakReference implements EventListener { public WeakEventListener(EventListener delegatee) { super(delegatee); } public EventListener getDelegatee() { return (EventListener)get(); } public boolean equals(Object other) { if (getClass() != other.getClass()) { return false; } return getDelegatee() == ((WeakEventListener)other).getDelegatee(); } public String toString() { return "WeakReference(" + get() + ")"; } abstract static aspect RemoveGarbageCollectedListeners { pointcut eventNotification(WeakEventListener weakListener, EventObject event) : execution(void WeakEventListener+.*(EventObject+)) && this(weakListener) && args(event) && lexicalScopeMatch(); abstract pointcut lexicalScopeMatch(); public abstract void removeListener(EventObject event, EventListener listener); void around(WeakEventListener weakListener, EventObject event) : eventNotification(weakListener, event) { if (weakListener.getDelegatee() != null) { proceed(weakListener, event); } else { System.out.println("Removing listener: " + weakListener); removeListener(event, weakListener); } } } }
The abstract lexicalScopeMatch()
pointcut ensures only methods from specific WeakListener
types are captured. Without this pointcut, execution of all WeakListeners
notification would be caught -- for example, table as well as lists. Since removeListener()
would cast arguments to specific types, a ClassCastException
would throw.
The EventListenerWeakening
aspect dominates EventListenerUniqueness
, which causes EventListenerWeakening
's advice to addListenerCall()
to be applied before that of EventListenerUniqueness
. That way, you create the wrapped listener first, and the uniqueness check using WeakEventListener.equals()
works correctly. Since listeners added in a model are WeakReference
wrapped, comparing bare listeners would cause a comparison using equals()
to not match. You could avoid the problem by modifying equals()
, but that could cause the equals()
method's symmetric nature to break.
The TableModelListenerWeakening
aspect handles table-related listeners. It uses a specialized WeakEventListener
that implements TableModelListener
by delegating to the referent object. You'll find the code for the aspect that handles the list-related listener in ListModelListenerWeakening.java
:
// TableModelListenerWeakening.java import java.util.*; import javax.swing.event.*; import javax.swing.table.*; public aspect TableModelListenerWeakening extends EventListenerWeakening { pointcut modelAndListenerTypeMatch() : target(AbstractTableModel) && args(TableModelListener); public EventListener getWeakListener(EventListener listener) { System.out.println("Weakening " + listener); return new WeakTableModelListener((TableModelListener)listener); } } public class WeakTableModelListener extends WeakEventListener implements TableModelListener { public WeakTableModelListener(TableModelListener delegatee) { super(delegatee); } public void tableChanged(TableModelEvent e) { TableModelListener listener = (TableModelListener)getDelegatee(); listener.tableChanged(e); } static aspect TableRemoveGarbageCollectedListeners extends WeakEventListener.RemoveGarbageCollectedListeners { pointcut lexicalScopeMatch() : within(WeakTableModelListener); public void removeListener(EventObject event, EventListener listener) { ((TableModel)event.getSource()) .removeTableModelListener((TableModelListener)listener); } } }
With the aforementioned aspects, you've created a crosscutting concern to avoid multiple notifications to the same model listener, as well as avoided loitering view objects. The simple Test.java
tests the functionality. The aspects use the utility Utils.java
class.