背景
相信很多同學(xué)對(duì)于祖?zhèn)鞔a都有極其恐怖的體驗(yàn),不改他難以維護(hù)、難以支撐新業(yè)務(wù),改了又會(huì)冒出一堆莫名其妙的 bug,而且,當(dāng)這些代碼以模塊的形式大量的出現(xiàn)在工程中時(shí),估計(jì)想死的心都有了。
湊巧的是,在我們的項(xiàng)目里就充斥著這樣的代碼,在我接手這個(gè)項(xiàng)目的時(shí)候這個(gè)項(xiàng)目已經(jīng)傳了4代了,并且第一代寫(xiě)這個(gè)項(xiàng)目的程序員不是寫(xiě) Android 的,他們寫(xiě)的很多代碼還仍被當(dāng)做核心模塊留在工程中,可能也是前幾代開(kāi)發(fā)者對(duì)祖?zhèn)鞔a充滿恐懼,所以一有新業(yè)務(wù)就僅是在其基礎(chǔ)上不斷添加代碼,從而導(dǎo)致項(xiàng)目越來(lái)越臃腫,很多模塊都難以讀懂。
因?yàn)槲沂且粋€(gè)對(duì)代碼有潔癖的人,并且在項(xiàng)目正常維護(hù)(開(kāi)發(fā)新內(nèi)容、bugfix)的時(shí)候,這些祖?zhèn)鞔a已經(jīng)難以再支撐下去,于是當(dāng)我負(fù)責(zé) Android 端整個(gè)項(xiàng)目的時(shí)候,我就決定大刀闊斧的對(duì)這些內(nèi)容進(jìn)行整體的重構(gòu),也是為后來(lái)想在項(xiàng)目中做出更多創(chuàng)新打下基礎(chǔ)。
分享一個(gè)模塊的重構(gòu) - 數(shù)據(jù)庫(kù)模塊
因?yàn)槲覀冺?xiàng)目的特殊性,大量的用戶數(shù)據(jù)都被存儲(chǔ)在本地的 SQLite 數(shù)據(jù)庫(kù)中,所以數(shù)據(jù)庫(kù)模塊成為了我們重點(diǎn)維護(hù)的核心模塊,這次分享也以這個(gè)模塊的重構(gòu)為基礎(chǔ)。
老數(shù)據(jù)庫(kù)模塊存在的問(wèn)題:
一. 數(shù)據(jù)庫(kù)表結(jié)構(gòu)設(shè)計(jì)混亂
用戶主要在我們的項(xiàng)目中通過(guò)做任務(wù)去獲取獎(jiǎng)勵(lì),任務(wù)有多個(gè)類型,用戶所做的任務(wù)都會(huì)存儲(chǔ)在數(shù)據(jù)庫(kù)中等待上傳,因?yàn)閿?shù)據(jù)庫(kù)的結(jié)構(gòu)設(shè)計(jì)問(wèn)題(也可能是前幾代同學(xué)在合作的時(shí)候沒(méi)有溝通好),導(dǎo)致出現(xiàn)了任務(wù)1、2、3存儲(chǔ)在表1,任務(wù)4存儲(chǔ)在表2,任務(wù)5存儲(chǔ)在表3的情況(如下圖),并且因?yàn)?SQLite 不支持字段刪除,很多表的字段數(shù)多達(dá) 20 多個(gè)。
二. 數(shù)據(jù)庫(kù)操作框架 bug
我們項(xiàng)目中之前一直使用的是 OrmLite 框架進(jìn)行數(shù)據(jù)庫(kù)操作,這個(gè)框架提供了很好的數(shù)據(jù)庫(kù)管理模式,帶來(lái)了很大的方便,但是卻存在一個(gè)致命問(wèn)題就是數(shù)據(jù)庫(kù)并行操作會(huì)導(dǎo)致 Crash(好像作者已經(jīng)不維護(hù)這個(gè)框架了...),雖然并發(fā)在客戶端上并不是很高頻,但是在我們的項(xiàng)目中確實(shí)存在很多這樣的情況,如用戶邊后臺(tái)上傳任務(wù)邊編輯任務(wù)等。
三. 數(shù)據(jù)庫(kù)操作層代碼混亂
原數(shù)據(jù)庫(kù)模塊在 OrmLite 框架的基礎(chǔ)上,針對(duì)每張表封裝了 Table 類進(jìn)行數(shù)據(jù)庫(kù)操作,再在 Table 層下封裝了 Service 層對(duì)外暴露靜態(tài)接口方便調(diào)用,雖然這個(gè)設(shè)計(jì)看起來(lái)很好,也有分層的概念,但是還是存在幾個(gè)問(wèn)題:
- 因?yàn)楸?1 中存儲(chǔ)了多個(gè)任務(wù),導(dǎo)致表 1 對(duì)應(yīng)的 Table 類代碼爆炸,達(dá)到了幾千行;
- Service 層僅作為方便調(diào)用的中轉(zhuǎn),“工作量” 不夠,且徒增代碼量;
- Table 層每個(gè)接口做的事情不夠 “單一”,摻雜了業(yè)務(wù)邏輯在里面,眾多接口不方便復(fù)用。
重構(gòu)方案
一. 重新設(shè)計(jì)模塊結(jié)構(gòu)
通過(guò)對(duì)業(yè)務(wù)的分析,每個(gè)數(shù)據(jù)庫(kù)操作實(shí)際都是對(duì)應(yīng)每一種 “任務(wù)” 的查詢操作,所以每種 "任務(wù)" 是我們需要直接面向的對(duì)象,而不應(yīng)該是真實(shí)的數(shù)據(jù)庫(kù)表,并且真實(shí)的數(shù)據(jù)庫(kù)表可能包含多種任務(wù)類型,如果直接對(duì)表進(jìn)行操作,就會(huì)出現(xiàn)一張表對(duì)應(yīng)的 Table 類里出現(xiàn)多個(gè)任務(wù)的業(yè)務(wù)邏輯代碼,于是基于這個(gè)思路重新設(shè)計(jì)模塊結(jié)構(gòu),如下圖:
基本的設(shè)計(jì)思路是:
既然數(shù)據(jù)表沒(méi)有很好的對(duì)任務(wù)進(jìn)行劃分,那我們就抽象一個(gè)虛擬的任務(wù)表層出來(lái),每個(gè)任務(wù)表只負(fù)責(zé)自己對(duì)應(yīng)任務(wù)的數(shù)據(jù)庫(kù)操作,并把真實(shí)的數(shù)據(jù)表層 Abstract 化,只對(duì)外暴露我們?nèi)蝿?wù)表,這樣我們之后就只需要關(guān)心對(duì)應(yīng)任務(wù)的操作了,每個(gè)任務(wù)的 Table 類也不會(huì)代碼量膨脹,便于維護(hù)。可能會(huì)有同學(xué)疑問(wèn)為什么不直接對(duì)數(shù)據(jù)庫(kù)結(jié)構(gòu)進(jìn)行調(diào)整,主要是因?yàn)榇鷥r(jià)太大,需要用戶強(qiáng)制升級(jí),并大量遷移數(shù)據(jù),當(dāng)然這一步在后續(xù)合適的時(shí)機(jī)也一定是回去做的。
對(duì)于虛擬的任務(wù) Table 類的每個(gè)接口,我們只提供最基本的增、刪、改、查操作,事務(wù)操作轉(zhuǎn)由 Helper 層提供, 這樣便于 Helper 層針對(duì)不同的業(yè)務(wù)邏輯來(lái)組合這些接口,達(dá)到代碼復(fù)用,并且因?yàn)閱螚l查詢粒度較小,對(duì)于數(shù)據(jù)庫(kù)并發(fā)框架提供的讀寫(xiě)鎖來(lái)說(shuō),可以提升很大的效率。
Helper 層則通過(guò) AbstractHelper 基類向下層提供的數(shù)據(jù)庫(kù)并發(fā)處理框架和虛擬任務(wù)表單例,針對(duì)不同的業(yè)務(wù)提供接口,對(duì)于需要“跨表”(這里也包括跨虛擬任務(wù)表,雖然它們實(shí)際在一張真實(shí)表里)的操作,提供 UnionQueryHelper 工具類來(lái)組合各 TaskHelper 提供查詢,這樣做到模塊間代碼界限清晰,不至于隨著后面的不斷維護(hù),各個(gè) Helper 層之間代碼界限又變的混亂不堪。
整體來(lái)說(shuō),就是抽象出一個(gè)虛擬表層來(lái)表示每種任務(wù)表,徹底將混亂不堪的 SQLite 的真實(shí)表層徹底屏蔽掉。
二. 解決數(shù)據(jù)庫(kù)框架的 bug
值得慶幸的是老的數(shù)據(jù)庫(kù)框架采用了分層的思想,不管分層做的好不好,但是對(duì)在日常需求開(kāi)發(fā)中穿插的代碼重構(gòu)來(lái)說(shuō),確實(shí)節(jié)省了很多的時(shí)間, 在解決并發(fā)問(wèn)題的時(shí)候我們就是在原 Table 層上添加一層并發(fā)處理層,很快的解決了這個(gè)問(wèn)題并融合到將要發(fā)版的版本進(jìn)行正常發(fā)版。
之前的同學(xué)就是在這個(gè)基礎(chǔ)上提供了一個(gè)解決并發(fā)問(wèn)題的小框架,借鑒了一些成熟框架的思想,采用了任務(wù)隊(duì)列的方式,雖然解決了并發(fā)問(wèn)題,但是并不十分適合我們的實(shí)際場(chǎng)景,也引入了一些其他的bug,比如對(duì)于用戶大量的任務(wù)數(shù)據(jù),當(dāng)我們進(jìn)行全量讀取的時(shí)候,會(huì)消耗大量的時(shí)間,此時(shí)其他的任何數(shù)據(jù)庫(kù)操作都需要等待,效率比較低。
針對(duì)客戶端并發(fā)量低、讀操作多、操作耗時(shí)基本較端的特性,我認(rèn)為依靠讀寫(xiě)鎖進(jìn)行數(shù)據(jù)庫(kù)并發(fā)限制就可以很好的解決問(wèn)題,并且執(zhí)行效率較高,也便于維護(hù),并發(fā)操作限制中轉(zhuǎn)層代碼如下:
/**
* 提供給所有 Helper 子類使用的數(shù)據(jù)庫(kù)表操作加鎖工具類,不對(duì)外暴露
*/
protected object Executor {
const val INSERT = 0L
const val DELETE = 1L
const val UPDATE = 2L
const val QUERY = 3L
private const val TRANSACTION = 4L
private val mLock = ReentrantReadWriteLock()
@IntDef(INSERT, DELETE, UPDATE, QUERY, TRANSACTION)
@Retention(AnnotationRetention.SOURCE)
annotation class ActionType
class TransactionResult<out T_DATA>(val success: Boolean, val data: T_DATA)
fun transaction(action: () -> Unit, catcher: ((Exception) -> Unit)? = null): Boolean {
var success = true
val lock = getAdaptiveLock(TRANSACTION)
lock.lock()
try {
LocalDatabase.instance.inTransaction { action() }
} catch (e: Exception) {
handleException(e, catcher)
success = false
} finally {
lock.unlock()
}
return success
}
fun <T_RESULT> transactionResult(action: () -> T_RESULT, defaultVal: T_RESULT): TransactionResult<T_RESULT> {
return transactionResult(action, null, defaultVal)
}
fun <T_RESULT> transactionResult(action: () -> T_RESULT,
catcher: ((Exception) -> Unit)? = null, defaultVal: T_RESULT): TransactionResult<T_RESULT> {
var success = true
val resultHolder = arrayOfNulls<Any>(1).apply { this@apply[0] = defaultVal }
val lock = getAdaptiveLock(TRANSACTION)
lock.lock()
try {
LocalDatabase.instance.inTransaction { resultHolder[0] = action() }
} catch (e: Exception) {
handleException(e, catcher)
success = false
} finally {
lock.unlock()
}
@Suppress("UNCHECKED_CAST")
return TransactionResult(success, resultHolder[0] as T_RESULT)
}
fun execute(@ActionType actionType: Long, action: () -> Unit,
catcher: ((Exception) -> Unit)? = null) {
val lock = getAdaptiveLock(actionType)
lock.lock()
try {
action()
} catch (e: Exception) {
handleException(e, catcher)
} finally {
lock.unlock()
}
}
fun <T_RESULT> executeResult(@ActionType actionType: Long,
action: () -> T_RESULT, defaultVal: T_RESULT): T_RESULT {
return executeResult(actionType, action, null, defaultVal)
}
fun <T_RESULT> executeResult(@ActionType actionType: Long,
action: () -> T_RESULT,
catcher: ((Exception) -> Unit)?, defaultVal: T_RESULT): T_RESULT {
var result = defaultVal
val lock = getAdaptiveLock(actionType)
lock.lock()
try {
result = action()
} catch (e: Exception) {
handleException(e, catcher)
} finally {
lock.unlock()
}
return result
}
private fun getAdaptiveLock(@ActionType actionType: Long): Lock =
if (actionType == QUERY) mLock.readLock() else mLock.writeLock()
private fun handleException(ex: Exception, catcher: ((Exception) -> Unit)?) {
if (BuildConfig.DEBUG) {
throw ex
}
CrabSDK.uploadException(ex)
catcher?.invoke(ex)
}
}
使用過(guò)程可以參考如下:
fun getAllData() =
Executor.executeResult(Executor.QUERY, Table.Task1Table::getAllData, safeEmptyList())
不用調(diào)整原先工程中對(duì) Service 層和 Table 層的調(diào)用,就可以將并發(fā)控制層插入,使用也很方便,安全性也都有保證。
總結(jié)
對(duì)于這些不是很好的祖?zhèn)鞔a,如果有機(jī)會(huì)我認(rèn)為一定是要改一改,一是對(duì)自己的提升,二也是降低日后維護(hù)的成本,便于開(kāi)發(fā),雖然可能帶來(lái)一些問(wèn)題,但是問(wèn)題總是可以解決的。即使不能重構(gòu),我覺(jué)得也可以在其上層封裝一層,徹底屏蔽它的實(shí)現(xiàn),日后的維護(hù)過(guò)程中就可以再也不用關(guān)心它了,并且有機(jī)會(huì)的時(shí)候也可以利用這層中轉(zhuǎn)的便利性直接把它替換掉。