Seeking feedback: ListEvent, ListEventBuilder APIs
As of today, beansbinding does a very lightweight approach to observable lists. There's two interfaces,
ObservableList.java and
ObservableListListener.java.
I'm refining a proposal for a more heavyweight approach. In addition to simplified List and Listener interfaces, I add a
ListEvent interface and a
ListEventBuilder class.
Motivation for these interfaces:
Support for fine-grained events. Now List.removeAll
can fire a single event.
Support for move events. This allows listeners to track state as elements are reordered or sorted by the user.
Support for event 'envelopes'. This allows the user of an ObservableList to prevent observers from seeing intermediate states.
Fine-grained events are a central part of the well-loved Glazed Lists project, and I think they could be very helpful in the JDK.
If you could read through this code and email me your comments, I'd appreciate it:
ListEvent
ListEventBuilder
# posted by Jesse Wilson
on Wednesday, August 22, 2007
0 comments
post a comment
Use Shift+Enter for multiline text in Gmail Chat
My IM client at work is the Google Talk widget that's built-in to Gmail. It works okay, but it's annoying for typing code snippets into chats. Code snippets require multiple lines of text, but pressing
Enter
commits the message. How to send a multi-line message?
Well today I accidentally discovered that pressing Shift+Enter
inserts a newline character in to the chat. This might work in other IM clients as well...
# posted by Jesse Wilson
on Tuesday, August 21, 2007
1 comments
post a comment
Properties should be stateless to avoid memory leaks
In the latest rev of beansbinding, the
AbstractProperty class is stateful. This means that when you add a listener to an object, the property independently remembers both.
For example, suppose your code uses databinding in a dialog:
private static final Property textFieldTextProperty
= new BeanProperty<JTextField,String>("text");
private static final Property customerNameProperty
= new BeanProperty<Customer,String>("name");
public JDialog createDialog() {
JTextField textField = new JTextField();
Customer customer = new Customer();
Binding customerNameBinding = new Binding<JTextField,String,Customer,String>(
textField, textFieldTextProperty, customer, customerNameProperty);
customerNameBinding.bind();
...
}
After the dialog has been dismissed, the form and its data should fall out-of-scope and get garbage collected. But they don't! Since the
textFieldTextProperty
field is static and holds a reference to the dialog, the dialog remains in memory until the application exits. This invisible memory leak exists because the
addPropertyStateListener
method has a hidden side effect.
In this case, the solution is to save a reference to the
customerNameBinding
and to
unbind()
it upon dialog closing. Alternately, recreating new
Property
objects every time the dialog is created will avoid the problem.
Perhaps instead, beansbinding will make Properties
stateless.
# posted by Jesse Wilson
on Thursday, August 16, 2007
0 comments
post a comment
Chaining Stateless Properties
In a
recent post, I described how I implemented the observers for stateless properties. In this follow up I describe how to do observers for chained properties.
What's a chained property?Suppose I'd like to bind a combo box to a customer's country. If the Customer bean has a
getCountry()
property, that's easy. But if the Country is only exposed via a series of getters like
getBillingAddress().getCountry()
, I bind to Customer#address and address#country in series. A chained property is created by composing multiple properties into one.
Implementing get() and set() is easyI resolve each property in the chain using the value of one property as the bean for the next:
public class ChainedProperty<B,V> implements Property<B,V> {
private final List<Property> propertyChain;
public ChainedProperty(...) { ... }
public V get(B bean) {
Object current = bean;
for (Property property : propertyChain) {
current = property.get(current);
}
return (V)current;
}
public void set(B bean, V value) {
Object current = bean;
for (Iterator<Property> m = propertyChain.iterator(); m.hasNext(); ) {
Property property = m.next();
if (m.hasNext()) {
current = property.get(current);
} else {
property.set(current, value);
}
}
}
...
}
Implementation Plan for Chaining Observers- Observe the bean for each property in the chain.
- When a property changes, action is required:
- If it's the last property whose value has changed, notify the chain's listener
- For other properties, change the observed bean for the next property in the chain
Each time a new listener is registered on the chain, a new chain of
SubListeners
is created. Each
SubListener
observes a single property of a single bean. And so the chain of
SubListeners
observes the chain of properties. We use a custom
SubListener
at the end of the chain who notifies the
PropertyChangeListener
:
public abstract class SubListener<B, V> implements PropertyChangeListener {
private final Property<B, V> property;
private B bean;
private V currentValue;
public SubListener(...) { ... }
void setBean(B bean) {
property.removePropertyChangeListener(bean, this);
this.bean = bean;
property.addPropertyChangeListener(bean, this);
valueChanged();
}
public void propertyChange(PropertyChangeEvent evt) {
valueChanged();
}
private void valueChanged() {
V oldValue = currentValue;
currentValue = property.get(bean);
fireValueChanged(oldValue, currentValue);
}
abstract void fireValueChanged(V oldValue, V currentValue);
}
public class ChainingSubListener<B, V> extends SubListener<B, V> {
private final SubListener<V, ?> next;
public ChainingSubListener(...) { ... }
@Override
public void fireValueChanged(V oldValue, V currentValue) {
next.setBean(currentValue);
}
}
public class LastSubListener<B, V> extends SubListener<B, V> {
private final Object rootBean;
private final String chainName;
private final PropertyChangeListener delegate;
public LastSubListener(...) { ... }
@Override
public void fireValueChanged(V oldValue, V currentValue) {
delegate.propertyChange(
new PropertyChangeEvent(rootBean, chainName, oldValue, currentValue));
}
}
Removing ObserversJust as with my
previous solution, I can support removing the chain of listeners with a carefully crafted
equals()
method. Two
SubListeners
are equal if they model the same property, have the same chain of
SubListener
s downstream, and notify the same
PropertyChangeListener
at the chain's end.
ChainedProperty.java source listing
# posted by Jesse Wilson
on Monday, August 13, 2007
0 comments
post a comment
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 adapterThe 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:
It needs WeakReferences
.
The shared registry requires synchronization.
There's memory overhead to store each registered bean/listener pair.
The registry is complex because it requires each listener pair to be uniquely identified.
It prevents bean/listener pairs from being serialized.
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.
# posted by Jesse Wilson
on Friday, August 10, 2007
2 comments
post a comment
Stop Writing Redundant Javadoc
I keep seeing this exact same Javadoc being written over and over again:
/**
* A pizza delivery.
*
* @author jessewilson
*/
public class PizzaDelivery {
...
/**
* Sets the address of this PizzaDelivery.
*
* @param address the address to set.
*/
public void setAddress(Address address) {
...
}
}
Naturally, this doc took the code author time to write. He typed 'address' an extra 3 times. But I'm not concerned with the author's time; he's got lots of free time on his hands. My problem is that he's wasting his team's time and hurting the code.
Redundant Javadoc
decreases the signal-to-noise ratio of the code. It trains your team that documentation is just a hoop that needs to be jumped through. It shows that Javadoc is for writing, not for reading. And that you think it's okay to put zero thought into documentation.
If you have nothing nice* to say, say nothing. If your code style guide makes Javadoc mandatory, change the style guide. A well-chosen method or class name is often enough.
# posted by Jesse Wilson
on Sunday, August 05, 2007
0 comments
post a comment
Returning boolean isn't always that helpful
In complex systems, transparency can sometimes help. In my fake pizzastore application, I've got an API that's used to control whether the
Place Order!
button is enabled or disabled:
public boolean isOrderValid(PizzaOrder pizzaOrder) {
if (!geography.isAcceptableAddress(pizzaOrder.getAddress())) {
return false;
}
if (!deliveryRules.isLargeEnoughOrder(pizzaOrder)) {
return false;
}
List<Store> storesInRange = storesRegistry.getStoresWithinRange(
pizzaOrder.getAddress(), MAXIMUM_DELIVERY_RADIUS);
if (storesInRange.isEmpty()) {
return false;
}
return true;
}
It works well. But sometimes I'll get a bug report complaining that the button is disabled when it shouldn't be. The problem
isn't that the button is disabled, it's that the user doesn't know
why the button is disabled. The fix is easy:
public List<String> getReasonsWhyOrderIsInvalid(PizzaOrder pizzaOrder) {
List<String> reasons = new ArrayList<String>();
if (!geography.isAcceptableAddress(pizzaOrder.getAddress())) {
reasons.add("Unacceptable address");
}
if (!deliveryRules.isLargeEnoughOrder(pizzaOrder)) {
reasons.add("Order is not large enough");
}
List<Store> storesInRange = storesRegistry.getStoresWithinRange(
pizzaOrder.getAddress(), MAXIMUM_DELIVERY_RADIUS);
if (storesInRange.isEmpty()) {
reasons.add("No stores in range of " + pizzaOrder.getAddress());
}
return reasons;
}
public boolean isOrderValid(PizzaOrder pizzaOrder) {
return getReasonsWhyOrderIsInvalid(pizzaOrder).isEmpty();
}
I can use the list of warnings as a tooltip on the disabled button, or use more sophisticated reporting as necessary. This works really nicely, and it keeps the end users informed.
Applying this technique elsewhereThere's lots of complex rulesets throughout the code. On one screen we show a tab with navigation data for deliveries in progress. That tab is displayed only if the navigation feature is configured, the user has sufficient credentials, and if GPS reports are being received.
By displaying the reasons why the navigation tab is not shown on an admin page, I can diagnose problems without a debugger. Reasons are more helpful than booleans because they guide me in my investigation. This approach works nicely in webapps, where it's easy to include extra admin pages.
In my app, reasons pages provide every detail on why the application is behaving the way it is - transparency is great!
# posted by Jesse Wilson
on Wednesday, August 01, 2007
0 comments
post a comment