第一版

首先需求是啥,是一个计算时的缓存设计。
考虑到我们的业务,我们本身是有多种场景的,1 次计算, 1 个 Session,1 个应用。
所以依托于这个思路,设计了第一版

500

  • FunctionCache - 缓存包装
  • CacheStats - 缓存统计数据
    • 这里有一个点,就是 Stats 是没有用接口的。这是因为,基本上所有的统计数据,都是统计同样的东西,所以,直接包装进来就行。
  • CacheListener - 缓存监听
    • 写 / 清空, 一般大家很少会对读做监听。所以主要是 写 / 清空 的场景
  • CacheId - 每一个缓存都有一个对象去创建,所以,这里包装一个 id 的类,之所以没用 string, 是因为考虑到,可能有些缓存的 cache 需要自定义。所以一般这种 key 值,除非不考虑兼容性,或者数据很多要减少内存占用,否则,我都会用一个类来代表 id
  • CacheLifeCycle - 一个 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 实在是太慢了。
  1. 比如 Session 的场景下,可能同时有 n 个 Session, 那要怎么给 Cache 创建唯一 Id 呢。虽然之前 FineCacheId 确实留下了容错。但是耦合在 Scope 上总归有点难看。 所以需要有一个 FineScopeId
  2. 在需要时要能灵活获取到对应的 ScopeId。如果这里直接提供 SessionScopeId, 那么为什么一个缓存结构,能感知到自己需要 Session 呢?
  3. 使用事件来传输 init/destory, 看上去挺灵活,实际上没必要设计 init/destory 的生命周期。考虑使用方案
    1. create event -> parse event to init/destory -> then action
    2. 有点炫技的意思,想的太多。没有那么多状态。
    3. 最重要的是没啥用,实际上的 Cache 是需要外面传进来的。这里 init 只是标记一下, destory 需要销毁,前后不对称。

第二版

考虑到第一版的问题。第二版处理的架构如下

和第一版相比,变更在以下几个部分

  1. LifeCycle 删除。直接转换成对应的方法 put get
  2. Scope 抽象成以下三种, 避免业务的侵入
    1. Thread
    2. Cross-Thread
    3. Application
  3. 提供工具类 FunctionThreadCacheHelper 帮助获取 ScopeId 和 生成 CacheId
    1. Thread 级别的 id, 直接用 ThreadLocal 绑定 uuid 即可。
  4. 提供计算的辅助类 CalculationCacheManager 帮助绑定计算的开始、关闭
    1. 之后其他的业务,也继承后,绑定自己的业务逻辑,使用即可。

变更后的流程图

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 当时做集群的功能,因为设计的不好,就是主动延期,重新设计。
我开始重新思考,怎么样才是最贴合业务的。

原先的设计如下图
500

感觉不舒服,复杂度高?耦合的东西太多了?那就拆。把 Scope 这个东西打散。让每一个业务自己去管理自己的缓存。于是,将设计转换成如下

500

基于这个思路,于是有了第三版

第三版

500

从这个结构图,就可以看到比之前简洁不少。

CacheManager 只管 Cache 的 put/get/destory

  • 如果是需要线程安全的,那么额外实现一个锁的即可。
    CacheBridge 来桥接各个业务

总结

我一直都觉得代码设计是一件蛮考验天赋的事情。之前跟着 loy 做新引擎的时候,就觉得 loy 设计的代码真 tmd 优雅。自己写的跟一坨 shit 一样。 看过很多书,读过很多设计原则。然而还是没有找到登天之路。没办法,多看,多抄,多写。 多看,看书学架构,看源码学设计。 多抄同类产品。 多写,写个 n 遍出来。总有一版,你会觉得,不管别人怎么看,我能自圆其说。那至少是符合自己的审美的。

最后送大家一句我很喜欢的话

一辈子很短,如白驹过隙,转瞬即逝。可这种心情很长,如高山大川,连绵不绝。

希望大家在做设计或者其他任何事情的时候,都能收获这种心情。