为什么几乎所有观察者模式的实现代码都是用副作用实现的?

258 天前
 netabare

感觉很难理解,因为几乎所有的教程/教材,样例代码或者实际代码里面,提到观察者模式的时候,都是清一色的用返回 void 的方法来 visit 和 accept ,然后依赖副作用和全局变量来返回结果。

感觉这样的代码很难懂而且很绕,也可能因为我比起命令式或 OOP 代码更容易理解纯函数式代码吧。

就,比如说我试图理解 visitor pattern 的话,我会把它当成一个「在命令式语言里通过动态分派实现模式匹配」的技巧。自然而然的就会想出这样的代码,比如说在 Java 里的话:

public ISomeVisitor<T> {
  public T visit(DerivedDataTypeA data);
  public T visit(DerivedDataTypeB data);
    ...
}
public IDataType {
  public T accept(ISomeVisitor visitor);
}

然后在 visitor 的具体实现里面,就只需要去重写然后使用 DerivedDataType 里面的访问方法去处理它,然后返回一个转换后的?T 类型。同时,由于每个 visit 分支都返回相同的类型,它们可以被组合起来,看起来就跟比如 Scala 或者 ML 语言里的模式匹配是同样的方式。

嗯,这个是理想情况。

但我记得在大学里学习 OOP 然后第一次按照这个路子写 visitor 后,就被其他人纠正说我实现的方法不对,因为我在 visit 函数里实现了具体逻辑。嗯,我一直没能搞懂为什么我实现错了,其实现在我也没搞懂。

但是我其实对返回 void 的方法和没有参数化泛型的类感觉更难理解,大概有这么几个原因吧:

所以觉得这样的代码既难以理解又难以维护。

当然也许有人会说顺序很重要,但是一般来说 visitor 在业务代码例如 web 应用里面都是用在树状数据结构上,这种使用场景应该没差?

所以我感觉困惑的大概就是,我这个 visitor 的实现思路错在了哪里?为什么几乎清一色的所有 visitor 的代码实现都是返回 void 方法并且通过副作用修改全局变量来储存返回计算结果的?这样做是为了什么呢?

然后这个草稿写完之后又读了一本叫 A little Java, A few Patterns 的书,就感觉更困惑了。因为这本书里的 visitor 不但也是不依赖副作用而是返回值的,它的 visit 函数甚至还可以接受多个参数,看起来更不符合 visitor 的一般定义。

所以我该怎么理解这个 visitor pattern 呢?

3898 次点击
所在节点    程序员
23 条回复
netabare
252 天前
@zhuisui
@mahaoqu

感谢讲解!

所以这里我想我可能陷入了一个很大的误区,就是把 visitor 机械地等同于 pattern matching ,然后把 pattern matching 在 subtyping 下面的作用(也就是 dynamic dispatch )给搬运到了 visitor pattern 下,但是这个 dynamic dispatch 只是 visitor 的其中一个「稍微显著但不是主要的作用」,而 visitor pattern 的主要作用,就像 @mahaoqu 说的,「面向对象把一个类的多个方法放在一起,而 Visitor 模式恰好反过来了」,或者 @zhuisui 所说的「多个 visitor 分别代表可以输出不同形式的业务逻辑,visitor 之间是互相独立的」这样的作用。

我的理解是你说的「避免了业务侧用 switch case 做模式匹配,仅需 iterate elements 的 accept 方法即可完成调用」用我前面的表达其实就是「 dynamic dispatch 」对吧。比如用户解析 Tree 的时候,它不关心 Tree 具体长啥样或者具体怎么解析,它只希望能够正确的拿到 XML/JSON 或者 Table 。那么 visitor pattern 就充当了这个解析的作用吧。

这么说来倒是很多东西都说得清了。

至于副作用的问题,主要是我对这样的代码有很大的意见:

```java
class ... {
/* L.18 */ResultType result;

public void visitSomeArm(...) {

/* L.259 */ ResultType oldResult = result;

/* L.343 */ ... = visitAnotherArm(...);

/* L.569 */ result = ...;
}

public void VisitAnotherArm(...) {
/* L.1982 */ result = ...
}
}
```

当然这个其实并不是 effectful visitor 的问题而更像是某种 structural 的 code smell 了,如果一个 visitor 的实现能够把副作用清晰的表达出来,让我能够人肉去建模出 Effect 大概长啥样,我对这样的代码并没有太大的意见。

顺祝新年快乐!
zhuisui
251 天前
倾向于用一组特别命名的私有方法去处理副作用,甚至单独封装成一个内部类,把状态对象的修改都放到里面,visitor 像调用纯函数一样调用这组方法。
netabare
248 天前
@zhuisui 这个也是我的想法倒是…如果非要使用副作用的话

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1101064

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX