這幾年的開發(fā)中,最讓人頭疼的事情之一就是數(shù)據(jù)統(tǒng)計。這里就來看看以單向數(shù)據(jù)流的角度如何改進統(tǒng)計系統(tǒng)的設計。
面向切面埋點
我非常的反對使用面向切面埋點來處理用戶行為,理由有三個:
- 統(tǒng)計數(shù)據(jù)極度依賴視圖結構,或者需要將每個數(shù)據(jù)綁定到視圖上。
- 不能完成復雜的交互統(tǒng)計,僅能實現(xiàn)簡單的事件數(shù)據(jù)。
- 視覺上的修改會影響統(tǒng)計結果。
以前在使用切面埋點的時候,就遇到很多的問題,雖然說每個數(shù)據(jù)點都不可能漏埋或者錯埋,但是每次上線后數(shù)據(jù)分析都需要跑過來讓開發(fā)給他們看看這些行為的埋點數(shù)據(jù)是怎么樣的。這樣也很難實現(xiàn)一個多期的版本對比。
用戶行為統(tǒng)計
那么按照標準的用戶行為統(tǒng)計又有哪些問題呢?
- 每個數(shù)據(jù)深入業(yè)務底層。需要統(tǒng)計要么把事件層層代理到Controller,要么在底層這些看似不合理的地方埋點。
- 復用問題。業(yè)務雖然一樣,但是埋點信息并不能完全保持一致,而且有些場景下也無法保持一致,因為可能會有重復場景。
- 埋點數(shù)據(jù)回歸測試。由于是人工埋點,所以可能會漏埋錯埋的情況發(fā)生。
目前
目前我們的埋點方案主要有3點:
- 數(shù)據(jù)盡量保持統(tǒng)一。相同的業(yè)務埋相同的點,然后根據(jù)頁面區(qū)分。這樣就能夠實現(xiàn)重用,缺點是有少部分需要特殊化的場景。
- 代理到業(yè)務層,然后再埋。缺點是如果中間層次過多,會出現(xiàn)多級代理,而僅僅是為了埋點。
- 子類化。專門子類化該頁面的專有子類。缺點是子類的目的就是為了區(qū)分埋點,有點多余。
以上都沒有一個很好的方案能夠解決數(shù)據(jù)回歸測試的問題。而回歸測試也只能靠人工執(zhí)行。
單向數(shù)據(jù)流方案
統(tǒng)計即是數(shù)據(jù),那么當然也非常符合數(shù)據(jù)流模型,那么我們就用數(shù)據(jù)流模型來簡化埋點方案,增加每個模塊的獨立性和復用性,同時也把埋點放到一個地方去做,減少埋點數(shù)據(jù)在整個應用內的散亂分布。
以上就是這套方案的大概結構。用戶觸發(fā)行為時,和之前直接統(tǒng)計行為不同,而是創(chuàng)建一個Action對象,將統(tǒng)計所需要的參數(shù),或者自身包含數(shù)據(jù)包裝在Action內,發(fā)送給Store。Store作為一個數(shù)據(jù)中心,負責接收和分發(fā)數(shù)據(jù),他將收到的數(shù)據(jù)分發(fā)給訂閱者Subscriber,最后由Subscriber完成統(tǒng)計數(shù)據(jù),并上報服務器。
Store、Action是完全可復用的,同時這兩者并不關聯(lián)實際業(yè)務,所以完全可以模塊化,同時只要行為足夠完整,也不需要關系具體業(yè)務方統(tǒng)計數(shù)據(jù)的樣式。這樣就可以讓其他模塊完全的復用了。
那么如何提升復用性,我們來關聯(lián)下之前討論過的MVP。
這里,紅色框內的部分都是邏輯性的,是完全可復用的;View也是獨立與邏輯的,也是可復用的;只有Subscriber和Controller是和業(yè)務強相關的,是不可復用的。那么我們就可以知道需要把哪些東西放到不可復用的地方,哪些東西放到可以復用的地方了。
同時我們也需要考慮下測試的問題,來解決埋點數(shù)據(jù)的完整性和正確性。
只要我們mock了Store部分,就可以輕易的檢查發(fā)生的Action,或者向訂閱者發(fā)送對應的Action,這樣就可以比較簡單的去回歸測試數(shù)據(jù)了。只不過這樣做的收益可能并不高。
實現(xiàn)
這里我們來看看實現(xiàn)的方式。
首先定義基礎的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來分發(fā)行為就可以了。
let action = StatStore(type: "star", params: ["id": "1234"])
self.store?.dispatch(action)
這里訂閱者甚至可以自己創(chuàng)建獨立的類來處理這些情況,這樣就更加的分離了行為統(tǒng)計這種不能劃分為任何模塊的內容了。
最后
這個方案將行為統(tǒng)計從整個app中剝離出一個單獨的模塊,同時實現(xiàn)了高度可復用性,而且使得統(tǒng)計也成為可以單元測試的了。唯一的缺點是在具體統(tǒng)計的時候需要大量switch...case...來區(qū)分不同的行為。