第一版
首先需求是啥,是一个计算时的缓存设计。
考虑到我们的业务,我们本身是有多种场景的,1 次计算, 1 个 Session,1 个应用。
所以依托于这个思路,设计了第一版
FunctionCache
- 缓存包装CacheStats
- 缓存统计数据- 这里有一个点,就是 Stats 是没有用接口的。这是因为,基本上所有的统计数据,都是统计同样的东西,所以,直接包装进来就行。
CacheListener
- 缓存监听- 写 / 清空, 一般大家很少会对读做监听。所以主要是 写 / 清空 的场景
CacheId
- 每一个缓存都有一个对象去创建,所以,这里包装一个 id 的类,之所以没用 string, 是因为考虑到,可能有些缓存的 cache 需要自定义。所以一般这种 key 值,除非不考虑兼容性,或者数据很多要减少内存占用,否则,我都会用一个类来代表 idCacheLifeCycle
- 一个 Cache 的生命周期。- 创建 / 销毁
CacheScope
- 缓存的范围- Calculation / Session / Application
CacheEvent
- 缓存事件- 包装上 id, scope, lifecycle
- 和
CacheManager
联动起来
CacheManager
- 缓存管理器- Scope - Cache
- Event - init / destory
sequenceDiagram biz->>CacheManager: biz <init> <scope> <cache> event CacheManager->>CacheManager: mark scope->cachemap->cache init biz->>Cache: create Cache->>biz: return biz->>CacheManager: put Cache biz->>CacheManager: <get> <scope> <cache> CacheManager->>biz: Cache biz->>CacheManager: biz <detory> <scope> <cache> event CacheManager->>Cache: destory CacheManager->>CacheManager: clear cache
问题
这样设计完之后,尝试写单测时,发现 scope
不够用。
- 虽然这里可以用 TDD 的模式,不过 TDD 实在是太慢了。
- 比如
Session
的场景下,可能同时有 n 个 Session, 那要怎么给 Cache 创建唯一 Id 呢。虽然之前FineCacheId
确实留下了容错。但是耦合在 Scope 上总归有点难看。 所以需要有一个FineScopeId
- 在需要时要能灵活获取到对应的 ScopeId。如果这里直接提供
SessionScopeId
, 那么为什么一个缓存结构,能感知到自己需要Session
呢? - 使用事件来传输 init/destory, 看上去挺灵活,实际上没必要设计 init/destory 的生命周期。考虑使用方案
create event -> parse event to init/destory -> then action
- 有点炫技的意思,想的太多。没有那么多状态。
- 最重要的是没啥用,实际上的 Cache 是需要外面传进来的。这里 init 只是标记一下, destory 需要销毁,前后不对称。
第二版
考虑到第一版的问题。第二版处理的架构如下
和第一版相比,变更在以下几个部分
LifeCycle
删除。直接转换成对应的方法put
get
Scope
抽象成以下三种, 避免业务的侵入- Thread
- Cross-Thread
- Application
- 提供工具类
FunctionThreadCacheHelper
帮助获取ScopeId
和 生成CacheId
- Thread 级别的 id, 直接用 ThreadLocal 绑定 uuid 即可。
- 提供计算的辅助类
CalculationCacheManager
帮助绑定计算的开始、关闭- 之后其他的业务,也继承后,绑定自己的业务逻辑,使用即可。
变更后的流程图
sequenceDiagram biz->>CalManager: enter CalManager->>ThreadHelper: getThreadId ThreadHelper->>CalManager: return threadId CalManager->>CalManager: mark scope init biz->>Cache: create Cache->>biz: return biz->>ThreadHelper: get threadId biz->>CalManager: put <threadid cacheid> to map
勉强算是脱离业务耦合,但是需要每一次都要查以下 threadhelper 里面的 scopeid。
而且是需要考虑线程的安全性,加锁的。因为不同的线程,可能会同时获取 Cache。
最重要的是, 用起来不优雅 。总觉得哪里怪怪的。比如需要通过 threadhelper 绑定 threadlocal 才能用,真的好么?
我奉行的设计第一原则:如果你要开放你的接口(扩展或直接用),你敢不敢,如果不敢,那一定是你设计错了。
如果只是因为你需要依赖很多的逻辑,那么完全可以封装到一起,提供出去使用。比如外观模式。
代码里面的异味在大多数情况下是不允许的,我入职的时候,我师傅 juju 就说过,他当时重构插件引擎,花了 2 个月。 rinoux 当时做集群的功能,因为设计的不好,就是主动延期,重新设计。
我开始重新思考,怎么样才是最贴合业务的。
原先的设计如下图
感觉不舒服,复杂度高?耦合的东西太多了?那就拆。把 Scope
这个东西打散。让每一个业务自己去管理自己的缓存。于是,将设计转换成如下
基于这个思路,于是有了第三版
第三版
从这个结构图,就可以看到比之前简洁不少。
CacheManager
只管 Cache 的 put/get/destory
- 如果是需要线程安全的,那么额外实现一个锁的即可。
CacheBridge
来桥接各个业务
总结
我一直都觉得代码设计是一件蛮考验天赋的事情。之前跟着 loy 做新引擎的时候,就觉得 loy 设计的代码真 tmd 优雅。自己写的跟一坨 shit 一样。 看过很多书,读过很多设计原则。然而还是没有找到登天之路。没办法,多看,多抄,多写。 多看,看书学架构,看源码学设计。 多抄同类产品。 多写,写个 n 遍出来。总有一版,你会觉得,不管别人怎么看,我能自圆其说。那至少是符合自己的审美的。
最后送大家一句我很喜欢的话
一辈子很短,如白驹过隙,转瞬即逝。可这种心情很长,如高山大川,连绵不绝。
希望大家在做设计或者其他任何事情的时候,都能收获这种心情。