记录的是我的个人项目Epidemic的改进经历,一个阶段写完之后发现问题还是很多,在有针对地画出UML图后就连我这种新手也能对一些问题一目了然。果然设计系统的前期准备工作还是不充分以及还是代码写的少。
原先情况
在一个阶段写完后主要遇到有以下几个情况:
- 不同类之间存在相同元素与相同函数
- 对类的引用设计有问题,导致某个类需要使用另一个类的时候需要重新修改整个类的构造函数,并且在传递的过程中需要经过一两个中间类
- 对定时任务设计不理想,系统中有两种任务调度:定时任务与异步任务
这是原先的UML图例:
Layout相关
可以发现,RouterLayout与StorageLayout之间还存在公共参数。同时定时任务只关联RouterLayout,导致StorageLayout需要执行异步或者定时任务的时候十分困难,在实际的实现中代码十分丑陋。
同时对于最主要的两个基类RouterLayout与StorageLayout,其中有很多方法设计的不合理。比如RouterLayout的findFriend,该方法的含义为判断路由节点是否为第一次启动,如果是则从配置文件中的关联节点获取集群中的与它接近的路由节点填充路由表
,这种定义的函数应该要在RouterLayout启动的时候就要执行。
还有StorageLayout中的store函数,其中的参数是GRPCs中定义的Request,这就使得StorageLayout与RPC模块过于耦合,不利于模块的拆分,不符合软件设计的高内聚低耦合原则。跟这个情况类似的还有RouterLayout的ping函数。
FileStorageLayout中的两个RandomAccessFile
类型的参数,原本的意思是设置这两个参数减少文件写入过程中对目标文件的定位过程,但实际上已经有了一个writeName参数可以用于快速定位,所花费的只有对象创建的开销,而这个开销对比IO来说可以忽略不计;
getVersion与public参数version之间的冲突,两者的功能重复,需要删除一个;
RouterLayout中的persistence()函数用于持久化,但在后续的设计中以及参考其他文件系统的实现中发现,持久化的操作不需要放在主对象中,而是最好放在守护进程中,减少对对象的占用。
strSizeToLong该方法与StorageLayout类的定义格格不入,不符合DDD原则。
ping函数的使用情况对于StorageLayout来说表示根据给入的新节点检查是否有距离该节点更近的文件,如果存在则将其放入Re-Publishing队列中等待定时推送;对于RouterLayout来说表示检查该节点是否已存在与当前路由表中,如果不存在则将其加入。两者虽然都是对一个ping过来的节点执行操作,但是实现的逻辑完全不同。因此需要将其拆分为两个不同的函数用于区分。或者将其重新命名成具有可识别性的新函数。
RouterLayout中的SNAPSHOT_FILENAME参数表示路由表持久化的名称,这个参数不应该放在RouterLayout这种抽象基类中,因为不同的路由实现会有不同的持久化方式,为了兼容多种不同实现,需要将其下移。
改进方法
Layout相关
各种修改方法如下:
- 在StorageLayout与RouterLayout的基础上继续抽象一个抽象类BaseLayout
- 将StorageLayout与RouterLayout各自的storagePath与routerPath抽象为path参数并放置在BaseLayout中
- 将config参数上移到BaseLayout中
- 将version以及versionFileName上移到BaseLayout中,同时也意味着在BaseLayout中实现版本的校验
- 将StorageLayout与RouterLayout持久化相关的消息头上移到BaseLayout中
- 将isCompatibleVersion函数上移到BaseLayout中
- 将RouterLayout与StorageLayout中的ping函数删除并用新的haveNewNode替代,该方法存在BaseLayout中
- 删除FileStorageLayout中的两个RandomAccessFile参数
- 删除getVersion参数
- 删除RouterLayout的persistence函数
- 删除strSizeToLong函数,将其功能交给config实现
- 将RouterLayout的SNAPSHOT_FILENAME参数下移给其实现
- 删除StorageLayout与StorageLayout的load函数,其功能由BaseLayout的before函数替代。
- 删除StorageLayout的chunkSize参数,该参数的获取由config提供
- 将StorageLayout的find函数修改为findFile
最终结构如下:
Timer相关
首先是对这一块功能进行分析,在系统中涉及到的主要有三个功能:
- 持久化路由节点
- 定时与路由表中的节点进行通信,确认其中有那些节点已经不可达
- 定时执行Re-Publishing操作,将文件推送给距离更近的节点
其中第一点由于有到达一定数量就触发以及操作日志,因此并不需要将路由表完全持久化到磁盘,因为没有持久化到磁盘的也会被写入操作日志,在重启的时候对操作日志进行恢复即可。因此这一步操作可以放置在线程组中进行。
第2与第3点可以作为两个守护线程分别执行。
第2点需要获取RouterLayout的路由表信息。第3点需要获取StorageLayout的待Re-Publishing文件列表。虽然都需要对应Layout的数据,但是两者确实完全不同的内在。对于第2条来说,执行检查的时候需要获取到当前RouterLayout维护的整张路由表的备份然后进行操作,每一次操作总是要获取到一次完整数据;而对于第3点来说,只需要满足Re-Publishing条件的文件信息即可,并不需要每次都读取全部文件信息。因此这两者的实现是完全不一样的。
- 第2点:需要获取到RouterLayout的对象引用,并在合适的时候获取路由表的复制。
- 第3点:需要建立与StorageLayout的单项通信队列,当要执行Re-Publishing的时候从队列中获取满足条件的文件信息即可
对于RouterLayout来说实际维护路由表的类是KademliaBucket,因此定时任务调用的也应该是这个对象。
最终情况如下
恩,比原来清晰很多,接下里就是按照这个修改代码了。