简体   繁体   中英

Java - Generic ChangeListener

Scenario:

I have a container object which holds a mixed bag of objects all inheriting from the MyContainedObject class. Consumers of the container class do not have direct access to contained objects, but my be interested in knowing when they change.

Design Decision:

What's the best way to listen for ChangeEvents on a specific class type? My initial thought is to do something with Generics. For example,

private TreeMap<Class, ChangeListener> listeners;

public <T extends MyContainedObject> addChangeListenerForObjectsOfType(Class<T> className, ChangeListener listener)
{
   listeners.put(className, listener);
}

When a change is detected the container class would iterate through the list and notify only listeners registered for that class type.

Other suggestions?

Thanks.

I'm assuming the key type on your TreeMap was meant to be a Class, not a MyContainedObject.

If you actually need to listen for ChangeEvents on specific class types, and you want to be able to add elements to your collection even after setting listeners, this seems pretty reasonable. You'll probably want to support multiple listeners for the same type, so you should either use a Multimap class ( Google Collections has some) or use a collection (probably an IdentityHashSet) for the values in your Map.

You may also want to add a type parameter to ChangeListener so that the listener can get the object the event fired on already casted to the appropriate type.

interface ChangeListener<T> {
    void changed(T obj, /* whatever */);
}

You'll have to do an unchecked cast inside of your container for this to work, but it should be safe as long as your listener adding method does the right thing. eg:

public <T extends MyContainedObject> addChangeListener(Class<T> klass,
                                                       ChangeListener<? super T> listener) {
    ...
}    

private <T extends MyContainedObject> Set<ChangeListener<? super T>> getChangeListeners(T obj) {
    Set<ChangeListener<? super T>> result = new IdentityHashSet<ChangeListener<? super T>>();
    for (Map.Entry<Class<? extends MyContainedObject>, Set<ChangeListener<?>>> entry : listeners.entrySet()) {
        if (entry.getKey().isInstance(obj)) {
            // safe because signature of addChangeListener guarantees type match
            @SuppressWarnings("unchecked")
            Set<ChangeListener<? super T>> listeners =
                (Set<ChangeListener<? super T>>) entry.getValue();
            result.addAll(listeners);
        }
    }
    return result;
}

One minor nit: I'd avoid using "className" as the name of a variable that holds a Class object. A class name is a String, typically the result of Class.getName(), etc.. It's a bit annoying, but the convention I've usually seen to avoid get around the fact that "class" is a reserved word is to misspell it as either "klass" or "cls".

Also, if you don't need the ability to update your collection after adding listeners then I'd go with what akf suggested, as it's simpler.

You could also simply have your container proxy the addChangeListener call to the contained objects in question. This will allow them to maintain their listener list and fire the calls as needed, without the added complexity of another level in the listener heirarchy.

One problem with this type-specific notification approach is that a client could potentially register a listener interested in when a particular interface had changed; eg addChangeListenerForObjectsOfType(Iterable.class) . This means your notification algorithm couldn't do a simple look-up / in your listeners map, unless you explicitly prevented registering listeners against interfaces, but instead would need to be more complex (and less efficient).

I would probably take a different approach rather than making your implementation fully generic. For example, if you could identify a handful of top-level sub-classes you're interested in you could provide a more explicit listener interface:

public interface Listener {
    void classAChanged(ChangeEvent e);
    void classBChanged(ChangeEvent e);
    void classCChanged(ChangeEvent e);
}

I personally prefer this as it's much more explicit for programmers implementing the interface, making the code more readable. Obviously it might be inappropriate if you're potentially storing hundreds of different sub-classes in your map.

You could take this one step further and provide a generic ChangeEvent<T extends MyObject> implementation to avoid downcasting within your listener callback methods.

There are a few limitations on your proposed design that may cause problems depending on what types of listeners you want to write

  1. A listener can only register for one raw type at a time. If a listener is interested for many types, or has other criteria it wants to use to decide what objects it is interested in, it would have to register multiple times. Also, the listener might not know what types are out there!
  2. It's unclear how to handle the case where you register for a subclass of MyContainedObject, and that subclass has subclasses.
  3. Changes can happen to MyContainedObject instances that the container is not aware of

I suggest instead having a listener for the container, and have each MyContainedObject instance also support listeners:

public interface ContainerListener {
  void itemAdded(MyContainedObject item);
  void itemRemoved(MyContainedObject item);
}

public interface MyContainedObjectListener {
  void itemChanged(MyContainedObject item);
}

As Adamski suggested, if there are a limited number of subclasses of MyContainedObject, then you can make the methods more specific:

public interface ContainerListener {
  void circleAdded(Circle item);
  void squareAdded(Square item);
  void shapeRemoved(Shape item);
}

Alternatively, you could make the specific methods in MyContainedObjectListener.

If you decide that registering for a type meets your needs, then consider making the listener generic:

public <T extends MyContainedObject> addChangeListenerFor(Class<T> className, ChangeListener<? super T> listener)

Either way, read the GoF section on Observer, since there are multiple implementation possibilities, each with advantages and disadvantages.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM