我注意到我的依赖注入、观察者模式重度代码(使用Guava的EventBus)常常比我过去写的没有这些特性的代码更难调试。特别是在试图确定何时和为什么调用观察者代码时。
Martin Oderski和他的朋友们写了一篇标题特别吸引人的长文,"Deprecating the Observer Pattern",但我还没有抽出时间去阅读。
我想知道观察者模式有什么问题,还有哪些(提出或其他的)替代方案比它好,以至于这些聪明的人要写这篇论文。
首先,我在这里找到了一篇(有趣的)论文批评该论文。
我注意到我的依赖注入、观察者模式重度代码(使用Guava的EventBus)常常比我过去写的没有这些特性的代码更难调试。特别是在试图确定何时和为什么调用观察者代码时。
Martin Oderski和他的朋友们写了一篇标题特别吸引人的长文,"Deprecating the Observer Pattern",但我还没有抽出时间去阅读。
我想知道观察者模式有什么问题,还有哪些(提出或其他的)替代方案比它好,以至于这些聪明的人要写这篇论文。
首先,我在这里找到了一篇(有趣的)论文批评该论文。
以下内容引用自该论文:
为了阐明观察者模式的精确问题,我们以鼠标拖动为例,这是一个简单而普遍的例子。下面的示例跟踪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键或使用其他指针设备,如触摸屏幕或图形板,来中止拖动操作。
- 资源管理:观察者的生命周期需要由客户端进行管理。由于性能原因,我们只想在拖动操作期间观察鼠标移动事件。因此,我们需要明确安装和卸载鼠标移动观察者,并且需要记住安装点(上面的控件)。
- 语义距离:最后,示例很难理解,因为控制流被反转,导致有太多样板代码,增加了程序员意图和实际代码之间的语义距离。PropertyChangeEvent
? - Benjamin Peter我认为观察者模式具有与解耦有关的标准缺点。主题与观察者分离,但您不能仅查看其源代码并找出谁观察它。硬编码依赖项通常更易于阅读和思考,但它们更难修改和重用。这是一种权衡。
至于这篇论文,它并不涉及观察者模式本身,而是涉及它特定的用法。特别地:单个被观察对象对应多个无状态观察者对象。这显然具有独立观察者需要相互同步的明显缺点("由于观察者是无状态的,我们通常需要多个观察者来模拟像拖动示例中的状态机。我们必须保存状态,以便所有相关的观察者都可以访问,例如上面的变量路径。")
上述缺点是特定用法的问题,而不是观察者模式本身的问题。您也可以创建一个单一(有状态的!)观察者对象,实现所有OnThis
, OnThat
,OnWhatever
方法,并摆脱跨多个无状态对象模拟状态机的问题。