在线文档的网络层开发思考--依赖关系梳理
更新日期:
最近在负责通用网络层的设计和开发,会记录该过程中的一些思考,本文主要介绍接入层设计过程中的一些依赖关系,以及处理这些依赖关系的一些思考。
在上一篇文章中,我尝试使用职责驱动设计来重新梳理了接入层的职责对象,最终得到了这样的依赖关系图:
这里的依赖关系表示得很简单,实际上这样简单的表示是无法完成代码开发的,我们还需要根据每个对象的职责将它们之间的协作方式整理出来,可以通过接口或者 UML 图的方式来进行。
依赖关系梳理
技术方案设计离不开业务,我们开发的很多工具和 SDK 最终也是服务与业务,因此我们首先需要梳理出网络层与业务侧的一些依赖关系,从而可得到更加明确的职责范围。
梳理网络层与业务侧依赖
原先的网络层由于历史原因与业务中其他模块耦合严重,其中网络层的代码中对其他模块(包括数据层、离线模块、worker 模块等)的直接引用以及使用事件通信多达 50+处。因此,如果希望重构后的网络层能正常在业务中使用,我们首先需要将相关依赖全部梳理出来,确认是否可通过适配层的方式进行解耦,让网络层专注于自身的职责功能。
经过梳理,我们整理出网络层的与业务层的主要依赖关系,包括:
- 业务侧为主动方时:
- 业务侧将数据提交到网络层
- 业务侧可控制网络层工作状态,可用于预防异常的情况
- 业务侧主动获取网络层自身的一些状态,包括网络层是否正确运行、网络层状态(在线/离线)等
- 业务侧为被动方时:
- 网络层告知业务侧,需要进行数据冲突处理
- 网络层告知业务侧服务端的最新状态,包括数据是否递交成功、是否有新的服务端消息等
- 网络层告知业务侧自身的一些状态变更,包括网络层状态变更(异常/挂起)、网络层工作是否存在异常等
除此之外,网络层初始化也依赖一些业务侧的数据,包括初始版本信息、用户登录态、文档 ID 等等。
到这里,我们可以根据这些依赖关系,简化网络层与业务侧的关系:
能看到,简化后的网络层与业务侧关系主要包括三种:
- 业务侧初始化网络层。
- 业务侧给网络层提交数据,以及控制网络层的工作状态。
- 业务侧监听网络层的状态变更。
前面我们也说了,业务侧与网络层的协作主要通过接入层的总控制器来完成,也就是说总控制器的职责和协作方式包括:
- 初始化整个网络层,创建网络层运行需要的各个协作对象,在这里总控制器也可视作创建者(creator)。
- 通过提供接口的方式,对业务层提供数据提交(
addData()
)和控制网络层状态(pause()
/resume()
/shutdown()
)的方法。 - 通过提供事件监听的方式,对业务层提供网络层的各种状态变更(
onNetworkChange()
/onDataCommitSuccess()
/onDataCommitError()
/onNewData()
)。
具体网络层中总控制器是如何调度其他对象进行协作的,这些细节不需要暴露给业务侧。在对齐了业务侧的需要之后,我们再来看看具体网络层的细节。
总控制器的职责梳理
对业务侧来说,它只关注和网络层的协作,不关注具体网络层中接入层和连接层的关系。而对于接入层来说,其实它对连接层有直接的层级关系,因此这里我们将连接层以及服务端视作一个单独的职责对象:
实际上这些模块之间的依赖关系比这些还要复杂得多,比如发送数据控制器和接受数据控制器都会直接依赖连接层。为了方便描述,这里我们就不纠结这些细节了。
初始化
前面也说了,总控制器需要负责整个网络层的初始化,因此它需要控制各个职责对象的创建。那么,图中发送数据控制器和接受数据控制器对其他对象的依赖,可以通过初始化控制器对象时注入的方式来进行控制。
如果是注入的方式,则这样的依赖关系可描述为对接口的依赖,我们用虚线进行标记:
其中虚线的地方,都可以理解为初始化时需要注入的依赖对象。初始化相关的代码大致会长这样:
1 | class NetworkController { |
这里虽然我们传入了实例对象,但在对象内部,依赖的对象除了是实例,还可以是抽象的接口。
使用依赖倒置进行依赖解耦
依赖倒置原则有两个,其中包括了:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。
以SendDataController
为例,它依赖TaskListManager
其实主要是依赖的添加任务的接口addTask()
,依赖DataListManager
则是依赖添加数据pushData()
、取出数据shiftData()
,则我们可以表达为:
1 | interface ITaskListManagerDependency { |
实际上,我们可以给每个对象提供自身的接口描述,这样其他对象中可以直接import
同一份接口也是可以的,管理和调整会比较方便。
如果项目中有完善的依赖注入框架,则可以使用项目中的依赖注入体系。在我们这个例子里,总控制器充当了依赖注入的控制角色,而具体其中的各个对象之间,实现了基于抽象接口的依赖,成功了进行了解耦。依赖注入在大型项目中比较常见,对于各个模块间的依赖关系管理很实用。
提供接口和事件监听
除了初始化相关,总控制器的职责还包括对业务层提供接口和事件监听,其中接口中会依赖具体职责对象的协作:
1 | class NetworkController { |
在最初的设计中,我们的状态变更这些也是通过注册回调的方式进行设计的:
1 | interface INetworkControllerOptions { |
这种方式意味着我们需要将这些接口实现保存下来,并传入到各个对象内部分别在恰当的时机进行调用,调用的时候还需要关注是否出现异常,同样以SendDataController
为例:
1 | interface ICallbackDependency { |
除此之外,这种方式还导致了业务侧在使用的时候,初始化就要传入很多的接口实现:
1 | const netWorkLayer = new NetworkController({ |
可以看到,业务侧中初始化网络层的代码特别长(传入了 20 多个方法),实际上在不同的业务中这些接口可能是不必要的。
使用事件驱动进行依赖解耦
在这里,我们使用了事件处理模型-观察者模式。事件驱动其实常常在各种系统设计中会用到,可以解耦目标对象和它的依赖对象。目标只需要通知它的依赖对象,具体怎么处理,依赖对象自己决定。
事件监听的实现,参考了VsCode 的事件系统设计的做法,比如在SendDataController
中:
1 | class SendDataController { |
在总控制器中,可以同样通过事件监听的方式传递出去:
1 | class NetworkController { |
使用事件监听的方式,业务方就可以在需要的地方再进行监听了:
1 | const netWorkLayer = new NetworkController({ |
到这里,我们可以简单实现了总控制器的职责,也通过接口和事件监听的方式提供了与外界的协作方式,简化了业务侧的使用过程。
结束语
在本文中,主要根据业务侧与网络层的依赖关系,清晰地梳理出总控制器的职责和协作方式,并尝试对其中的依赖关系进行解耦。
而具体到网络层中每个对象的设计和实现,也都是可以通过接口的方式提供给外部使用某些功能、通过事件监听的方式提供给外部获取状态变更。而恰当地使用依赖倒置原则和事件驱动的方式,可以有效地解耦对象间的依赖。
码生艰难,写文不易,给我家猪囤点猫粮了喵~
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢