Java - 通用ChangeListener

4

场景:

我有一个容器对象,它包含一些继承自MyContainedObject类的混合对象。容器类的消费者没有直接访问包含的对象,但可能会对它们的更改感兴趣。

设计决策:

如何最好地监听特定类类型的ChangeEvents?我最初的想法是使用泛型。例如,

private TreeMap<Class, ChangeListener> listeners;

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

当检测到变化时,容器类会迭代列表并仅通知已注册该类类型的侦听器。
其他建议?
谢谢。
4个回答

7
我假设您的TreeMap上的键类型应该是一个类,而不是MyContainedObject。
如果您需要监听特定类类型的ChangeEvents,并且您想在设置侦听器后添加元素到集合中,那么这似乎是相当合理的。您可能希望为同一类型支持多个侦听器,因此应该使用Multimap类(Google Collections有一些)或使用集合(可能是IdentityHashSet)作为Map中值的容器。
您还可以向ChangeListener添加类型参数,以便侦听器可以获取已转换为适当类型的事件对象。
interface ChangeListener<T> {
    void changed(T obj, /* whatever */);
}

你需要在容器内进行一个未经检查的类型转换才能使这个工作,但只要你的监听添加方法做得对,那么它应该是安全的。例如:
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;
}

一个小问题:我建议避免使用“className”作为保存Class对象的变量名称。类名是一个字符串,通常是Class.getName()等的结果。这有点让人烦恼,但我通常看到的惯例是避免绕过“class”是一个保留字的事实,将其拼错为“klass”或“cls”。
此外,如果您在添加侦听器后不需要更新集合,则建议采用akf建议的方法,因为它更简单。

谢谢。这正是我所想的。另外,你是正确的,我确实是指类而不是我的包含对象。 - javacavaj

1
你也可以让你的容器代理 addChangeListener 方法来处理相关对象,这将使它们维护其监听器列表并在需要时触发调用,而无需增加另一个层次的监听器结构的复杂性。

1
这是一个不错的建议,但仅适用于在添加监听器之后从未修改包含对象集的情况,或者如果您对具有侦听器的对象集和包含对象集随时间分歧感到满意。 - Laurence Gonsalves

0

这种特定类型通知方法的一个问题是,客户端可能会注册一个感兴趣的监听器,以便在特定接口更改时进行通知;例如:addChangeListenerForObjectsOfType(Iterable.class)。 这意味着您的通知算法无法进行简单的查找 / 在您的侦听器映射中,除非您明确阻止注册针对接口的侦听器,否则需要更复杂(并且效率更低)。

我可能会采取不同的方法,而不是使您的实现完全通用。 例如,如果您可以识别出几个顶级子类,则可以提供更明确的侦听器接口:

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

我个人更喜欢这种方式,因为它对于实现接口的程序员来说更加明确,使代码更易读。当然,如果您可能在映射中存储数百个不同的子类,则可能不适用。

您可以进一步采取这一步骤,并提供一个通用的 ChangeEvent<T extends MyObject> 实现,以避免在监听器回调方法中进行向下转换。


0

你提出的设计存在一些限制,这可能会导致问题,具体取决于你想编写哪些类型的监听器。

  1. 一个监听器一次只能注册一个原始类型。如果一个监听器对多种类型感兴趣,或者有其他标准来决定它感兴趣的对象,那么它必须多次注册。此外,监听器可能不知道有哪些类型存在!
  2. 如果你注册了 MyContainedObject 的子类,并且该子类有子类,那么如何处理这种情况是不清楚的。
  3. MyContainedObject 实例可能发生更改,而容器并不知道。

我建议改为为容器设置监听器,并使每个 MyContainedObject 实例也支持监听器:

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

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

如Adamski所建议的那样,如果MyContainedObject的子类数量有限,则可以使方法更加具体:
public interface ContainerListener {
  void circleAdded(Circle item);
  void squareAdded(Square item);
  void shapeRemoved(Shape item);
}

或者,您可以在MyContainedObjectListener中创建特定的方法。

如果您决定注册类型符合您的需求,则考虑使监听器成为通用的:

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

无论如何,阅读GoF中的观察者部分,因为有多种实现可能性,每种都有优缺点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接