为什么观察者模式应该被弃用?

49

我注意到我的依赖注入、观察者模式重度代码(使用Guava的EventBus)常常比我过去写的没有这些特性的代码更难调试。特别是在试图确定何时和为什么调用观察者代码时。

Martin Oderski和他的朋友们写了一篇标题特别吸引人的长文,"Deprecating the Observer Pattern",但我还没有抽出时间去阅读。

我想知道观察者模式有什么问题,还有哪些(提出或其他的)替代方案比它好,以至于这些聪明的人要写这篇论文。

首先,我在这里找到了一篇(有趣的)论文批评该论文。


3
这里也有讨论... http://lambda-the-ultimate.org/node/4028 - Ian Mercer
3个回答

38

以下内容引用自该论文:

为了阐明观察者模式的精确问题,我们以鼠标拖动为例,这是一个简单而普遍的例子。下面的示例跟踪Path对象中鼠标拖动期间的移动并在屏幕上显示出来。为了保持简单,我们使用Scala闭包作为观察者。

var path: Path = null
val moveObserver = { (event: MouseEvent) =>
   path.lineTo(event.position)
   draw(path)
}
control.addMouseDownObserver { event =>
   path = new Path(event.position)
   control.addMouseMoveObserver(moveObserver)
}
control.addMouseUpObserver { event =>
   control.removeMouseMoveObserver(moveObserver)
   path.close()
   draw(path)
}
上述示例以及我们将在[25]中定义的观察者模式一般违反了许多重要的软件工程原则:
- 副作用:观察者会引发副作用。由于观察者是无状态的,因此我们通常需要多个观察者来模拟状态机,如拖动示例中所示。我们需要保存状态并使其对所有相关观察者可访问,例如上面的变量path。 - 封装:由于状态变量path超出了观察者的范围,观察者模式破坏了封装。 - 组合性:多个观察者形成一个宽松的对象集合,处理单个关注点(或多个关注点,请参见下一个点)。由于在不同的时间点安装了多个观察者,因此我们无法轻松地彻底处理它们。 - 关注点分离:上述观察者不仅跟踪鼠标路径,还调用绘图命令,或者更通用地说,在同一代码位置中包含两个不同的关注点。通常最好将构建路径和显示路径的关注点分开,例如,在模型视图控制器(MVC)[30]模式中。 - 可扩展性:我们可以通过为路径创建一个类本身发布事件来在示例中实现关注点分离。不幸的是,观察者模式不能保证数据一致性。假设我们将创建另一个依赖于原始路径更改的事件发布对象,例如,表示路径边界的矩形。还有考虑同时监听路径和其边界变化的观察者以绘制框架路径。此观察者需要手动确定边界是否已更新,如果未更新,则推迟绘制操作。否则,用户可能会在屏幕上看到大小不正确(故障)的框架。 - 统一性:使用不同的方法安装不同的观察者会降低代码的统一性。 - 抽象:示例中存在低级别的抽象。它依赖于控制类的重量级接口,该接口提供了更多仅用于安装鼠标事件观察者的具体方法。因此,我们无法对精确的事件源进行抽象。例如,我们可以让用户通过按下Esc键或使用其他指针设备,如触摸屏幕或图形板,来中止拖动操作。 - 资源管理:观察者的生命周期需要由客户端进行管理。由于性能原因,我们只想在拖动操作期间观察鼠标移动事件。因此,我们需要明确安装和卸载鼠标移动观察者,并且需要记住安装点(上面的控件)。 - 语义距离:最后,示例很难理解,因为控制流被反转,导致有太多样板代码,增加了程序员意图和实际代码之间的语义距离。
[25]E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design patterns: elements of reusable object-oriented software. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1995. ISBN 0-201-63361-2.

2
我想知道这里列出的弱点是否固有于观察者模式。我刚刚阅读了MobX库的描述,我相信它使用观察者模式,但似乎解决了上述许多问题。例如,MobX似乎确实包括对值的一致性和“新鲜度”以及它们更新的顺序的保证。请参见此处。好奇其他人的想法... - fraxture
5
我不会评论所有内容,但许多问题可以通过使用正确的模式来缓解,例如在定义路径时将观察者更改为变更管理器。变更管理器是中介者。因此,我认为你的例子有点牵强附会。 - Robinson
答案中的链接已经无法访问了...但问题中的链接可以(而且应该是相同的) - Jefferson Quesado
1
有没有人有一个例子,展示如何更好地使用现在推荐的 PropertyChangeEvent - Benjamin Peter
@jeff 谢谢。你更喜欢哪种观察者模式?通过闭包委托吗? - ScottyBlades

21

我认为观察者模式具有与解耦有关的标准缺点。主题与观察者分离,但您不能仅查看其源代码并找出谁观察它。硬编码依赖项通常更易于阅读和思考,但它们更难修改和重用。这是一种权衡。

至于这篇论文,它并不涉及观察者模式本身,而是涉及它特定的用法。特别地:单个被观察对象对应多个无状态观察者对象。这显然具有独立观察者需要相互同步的明显缺点("由于观察者是无状态的,我们通常需要多个观察者来模拟像拖动示例中的状态机。我们必须保存状态,以便所有相关的观察者都可以访问,例如上面的变量路径。")

上述缺点是特定用法的问题,而不是观察者模式本身的问题。您也可以创建一个单一(有状态的!)观察者对象,实现所有OnThis, OnThat,OnWhatever方法,并摆脱跨多个无状态对象模拟状态机的问题。


但是在示例中,观察者确实具有副作用,就像您引用中提到的那样。(请参见对“path”定义的所有操作)。 - Jeff Axelrod

9
我会简短概括因为我对这个主题很新,并且尚未阅读该特定文章。
观察者模式在直觉上是错误的:被观察的对象知道谁在观察(Subject<>--Observer)。这与现实生活中(在基于事件的场景中)相违背。如果我尖叫,我不知道谁在倾听;如果闪电击中地面……闪电在撞击之前并不知道有地面存在!只有观察者知道他们可以观测什么。
当这种事情发生时,软件通常会一团糟——因为它是根据我们的思维方式构建的。这就像一个对象知道其他对象可以调用他的方法。
在我看来,“环境”这样的层应该负责处理事件并通知相关方。(或者混合事件和事件生成器)
事件源(Subject)生成事件到环境。环境将事件传递给观察者。观察者可以注册接受影响的事件类型,或者已经在环境中定义。这两种可能性都是有意义的(但我想简要说明)。
在我的理解中,观察者模式将环境和主题结合在一起。
PS.讨厌将抽象思想分段! :P

1
这里http://www.marco.panizza.name/dispenseTM/slides/exerc/eventNotifier/eventNotifier.html 我找到了基本上表达相同意思的图示: -发布者(主题) -订阅者(对象) -事件服务(环境) (1998年) 毕竟,“常识”会引导我们在编程时以某种方式实现这种中间层。问题在于模式何时激励或误导。 - Luis Fernando Robledano
主题不知道它们的观察者(也许只通过基类)。设计模式书籍也提到了一个更改管理器。这与您的环境有些相似。在我看来,观察者模式非常适合通知(独立的)GUI元素,但该模式也有一些严重的缺点:当主题或业务层不处于一致状态时会出现虚假通知;在通知更新期间,观察者对主题进行更改。 - gast128
1
这里有几个要点,主题不知道观察者的任何信息,我们可以通过抽象接口来实现这一点。 - Farruh Habibullaev

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