【问题标题】:How to avoid circular references with the observer pattern?如何避免使用观察者模式进行循环引用?
【发布时间】:2017-04-04 06:58:25
【问题描述】:

我遇到了这样的情况:

public class A
{
    private ArrayList<Listener> listeners = new ArrayList<Listener>();

    public A()
    {
        // stub
    }

    public void addListener(Listener listener)
    {
        this.listeners.add(listener);
    }

    public void removeListener(Listener listener)
    {
        this.listeners.remove(listener);
    }

    public interface Listener
    {
        // stub
    }
}

public class B implements A.Listener
{
    private A instanceOfA;

    public B()
    {
        this.instanceOfA = new A();
        this.instanceOfA.addListener(this);
    }
}

我相信 B 永远不会被破坏,因为 A 保留了对它的引用,而 A 永远不会被破坏,因为 B 保留了对它的引用。

这似乎是一种常见的设计模式,但没有人解释如何避免这种循环引用?

【问题讨论】:

  • 如果你想防止循环引用,只需在类(这里AB)之间建立单向而不是双向的关系。或者添加一个调解员(但最后可能会很丑)......编辑:这个问题属于Software Engineering SE
  • @KarelG 我不能让它单向,因为 B 需要能够调用 A 上的方法(此外,如果 B 没有保留对 A 的引用,那么 A 将被销毁)。而且我无法删除从 A 到 B 的通信,因为 B 需要知道 A 何时发生某些事情。

标签: java oop observer-pattern circular-reference


【解决方案1】:

在实践中,这已解决,因此观察者B 不会直接引用A。它应该是一个专注于处理其事件的唯一任务的小类,并且不应该与A 有任何直接耦合。事实上,这正是观察者模式的重点:一种向A 添加代码的灵活方式,而无需硬依赖它。

如果事件处理程序必须观察作为事件源的对象 (A),那么事件回调方法应该声明一个 A 类型的参数,以便它可以以无状态方式传递给处理程序,以仅在事件处理期间使用,之后不保留。

【讨论】:

  • A 由 B 创建以处理网络内容。这两个班级联系非常紧密。例如,在初始化期间 B 调用 instanceOfA.connect(),然后当用户想要在关闭 UI 之前断开 B 调用 instanceOfA.disconnect() 时,或者如果用户做了一些导致发送网络活动的事情 B 调用 instanceOfA.sendSomething(),等等.因此,B 必须引用 A。A 还必须引用 B,以便 A 可以告诉 B,例如,何时收到响应。我看不出有任何方法可以将 B 的其余部分与 B 处理来自 A 的事件的部分分开。
  • 在这种情况下,对观察者的普遍支持毫无意义。你只有两个紧密耦合的类。如果 B 甚至可以控制 A 的连接状态,那听起来就像是一对一的关系。
  • 除了我说的,B是UI类,A是核心类。 B 如何显示内容与 A 无关,B 可以很容易地被另一个 UI 类或不显示 UI 但自动执行网络操作的类替换。将 A 的实例视为与特定类型服务器的特定类型的连接,将 B 的实例(或实现 A.Listener 的任何东西)视为使用该连接的东西。
  • 所以当你处理 B 时,显然你需要手动代码从 A 注销它,否则从 A 到 B 的强引用不会消失。此外,由于 A 获得了外部资源,因此也不应将其处置留给 GC。它应该明确地对例如最后一个监听器通过释放资源被移除。然后它可以被单独留下,最终被 GC 处理。
【解决方案2】:

因为您没有按照模式通常定义的方式进行操作。你可以看看这个Tutorial

将监听器定义为这样的接口,例如:

/**
 * The listener interface for receiving message events. The class that is interested in processing a
 * message event implements this interface.
 *
 * @see MessageEvent
 */
public interface MessageListener
{

  /**
   * Message received notification.
   *
   * @param data the data
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public void MessageReceived( byte[] data ) throws IOException;

  /**
   * Connection closed notification.
   */
  public void closed();
}

现在您可以根据需要创建尽可能多的实现,例如:

public class MessageListenerImpl implements MessageListener
{

  /** The driver. */
  private DeviceClassInternal driver;

  /** The log. */
  private Log log;

  /**
   * Instantiates a new TCP message listener.
   *
   * @param driver the driver
   * @param log the log
   */
  public MessageListenerImpl( DeviceClassInternal driver, Log log )
  {
    this.driver = driver;
    this.log = log;
  }

  /** {@inheritDoc} */
  @Override
  public void MessageReceived( byte[] data ) throws IOException
  {
    log.info( "data received: {1}", new String( data ) );
    driver.dataReceived( data );
  }

  @Override
  public void closed()
  {
    log.info( "{0}: Connection closed", physicalConnection.getName() );
    driver.closed();
  }

}

最后你可以创建它并将这个监听器注册到你想要的对象:

  MessageListenerImpl listener = new MessageListenerImpl( connection, log ); // create the listener and you're good to go.

  physicalConnection.registerListener( listener ); // Just some object with a register function.

【讨论】:

  • 这与我所做的有什么不同?我正在使用一个界面。最终,主题仍然保持对观察者的引用(通过接口),而观察者保持对主题的引用。
  • 不,您不应该保留任何参考。你需要一个独立的接口,它有一个实现来处理你扔给它的东西。看看我的最后一个代码示例。我正在创建一个新的侦听器对象并将它也传递给我的对象 A。我的侦听器没有实现 A,因此它没有引用。
【解决方案3】:

这一切都取决于特定的用例。 正如其他人指出的那样,在许多情况下,您不需要在侦听器中保存 A 引用,而是依赖于在事件本身中传递它。但这并不总是足够的 - 例如,您可能希望根据其他事件注销自己,并且您需要原始引用才能调用 removeListener。

但这是一个更通用的问题 - 如果您仍然在其他地方引用 A,那么无论您的侦听器设计如何,它都不会被 GCed。另一方面,如果你忘记了程序中对 A 的引用并且不持有对 B 的引用,那么它们无论如何都会被垃圾回收 - 循环引用不会阻止 Java 中的垃圾回收 (How does Java Garbage Collection work with Circular References?)

过去我也遇到过类似的情况,但我关心的是 B 被 GC 而不是 A。当从外部对 B 的所有引用都丢失时,没有人可以取消注册它,但它仍然会收到 A 的通知事件。如果您没有完美地清理自己,那么在 Java 的 UI 框架中以这种危险的侦听器结束是很常见的(人们通常不会这样做,因为他们认为以图形方式摆脱 UI 组件并忘记所有对这应该足够了——例如,一些全局键盘处理程序的侦听器或某些东西仍然将所有内容保存在强可达集中)。

虽然对于 UI 框架,您没有机会更改核心代码,但您可以使用自己的代码尝试使侦听器列表具有 WeakReference 而不是强引用,并在需要时对每个通知进行一些清理。它的副作用是您必须保持听众对其他代码位置的引用,但无论如何这是一个很好的做法(因为在其他情况下您应该手动取消注册它们) - 如果您不这样做,则参考较弱你会突然停止在随机时间被回调(在几个 gc 循环运行后)。

无论如何,您首先需要了解(并告诉我们),您如何想象 A 和 B 的生命周期以及为什么在 A 可能已经消失后 B 会被强引用。您也可以随时将 B->A 引用设为弱引用,但请首先确保充分了解您对在什么时候忘记什么的期望。

【讨论】:

  • 这是一个 GUI 应用程序,是的。 B 是一个 UI 类,将由 UI 框架实例化。 B 创建 A 并将自己注册为侦听器。只要 B 存在,A 就应该存在,但一旦不再需要 B,就不需要(还)。但是因为 B 持有对 A 的引用,而 A 持有对 B 的引用(作为侦听器),即使 UI 框架删除了对 B 的所有引用,也不会被垃圾收集。我现在也遇到了更多问题,因为 B 正在创建更多对象并将它们传递给 UI 框架,而这些对象也将自己注册为 A 的侦听器。
  • 所以我的情况将是完美的,因为监听器实现将在 A 和 B 之间,就像这样 B
  • 这不应该是一种情况,除非您从其他地方强烈引用 A。如果您完全摆脱 B 并且仅从那里引用 A,它们将获得 GCed。您提到的其他对象很有可能保持强引用,持有对 A 的引用,而 A 又持有对 B 的引用 - 但这意味着 B->A 引用在这里不是罪魁祸首(情况是一样的,无论如何如果它存在与否)。如果是这种情况,恐怕唯一的解决方案是对 B 进行适当的手动清理,取消注册对 A 的其他引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-24
  • 1970-01-01
相关资源
最近更新 更多