这几年的开发中,最让人头疼的事情之一就是数据统计。这里就来看看以单向数据流的角度如何改进统计系统的设计。
面向切面埋点
我非常的反对使用面向切面埋点来处理用户行为,理由有三个:
- 统计数据极度依赖视图结构,或者需要将每个数据绑定到视图上。
- 不能完成复杂的交互统计,仅能实现简单的事件数据。
- 视觉上的修改会影响统计结果。
以前在使用切面埋点的时候,就遇到很多的问题,虽然说每个数据点都不可能漏埋或者错埋,但是每次上线后数据分析都需要跑过来让开发给他们看看这些行为的埋点数据是怎么样的。这样也很难实现一个多期的版本对比。
用户行为统计
那么按照标准的用户行为统计又有哪些问题呢?
- 每个数据深入业务底层。需要统计要么把事件层层代理到Controller,要么在底层这些看似不合理的地方埋点。
- 复用问题。业务虽然一样,但是埋点信息并不能完全保持一致,而且有些场景下也无法保持一致,因为可能会有重复场景。
- 埋点数据回归测试。由于是人工埋点,所以可能会漏埋错埋的情况发生。
目前
目前我们的埋点方案主要有3点:
- 数据尽量保持统一。相同的业务埋相同的点,然后根据页面区分。这样就能够实现重用,缺点是有少部分需要特殊化的场景。
- 代理到业务层,然后再埋。缺点是如果中间层次过多,会出现多级代理,而仅仅是为了埋点。
- 子类化。专门子类化该页面的专有子类。缺点是子类的目的就是为了区分埋点,有点多余。
以上都没有一个很好的方案能够解决数据回归测试的问题。而回归测试也只能靠人工执行。
单向数据流方案
统计即是数据,那么当然也非常符合数据流模型,那么我们就用数据流模型来简化埋点方案,增加每个模块的独立性和复用性,同时也把埋点放到一个地方去做,减少埋点数据在整个应用内的散乱分布。
以上就是这套方案的大概结构。用户触发行为时,和之前直接统计行为不同,而是创建一个Action对象,将统计所需要的参数,或者自身包含数据包装在Action内,发送给Store。Store作为一个数据中心,负责接收和分发数据,他将收到的数据分发给订阅者Subscriber,最后由Subscriber完成统计数据,并上报服务器。
Store、Action是完全可复用的,同时这两者并不关联实际业务,所以完全可以模块化,同时只要行为足够完整,也不需要关系具体业务方统计数据的样式。这样就可以让其他模块完全的复用了。
那么如何提升复用性,我们来关联下之前讨论过的MVP。
这里,红色框内的部分都是逻辑性的,是完全可复用的;View也是独立与逻辑的,也是可复用的;只有Subscriber和Controller是和业务强相关的,是不可复用的。那么我们就可以知道需要把哪些东西放到不可复用的地方,哪些东西放到可以复用的地方了。
同时我们也需要考虑下测试的问题,来解决埋点数据的完整性和正确性。
只要我们mock了Store部分,就可以轻易的检查发生的Action,或者向订阅者发送对应的Action,这样就可以比较简单的去回归测试数据了。只不过这样做的收益可能并不高。
实现
这里我们来看看实现的方式。
首先定义基础的Store和Action
class StatAction {
var type: String?
var params: [String: Any]?
}
protocol StatSubscriber {
func newStatAction(action: StatAction)
}
class StatStore {
func dispatch(_ action: StatAction) {}
func subscribe(_ subscriber: StatSubscriber) {}
func subscribe(_ subscriber: (StatAction)->Void) {}
}
那么在ViewController里就可以这样配置。
func viewDidLoad() {
super.viewDidLoad()
self.store = StatStore()
self.store?.subscribe({ action in
// ... switch case action.type.
// Track
});
self.submodule.store = self.store
}
而子模块中只需要使用store来分发行为就可以了。
let action = StatStore(type: "star", params: ["id": "1234"])
self.store?.dispatch(action)
这里订阅者甚至可以自己创建独立的类来处理这些情况,这样就更加的分离了行为统计这种不能划分为任何模块的内容了。
最后
这个方案将行为统计从整个app中剥离出一个单独的模块,同时实现了高度可复用性,而且使得统计也成为可以单元测试的了。唯一的缺点是在具体统计的时候需要大量switch...case...来区分不同的行为。