今天在微博上表示,React在面向对象编程方面有两个启示:组件模型的接口设计,以及生命周期管理;比较抽象,这里举个例子来讨论一些细节。这个例子是我正在写的一个项目。有一个功能,多路复用/解复用的多个流出现在一个连接上;考虑到使用方便,应该提供给用户,比如node的stream.Readable/stream.Writable类实例。Connection是node中的一个流对象。实际项目中可能是tcp/tls,也可能是duck-type之类的duplexstream。每一个连接都对应着通信的另一方,但是除了连接之外还有其他的状态需要维护,所以首先有一个类叫Peer,里面包含了一个连接,大致是这样的:classPeerextendsEventEmitter{constructor(conn){super()this.conn=conn...}}DemuxaWritablefromconn;我们假设每个Writable都有一个唯一的id;Peer可以有Array或Map来维护所有的Writables。当Writable是用户写的时候,它必须能够将数据打包发送,所以需要一个可以调用peer.conn上的write方法的方法,或者封装一个peer.write方法;当用户写完Writable的end会被调用,Writable会发出finish,这似乎是从Peer中移除Writable的好地方;用户也可能出于某种原因提前中止流,它应该调用destroy方法,这是节点的流。根据设计,开发人员应遵循此约定,但方法可以重载。比较友好的实现是在destroy的时候给对方发送一个error包,通知对方流已经异常取消。如果Writable内部有一些逻辑,比如编码,也可能会出错;根据node的设计习惯,对象都是一次性的,一旦出错就会被丢弃,不考虑修复。相反方向也有几个错误的逻辑:Peer被上层终止,比如调用了Peer.end(),此时会清理所有Writables,可以抛出异常;Peer的另一端决定放弃这个Writable,abort;对端连接断开也是类似的灾难情况,需要优雅的处理。常见的设计方法是:classWritableextendsstream.Writable{constructor(id,peer){super()this.peer=peer}_write(chunk,encoding,callback){this.peer.write(chunk,encoding,callback)}_final(callback){this.peer.write('byebyemydeer',callback)}_destroy(err,callback){this.peer.write('Iamdestroyed')callback()}}不熟悉节点流的朋友可能需要了解;节点允许自己继承和实现一个流;这个继承的流,像这里一样,提供了内部调用的以_开头的函数,对应write、end、destroy三个public方法;这样实现的stream最大的好处就是node保证了这些方法是顺序调用的。比如在_write实现中调用回调参数之前,不会调用这个方法,或者其他方法,这给开发者带来了很大的好处。非常方便,无需自己处理并发和排队问题。对于熟悉节点流代码的朋友来说,这段代码是家常便饭。没什么好讨论的。那么Writable的生命周期维护者Peer什么时候会把这个对象从自己的队列中移除呢?当它成功结束(end)时,它可以监听finish;如果是错误,它可以监听错误;但是这个destroy有点烦人,它不会抛出任何事件;当然,这虽然破坏了美感,但并不构成任何实际困难,大家可以直接操作同行数据,自行去除。然后我们仔细考虑错误处理:如果错误来自上层终止整个连接,或者对方挂断了整个连接,或者对方决定中止,只要触发Writable发出错误即可;如果Writable本身有内部错误,也可以直接抛出;在Writable构造函数结束之前,可以给自己挂一个errorhandler,这样可以直接处理各种错误。只需要在抛出错误时区分连接是否仍然可用,如果可用则发送一个数据包给对方通知。如果实际代码是这样写的,其实我觉得没什么问题,错误处理覆盖率也够了,就不用多求了;即使粗略考虑有模糊的细节,也可以写代码和测试来澄清;我相信这样写的代码在任何公司都不会被批评或解雇。但是让我们吹毛求疵:首先,我们谈论的是React;React在任何情况下都不会传递组件引用,只会传递props,包括BoundFunction;所以Writable的this.peer参考不是很好;Peer有更多的功能继续,所有对Writable的接触都是有范围的;我们实际上只看到两个需要它的地方。第一个是需要调用peer.write,第二个是销毁的时候需要把自己remove掉。对于第一个,对等方可以传入一个绑定函数;第二种,也可以有这样一个函数,封装了移除Writable的过程,但是应该叫什么?我们称它为deleteMe。那我们想想这段代码有什么问题?我能想到几个:第一,Writable维护者(Peer)和Writable之间在生命周期维护上有很多约定吗?Peer需要知道Writable有一个finish事件,也需要知道它有一个叫做error的事件,相当于finish;最后,还得给他提供deleteMe之类的东西?为什么Peer需要懂那么多?如果下次不是Writable,改名为Readable,finish事件改名为end,Peer也需要知道吗?其次,EventEmitter.emit本身是一个同步过程。如果Peer收到连接错误,直接调用:writable.emit(error)如果清除writable的代码直接hook在error事件上,当然这很可能是OK的;但是如果你需要异步呢?哦,好像不是直接调用writable.emit(err),而是可以有个handleError之类的方法缓冲一下,清理完再抛出错误;但是这也有一个问题,如果这个流的用户有一个非常耗资源的过程,比如准备要写入的数据,它应该尽早得到错误,对吧?仔细想想就会明白,Writable的所有接口方法和事件都是为用户服务的,而不是为它的生命周期维护者服务的。它的生命周期维护者可以与它合作,以更简单和保密的协议完成生命周期维护的任务。finish或error代表另一种finish,Peer不需要理解。Peer只需要Writable告诉它“我被终止了”。当它完成时。我相信你会同意它需要一个更好更准确的名字:onStreamTerminated(id)你看,这样当你的下一个任务是在demux中实现一个ReadableStream时,你不需要做任何更改,对吧?这是非常反应性的,对吧?这是一个Peer传递给Writable的Prop,是一个绑定函数。调用的时候,控制权返回给Peer,它把这个writable(或者futurereadable)去掉,相当于setState之后重新渲染,只是这次是同步的。至于Writable提供的那一套方法和事件,或者下次改成可读的时候,又会出现另一套方法和事件。它们就像您在容器中嵌入另一个组件。这些方法和事件就是组件和用户。或者其他你看不到的组件,它们之间的协议/接口;组件设计思想最重要的部分是你需要知道哪个是“你的”接口,不要仅仅因为手头有参考就使用它们中的任何一个。使用。下一个问题:Peer可以调用Writable的destroy方法吗?我的回答是No,Peer是Writable的生命周期维护者,但Peer不是Writable的使用者。Peer只要和Writable有两个boundfunction协议就可以工作,一个是write,一个是onStreamTerminated。为什么Peer需要知道Writable还有哪些其他状态和行为细节?相信在很多类似的场景下,开发者都会编写这样的错误处理代码,即在Peer中主动处理错误,直接销毁Writable(或Readable)。这个可以吗?可能的。如果把Destroy看成一个生命周期方法,它的拥有者就是它的生命周期维护者;就像React有ComponentDidMount这样的钩子,当生命周期维护变得复杂时,components需要提供很多生命周期方法供维护者使用;然而!如果destroy方法是生命周期方法,则禁止用户使用。至少在node中,这不是一个好办法。它打破了接口的语义约定,这是开发的大忌。最后:你会在Writable上安装一个handleError方法吗?从这里塞满各种错误?作为界面设计师,我认为这不是一个好的做法,原因有二。首先,功能接口应该追求文字而不是抽象。这不是算法。handleError这个名字是没有意义的,十有八九应该用if/then/else在里面,那为什么不这样设计接口呢?:peerAbort(err)//对方已经放弃connectionFinished()//节点习惯表示自己结束connectionEnded()//节点习惯表示对方已经结束。这不是一目了然吗?其次,你会发现上面写的单独的函数位于不同的事件处理器中,相互之间没有干扰,逻辑清晰。我借用一个叫做串扰的硬件术语来指代使用handleError函数来混合这些错误处理路径的方法。串扰是指信号距离太近,存在干扰。分离干扰还有性能优势,因为JavaScript内联和内联缓存可以更好地工作。以上是基于React的设计思想理解的一个例子。如果你不从React中理解它也没关系;but:最小的接口,Writable并没有拿到整个Peerreference为所欲为;耦合度最低,Peer不了解Writable的行为,也不会调用Writable的任何方法,只提供Writable的两个必要方法;BeLiteral,不要盲目混用代码处理路径,尤其是错误处理;我相信这些是普世价值;React的借鉴意义在于ReactComponent之间没有引用,props是组件之间唯一的接口协议,清晰无歧义,不扩大范围,是最好的耦合设计;React有严格的自上而下的生命周期维护结构;它实际上赋予了一个组件生命周期维护维护者的特殊身份,维护者与维护者不应该通过用户界面进行通信,他们之间需要有一个“秘密通道”作为双方之间的工作协议,所以以免污染用户界面;例如,传递onStreamTerminated优于streame.emit('terminated')方法。可能你还想反抗说,添加到流中的terminated事件可能不仅仅被peer使用,其他用户可能也需要这个finish||错误逻辑;我对此的回答是:让Peer安装一个onStreamTerminated该方法的所有流是否可用,或者它的所有流是否都有终止事件?以上,个人看法,仅供参考。编写代码的乐趣之一是您可以随时更加挑剔。
