如何將第三方庫(kù)與業(yè)務(wù)代碼解耦

日常開(kāi)發(fā)中我們經(jīng)常會(huì)用到各種第三方庫(kù),而如何使用別人的代碼其實(shí)也有一點(diǎn)講究蛇摸。如果直接在業(yè)務(wù)代碼中使用第三方庫(kù),導(dǎo)致項(xiàng)目對(duì)某個(gè)第三方庫(kù)的依賴(lài)過(guò)重,那一旦因?yàn)楦鞣N原因需要更換方案的時(shí)候骨宠,所需要修改的代碼量之大可能還不如直接重寫(xiě)了浮定。

所以關(guān)鍵就在于如何將第三方庫(kù)與業(yè)務(wù)代碼進(jìn)行解耦,常見(jiàn)的思路就是對(duì)第三方庫(kù)進(jìn)行二度封裝层亿。而具體怎么封裝桦卒,可以有很多方案,比較簡(jiǎn)單粗暴的是使用繼承匿又。譬如我們?cè)陧?xiàng)目中集成了 AFNetworking方灾,但我們不直接使用AFHTTPRequestOperationManager來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,而是新建一個(gè)MyHTTPRequestOperationManager繼承自AFHTTPRequestOperationManager碌更,聲明一系列方法供業(yè)務(wù)方調(diào)用裕偿,這些方法可以執(zhí)行一些額外的操作,然后將請(qǐng)求消息轉(zhuǎn)發(fā)給父類(lèi)痛单。這樣如果以后不準(zhǔn)備使用 AFNetworking 了嘿棘,只需要改寫(xiě)AFHTTPRequestOperationManager這個(gè)類(lèi),而不用去修改其它地方旭绒。這是可行的鸟妙,但是我們可以有更好的解決方案。

2015年的 WWDC 有一個(gè) topic 是 Protocol-Oriented Programming in Swift挥吵,蘋(píng)果宣稱(chēng) Swift 是一門(mén)面向協(xié)議的語(yǔ)言重父,說(shuō)是這將顛覆人們寫(xiě)代碼的方式。這多少有點(diǎn)夸大其辭忽匈,畢竟所謂的面向協(xié)議編程其實(shí)跟幾十年前就被提出的設(shè)計(jì)原則——面向接口編程而不是面向?qū)崿F(xiàn)編程房午,是有異曲同工之妙的。

那接下來(lái)丹允,我就運(yùn)用面向協(xié)議的思維來(lái)進(jìn)行解耦歪沃。舉個(gè)真實(shí)的案例吧,最近我準(zhǔn)備在項(xiàng)目中集成 Realm 來(lái)做緩存嫌松,于是我先聲明了一個(gè)CacheManagerType

typealias Handler = () -> Void
protocol CacheManagerType {
    associatedtype Cacheable
    
    static var defaultManager: Self { get }
    
    func addItem(item: Cacheable)
    func updateItem(item: Cacheable)
    func deleteItem(item: Cacheable)
    
    func addItems(items: [Cacheable])
    func updateItems(items: [Cacheable])
    func deleteItems(items: [Cacheable])
    func deleteAllItems()
    
    func itemWithId(id: Int) -> Cacheable?
    
    func updateWithHandler(handler: Handler)
}

Swift 不直接支持范型協(xié)議沪曙,不過(guò)可以用associatedtype達(dá)到相同的效果,這邊的Cacheable就是一個(gè)范型萎羔,代表了可以被緩存的類(lèi)型液走。接下來(lái)我定義一個(gè)RealmCacheManager,它遵守CacheManagerType協(xié)議:

private let errorMessage = "Realm exception: write failed."
private let cacheQueueName = "cn.sheepy.CacheManager"
private let cacheQueue = dispatch_queue_create(cacheQueueName, DISPATCH_QUEUE_CONCURRENT)

private let instance = CacheManager()

final class RealmCacheManager: NSObject, CacheManagerType {
    
    static var defaultManager: RealmCacheManager {
        return instance
    }
    
    private var cache: RLMRealm {
        // The default `RLMRealm` instance for the current thread
        return RLMRealm.defaultRealm()
    }
    
    // MARK: - Handle item
    
    func addItem(item: RLMObject) {
        invokeInCacheQueue {
            self.cache.addObject(item)
        }
    }
    
    func updateItem(item: RLMObject) {
        invokeInCacheQueue {
            self.cache.addOrUpdateObject(item)
        }
    }
    
    func deleteItem(item: RLMObject) {
        invokeInCacheQueue {
            self.cache.deleteObject(item)
        }
    }
    
    // MARK: - Handle items
    
    func addItems(items: [RLMObject]) {
        invokeInCacheQueue {
            self.cache.addObjects(items)
        }
    }
    
    func updateItems(items: [RLMObject]) {
        invokeInCacheQueue {
            self.cache.addOrUpdateObjectsFromArray(items)
        }
    }
    
    func deleteItems(items: [RLMObject]) {
        invokeInCacheQueue {
            self.cache.deleteObjects(items)
        }
    }
    
    func deleteAllItems() {
        invokeInCacheQueue {
            self.cache.deleteAllObjects()
        }
    }
    
    // MARK: - Handle with closure
    
    func updateWithHandler(handler: Handler) {
        invokeInCacheQueue {
            handler()
        }
    }
    
    // MARK: - Query
    
    func itemWithId<T: RLMObject>(id: Int) -> T? {
        if let result = T.objectsWhere("id == \(id)").firstObject() as? T {
            return result
        } else {
            return nil
        }
    }
    
    // MARK: - Private methods
    
    private func invokeInCacheQueue(handler: Handler) {
        dispatch_async(cacheQueue) {
            self.invoke(handler)
        }
    }
    
    private func invoke(handler: Handler) {
        do {
            try cache.transactionWithBlock {
                handler()
            }
        } catch {
            printLog(errorMessage)
        }
    }
}

這個(gè)類(lèi)是CacheManagerType的 Realm 版本的實(shí)現(xiàn)贾陷,我們可以定義生成方法:

fun genericCacheManager<T: CacheManagerType>() -> T {
    return T.defaultManager
}

func cacheManager() -> RealmCacheManager {
    return genericCacheManager()
}

在業(yè)務(wù)方需要進(jìn)行緩存操作的時(shí)候缘眶,只需要調(diào)用生成方法拿到一個(gè)defaultManager單例即可使用:

let cacheManager = cacheManager()

如果要以它作為方法參數(shù),那該方法應(yīng)該聲明為范型方法:

func doSometihingWithCacheManager<T: CacheManagerType>(cacheManager: T)

這樣髓废,一旦以后需要更換緩存方案巷懈,譬如還是準(zhǔn)備使用 CoreData,那只需要?jiǎng)?chuàng)建一個(gè)CoreDataCacheManager慌洪,讓它遵守CacheManagerType顶燕,然后把cacheManager方法的返回類(lèi)型改為CoreDataCacheManager即可:

func cacheManager() -> CoreDataCacheManager {
    return genericCacheManager()
}

這樣原先業(yè)務(wù)代碼中得到的cacheManager就自動(dòng)變成CoreDataCacheManager的一個(gè)單例了凑保。用了一段時(shí)間如果覺(jué)得還是 Realm 好用,依舊只要把cacheManager方法的返回類(lèi)型改為RealmCacheManager就可以了涌攻,非常方便欧引。

以上方案還有一個(gè)問(wèn)題,就是RealmCacheManager的一系列方法的參數(shù)都使用了RLMObject恳谎,這是Realm中 Model 的基類(lèi)芝此,業(yè)務(wù)代碼中需要生成具體RLMObject對(duì)象才能使用這些緩存方法,這也是一種緊耦合因痛。一旦移除了 Realm婚苹,還是需要修改許多業(yè)務(wù)代碼。更合適的方法是聲明一個(gè)Cacheable協(xié)議:

protocol Cacheable {
    var entity: RLMObject { get }
}

然后讓一個(gè)原先就在項(xiàng)目中用來(lái)表示 Model 的類(lèi)型來(lái)遵守這個(gè)協(xié)議鸵膏,譬如 JOSN租副、Dictionary、MTLModel 等等都行较性。譬如這樣:

extension XXXModel: Cacheable {
    var entity: RLMObject {
        return RLMObject(object: self)
    }
}

具體的轉(zhuǎn)換邏輯可以用 extension 加到 RLMObject 中去用僧,這樣就把轉(zhuǎn)換邏輯和 RLMObject 對(duì)象從業(yè)務(wù)代碼中剝離出來(lái)。而且CacheManagerType中的associatedtypedefaultManager也可以去掉了赞咙,CacheManagerType可以作為類(lèi)型使用了(Swift 中使用了associatedtypeSelf的協(xié)議不能作為類(lèi)型责循,只能作為范型約束)。相應(yīng)的在RealmCacheManager中相關(guān)的方法要進(jìn)行一點(diǎn)修改攀操,以addItem為例:

func addItem(item: Cacheable) {
    invokeInCacheQueue {
        self.cache.addObject(item.entity)
    }
}

生成方法只要這樣就行了:

func cacheManager() -> CacheManagerType {
    return RealmCacheManager.defaultManager
}

以后要換緩存方案的時(shí)候院仿,只需要修改Cacheableentity的返回類(lèi)型(譬如 CoreData 中的NSManagedObject),然后相應(yīng)地添加轉(zhuǎn)換邏輯就行了速和。cacheManager實(shí)例的替換跟之前差不多歹垫,把cacheManager中的RealmCacheManager換成CoreDataCacheManager就行了。

說(shuō)了這么多颠放,其實(shí)主要目的就是在工具庫(kù)和業(yè)務(wù)代碼間設(shè)置一層抽象屏障排惨,不讓業(yè)務(wù)代碼對(duì)某個(gè)類(lèi)庫(kù)依賴(lài)過(guò)重(當(dāng)然,如果是確定會(huì)一直使用碰凶,中途不會(huì)有任何變更的第三方庫(kù)暮芭,那大可不必如此)。具體的做法不必拘泥于我文中的思路欲低,大家盡可各展神通辕宏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市砾莱,隨后出現(xiàn)的幾起案子瑞筐,更是在濱河造成了極大的恐慌,老刑警劉巖腊瑟,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聚假,死亡現(xiàn)場(chǎng)離奇詭異块蚌,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)魔策,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)河胎,“玉大人闯袒,你說(shuō)我怎么就攤上這事∮卧溃” “怎么了政敢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)胚迫。 經(jīng)常有香客問(wèn)我喷户,道長(zhǎng),這世上最難降的妖魔是什么访锻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任褪尝,我火速辦了婚禮,結(jié)果婚禮上期犬,老公的妹妹穿的比我還像新娘河哑。我一直安慰自己,他們只是感情好龟虎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布璃谨。 她就那樣靜靜地躺著,像睡著了一般鲤妥。 火紅的嫁衣襯著肌膚如雪佳吞。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,021評(píng)論 1 291
  • 那天棉安,我揣著相機(jī)與錄音底扳,去河邊找鬼。 笑死贡耽,一個(gè)胖子當(dāng)著我的面吹牛花盐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菇爪,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼算芯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了凳宙?” 一聲冷哼從身側(cè)響起熙揍,我...
    開(kāi)封第一講書(shū)人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氏涩,沒(méi)想到半個(gè)月后届囚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體有梆,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年意系,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泥耀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛔添,死狀恐怖痰催,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迎瞧,我是刑警寧澤夸溶,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站凶硅,受9級(jí)特大地震影響缝裁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜足绅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一捷绑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氢妈,春花似錦胎食、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蕾总,卻和暖如春粥航,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背生百。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工递雀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蚀浆。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓缀程,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親市俊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杨凑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容