Implementing Observers with Stateless Properties
I've got a simplified Property interface and a mixin that makes the property observable:public interface Property<B,V> {
V get(B bean);
void set(B bean, V value);
}
public interface ObservableProperty<B,V> extends Property <B,V> {
void addPropertyChangeListener(B bean, PropertyChangeListener p);
void removePropertyChangeListener(B bean, PropertyChangeListener p);
}
For most use cases, the interface is implemented by a library. For example, this instance uses the getters and setters: ObservableProperty p = new BeanProperty(Customer.class, "address");
Today I'm interested in the implementation of this interface. Here's a very straightforward implementation that doesn't even need reflection - getting the Document
property from a JTextField
:public class JTextFieldDocumentProperty
implements ObservableProperty<JTextField, Document> {
public Document get(JTextField bean) {
return bean.getDocument();
}
public void set(JTextField bean, Document value) {
bean.setDocument(value);
}
public void addPropertyChangeListener(JTextField bean, PropertyChangeListener p) {
bean.addPropertyChangeListener("document", p);
}
public void removePropertyChangeListener(JTextField bean, PropertyChangeListener p) {
bean.removePropertyChangeListener("document", p);
}
}
This code is deceivingly simple because the listener types in the Property interface (PropertyChangeListener
) exactly match the listener types in the bean. Things get a much more interesting if the listener types are different.Using an adapter
The natural solution is to code the
addPropertyChangeListener
to use a simple adapter to convert events of one type to events of another type. This adapter converts between DocumentListener
and PropertyChangeListener
:private class DocumentToPropertyChangeListener
implements DocumentListener {
private String currentText;
private final Document document;
private final PropertyChangeListener delegate;
public DocumentToPropertyChangeListener(Document bean, PropertyChangeListener p) {
this.document = bean;
this.delegate = p;
this.currentText = getDocumentText();
}
private String getDocumentText() {
try {
return document.getText(0, document.getLength());
} catch (BadLocationException e) {
throw new RuntimeException(e);
}
}
public void insertUpdate(DocumentEvent e) {
fireChanged();
}
public void removeUpdate(DocumentEvent e) {
fireChanged();
}
public void changedUpdate(DocumentEvent e) {
fireChanged();
}
public void fireChanged() {
String oldText = currentText;
currentText = getDocumentText();
delegate.propertyChange(
new PropertyChangeEvent(document, "text", oldText, currentText));
}
}
As you can see, the Document
class is a bit cumbersome to use. But this interface correctly adapts the listener types. Unfortunately, we still have the trickiest problem ahead of us...The problem with removePropertyChangeListener()
In my second example, I'll implement Property for the text of a
Document
. The implementation of addPropertyChangeListener
is easy; we just create the adapter inline:public class DocumentTextProperty
implements StatelessProperty<Document, String> {
...
public void addPropertyChangeListener(Document bean, PropertyChangeListener p) {
bean.addDocumentListener(new DocumentToPropertyChangeListener(bean, p));
}
public void removePropertyChangeListener(Document bean, PropertyChangeListener p) {
...
}
}
But how does remove work? There's a big problem - we didn't save a reference to our adapter! We need to recover that reference somehow so we can remove the same adapter that was added.The
Document
interface doesn't expose its listeners; ie. there's no getDocumentListeners()
method. So we can't just iterate over the document's listeners to find the matching adapter.If we store a reference to the adapter in the
DocumentTextProperty
object, then we'll almost certainly have a memory leak. Even if we stored just a WeakReference<DocumentTextProperty>
, it would make our Property stateful. In the ideal implementation, any two instances of the same property should be interchangeable. For example, we shouldn't force the user to store a reference to the Property instance. This code should work fine but it won't work if the property is stateful: public void installListeners() {
new DocumentTextProperty().addPropertyChangeListener(
myDocument, myPropertyChangeListener);
}
...
public void shutdown() {
new DocumentTextProperty().removePropertyChangeListener(
myDocument, myPropertyChangeListener);
}
The next option is to store this state in a static global registry. This is the approach taken by the bean-properties Java.net project. I don't like this approach because it feels very heavyweight:
WeakReferences
.Fortunately, there is a clever solution to this problem - don't store a reference to the adapter. Instead, when we call
removePropertyChangeListener
, we pass in a different instance that equals the first:public class DocumentTextProperty
implements StatelessProperty<Document, String> {
...
public void addPropertyChangeListener(Document bean, PropertyChangeListener p) {
bean.addDocumentListener(new DocumentToPropertyChangeListener(bean, p));
}
public void removePropertyChangeListener(Document bean, PropertyChangeListener p) {
bean.removeDocumentListener(new DocumentToPropertyChangeListener(bean, p));
}
}
This requires us to implement equals
and hashCode
in our adapter:private class DocumentToPropertyChangeListener
implements DocumentListener {
public boolean equals(Object obj) {
return obj != null
&& obj.getClass() == this.getClass()
&& ((DocumentToPropertyChangeListener) obj).delegate.equals(delegate)
&& ((DocumentToPropertyChangeListener) obj).document.equals(document);
}
public int hashCode() {
return delegate.hashCode() * 37 + document.hashCode();
}
}
I love equals()
We've taken advantage of one of Java's greatest features: a strong concept of equality. It makes our solution stateless, immutable, serializable, efficient and easy to use.