Android進階寶典 -- 深究23種設計模式(上)

語言: CN / TW / HK

談到設計模式,我們很多夥伴可能都瞭解一二,看到原始碼中的一些設計模式後,恍然大悟原來程式碼可以這麼寫,但真正自己下手的時候,反而會把這些理念拋在了腦後。所以如果把熟練使用設計模式分個等級(最高10級),只是看懂了設計模式,只能是1,而能用某些設計模式優化程式碼結構,就算是3級,當看到某個業務邏輯就能知道使用什麼樣的設計模式,就能達到8級甚至以上。

所以想要真正的熟悉設計模式,當然也要從看開始,我自己一開始寫程式碼的時候,也是眉毛鬍子一把抓,很隨意,但是自己慢慢有了”程式碼潔癖“之後,就會注意這些問題,作為博主自己肯定也不是到了熟練使用設計模式這個階段,但是還是想把一些自己的思想分享給夥伴們。

1 老生常談 - 7大設計原則

這部分可能比較枯燥,夥伴們也都熟記這些原則,可真正寫程式碼的時候,貌似並沒有完全遵守這些原則,包括我自己在內,那麼這些原則到底怎麼樣才算是沒有違背呢?

7大設計原則:SOLID,分別為單一職責原則、開閉原則、里氏替換原則、依賴倒置原則、介面隔離原則、迪米特原則、合成複用原則。

1.1 單一職責

其實從名字上我們就已經知道,當我們封裝一個類的時候,它遵循的是面向物件,因此這個類應該只做自己該乾的事,例如一個User類,它可以提供使用者資訊,但是讓它提供今天是幾月幾號的能力,就不合理了。正常一個類不能超過200行程式碼,可見夥伴們動輒一個類上千行,那麼就有可能存在冗餘其他職責的嫌疑了。

1.2 開閉原則

這個原則應該是6大設計原則中最終的一個原則。開閉原則即為面向修改關閉,面向擴充套件開發,看下面這個例子 ```kotlin class SDKImpl {

fun init(){
    // TODO impl
}

fun send(){
    // TODO impl
}

} ``` 當我們接到一個新的需求時,例如要使用A SDK來完成,其中有兩個方法,我們完成了具體的實現;後續的時候,發現A SDK有問題,想要換成B SDK,這個時候如果要去修改SDKImpl類中的方法為了滿足新SDK的功能,此時就違背了開閉原則

```kotlin interface ISDK {

fun init(){

}

fun send(){

}

} 這個時候,就需要介面或者抽象類來提供一個穩定的抽象層,當有新的需求來的時候,只需要派生出一個新的物件就能夠避免對原有邏輯的修改,這樣就完成了面向抽象是開放的原則。kotlin class ASDKImpl : ISDK{ override fun init() { super.init() }

override fun send() {
    super.send()
}

} kotlin class BSDKImpl : ISDK{ override fun init() { super.init() }

override fun send() {
    super.send()
}

} ``` 像這樣派生多個子類,一般情況下都會有一個靜態代理類負責管理這些子類的切換,之後的設計模式中會提到。

1.3 里氏替換原則

對於里氏替換原則,夥伴們熟悉嗎?但是在專案中,我想夥伴們都見到過,但是可能還沒有認知,這個原則是幹什麼的?夥伴們可以這麼理解:子類可以擴充套件父類中的方法,但是不能改變父類中原有的邏輯。 ```kotlin open class Car {

var mSpeed: Int = 0
fun setSpeed(speed: Int) {
    this.mSpeed = speed
}

fun getRunTime(duration: Long): Long {
    return duration / mSpeed
}

} 有一個簡單的Car類,能夠設定速度並計算出執行的時長; class AudiCar : Car() { override fun setSpeed(speed: Int){ mSpeed = 0 } } ``` 如果有一個子類實現了Car,並重寫了父類的方法,此時將mSpeed設定為0,其實父類原始邏輯中,會把mSpeed賦值,而子類中則是永遠為0,就會導致呼叫父類getRunTime方法報錯,所以就違背了里氏替換原則

1.4 依賴倒置原則

依賴倒置原則,其實跟前面1.2節中的開閉原則類似,其實就是面向介面程式設計。當我們在設計一個框架的時候,我們的分層設計需要遵循的一個原則就是:高層的模組最好不要直接依賴低層的直接實現,而是通過操作介面完成低層實現的呼叫。

這個是什麼意思呢?還是拿1.2中的例子來說,假如我們在當前版本要使用A SDK,我們的框架設計如下,有一個Manager類: ```kotlin object SdkManager {

fun init(sdk:ASDKImpl){
    sdk.init()
}

} ``` 我們看是直接呼叫了A SDK實現類的init方法,當下個版本的時候,要換成B SDK,那麼就需要修改init方法,這樣就違背了開閉原則。

當然也可能這麼想,就是我加一個方法不行嗎?

```kotlin object SdkManager {

fun init(sdk:ASDKImpl){
    sdk.init()
}

fun initBSdk(sdk:BSDKImpl){
    sdk.init()
}

} ``` 當然也沒有問題,但是我們需要想到一點就是,如果SDK的種類很多,豈不是要加很多方法,所以回到我們前面說到的,高層的模組不要直接操作直接子類,而是需要通過介面來排程。

```kotlin object SdkManager {

fun init(sdk:ISDK){
    sdk.init()
}

} 這樣整個實現就非常靈活了,在使用某個SDK時,只需要傳入對應的具體實現即可。kotlin SdkManager.init(ASDKImpl()) ```

1.5 介面隔離原則

還是對接1.4的話題,介面隔離原則就是在設計介面的時候,儘量保證介面的單一性。什麼意思呢?並不說每個介面只能寫一個方法,那這樣下去整個專案的介面就爆炸了,而是說一個介面也要保證職責單一,這樣才能保證派生類的職責單一,其實和1.1是遙相呼應的。

1.6 迪米特原則

這個設計原則,夥伴們貌似挺多,但是瞭解的可能不多,那我說一個場景可能夥伴們就恍然大悟。

image.png

有三個模組,其中A和B,B和C是有直接的依賴關係,但是A和C之間沒有直接的關聯關係,如果模組A想要呼叫模組C中的某個方法,迪米特原則就是捨近求遠,A可以通過B,在由B通過呼叫C中的方法,那麼模組A和模組C就相對隔離,這樣的好處在於:減少模組間的耦合,以及相互依賴。

1.7 合成複用原則

合成複用原則:在能通過組合的方式達成關聯的情況下,儘量避免使用繼承來實現。例如前面我們提到的Car類,它可以認為是一個抽象類,如果想要一個具體的Car類,那麼可以通過繼承Car類來實現。

那麼合成複用原則想要我們儘量不要用繼承來實現,為什麼呢?我認為使用繼承沒有問題,但是如果因為實現邏輯導致繼承的鏈條特別長,就不建議使用,如下圖所示。

image.png

那麼不用繼承,組合的方式有哪些呢?如果熟悉裝飾器模式的夥伴,或許能夠理解其中的道理了,舉個例子。

```kotlin class BenzCarWrapper(val car: Car) {

fun setSpeed(speed: Int) {
    car.setSpeed(speed)
}

fun getRunTime(duration: Long): Long {
    return car.getRunTime(duration)
}

fun getName():String{
    return "xxx"
}

} ``` BenzCarWrapper可以認為是對於Car的一次包裝,除了能夠呼叫Car中的方法,還能夠擴充套件其他的方法,這裡其實就沒有通過繼承來實現。

2 進入設計模式世界

通過前面對於設計原則的瞭解,其實都是為了給設計模式做鋪墊,這裡我不會講所有的設計模式,因為在實際的專案中可能根本就用不到,或者使用的頻率很低,這裡主要介紹核心的設計模式,相信會對日常的業務開發有所幫助。

2.1 單例設計模式

不講了,這個我相信是夥伴們用的最多的一種設計模式了。

2.2 工廠模式

工廠模式,我相信也是夥伴們聽的最多的一種設計模式,但是在實際的開發過程中真正去使用工廠設計模式的卻是很少。工廠模式的作用是什麼呢?就是我們前面在介紹開閉原則的時候,提到的面向擴充套件開放,當我們設計介面並建立多個派生類的時候,如何去拿到每個派生類的例項物件,就會使用到工廠設計模式。

```kotlin class SimpleFactory {

enum class SdkType(val index: Int) {
    ASDK(1), BSDK(2)
}

companion object {
    fun getSdk(type: Int): ISDK {
        return when (type) {
            SdkType.ASDK.index -> {
                ASDKImpl()
            }
            SdkType.BSDK.index -> {
                BSDKImpl()
            }
            else -> {
                ASDKImpl()
            }
        }
    }
}

} ``` 首先我們先看下簡單工廠設計模式,它的理念就是通過傳入的引數看命中哪個列舉值或者其他的標識,來判斷要具體生產哪個型別的SDK。

使用這種工廠模式的好處就是:將建立和呼叫分離開。通常我們在建立一個物件的時候,都是直接new出來,那麼這樣會帶來一個問題就是,當這個物件我們不再使用的時候,就需要改原先的邏輯程式碼,將其換成新的類。 ```kotlin // 老版本使用ASDKImpl val sdk1 = ASDKImpl() sdk1.init()

// 新版本使用ASDKImpl的擴充套件版本v2 //val sdk1 = ASDKImpl() val sdk1_v2 = ASDKV2Impl() sdk1.init() 修改原先的程式碼邏輯是大忌,我們無法保證修改一定沒有問題,但是工廠模式的優勢在於,上層的業務邏輯不需要改動,例如下面的呼叫:kotlin val sdk1 = SimpleFactory.getSdk(SimpleFactory.SdkType.ASDK.index) sdk1.init() 當有SDK版本需要改動,只需要修改SimpleFactory中實現,將其換成擴充套件版本即可。kotlin companion object { fun getSdk(type: Int): ISDK { return when (type) { SdkType.ASDK.index -> { // ASDKImpl() ASDKV2Impl() } SdkType.BSDK.index -> { BSDKImpl() } else -> { ASDKImpl() } } } } ```

這種設計適應的場景是派生類的個數少,如果有成百上千的派生類,那麼getSdk這個方法就會變得巨大不易維護,因此很少會用簡單工廠設計模式。

這裡我再簡單介紹下工廠模式的另一個變種:工廠方法模式,其實和簡單工廠不一樣的是,每個產品都有自己對應的一個工廠,互不干擾。 ```kotlin interface IAbsFactory {

fun create():ISDK

} kotlin class ASdkFactory : IAbsFactory { override fun create(): ISDK { return ASDKImpl() } } ```

kotlin class BSdkFactory : IAbsFactory { override fun create(): ISDK { return BSDKImpl() } } 那麼在使用的時候,如果使用ASDK,那麼就使用ASdkFactory建立即可,如果有涉及到ASDK的改動,那麼也不需要上層業務發起修改,只需修改ASdkFactory的實現邏輯即可。

kotlin val sdk1 = ASdkFactory().create() sdk1.init() 工廠方法模式存在的弊端就是當有新的產品出現之後,必須要建立一個新的Factory,因此針對這個問題,出現了第三種工廠設計模式:抽象工廠設計模式。

抽象工廠設計模式,是能夠在一個工廠中生產不同的產品,看下面的示例: ```kotlin interface IAbsFactory {

fun createSdkA():ISDK
fun createSdkB():ISDK

} ``` 我們可以看到,在IAbsFactory介面中,聲明瞭所有產品的建立方法,而且也遵循了介面隔離的原則,只負責生產,而沒有其他額外的邏輯

```kotlin class SdkFactory : IAbsFactory { override fun createSdkA(): ISDK { return ASDKImpl() }

override fun createSdkB(): ISDK {
    return BSDKImpl()
}

} ```

kotlin val sdk1 = SdkFactory().createSdkA() sdk1.init()

其實工廠設計模式的核心還是在於,業務層的呼叫與建立的隔離,提高維護性和擴充套件性,其實有點兒像依賴注入,有熟悉Dagger2和Hilt的夥伴應該有這個感受,還有就是Bitmap的建立,其實也是通過工廠模式來實現的,呼叫不同的方式邏輯,但是真正使用的時候,還是需要看場景,像抽象工廠模式,如果產品非常多,也會造成介面爆炸。

2.3 建造者設計模式

建造者設計模式,用於對外暴露這個類物件建立時,能夠傳入那些引數,從而建立一個物件,常見的就是建立Dialog的時候,傳入一些必要的引數,建立一個Dialog並顯示,這裡就不詳細介紹了,這個同上面幾種設計模式一致,都是建立型的設計模式

2.4 代理模式

代理模式主要分為兩種:靜態代理模式和動態代理模式。

首先我們先看一下靜態代理模式,還是拿1.2中SDK的例子來說,如果我們想要對ASDKImpl的init方法執行前後加上一些邏輯的處理,那麼如果使用靜態代理,就需要SDKProxy代理類持有某個類的具體引用。 ```kotlin object SDKProxy : ISDK {

//代理SDK A
private var sdka: ASDKImpl? = null

fun setSdk(asdkImpl: ASDKImpl) {
    this.sdka = asdkImpl
}

override fun init() {
    if (sdka == null){
        sdka = ASDKImpl()
    }
    //todo 方法執行前的處理
    sdka?.init()
    //todo 方法執行後的處理
}

override fun send() {

}

} 當然這種寫法還有優化,因為SDK的實現類眾多,如果需要持有每個實現類的引用不現實,其實可以**通過面向介面程式設計,遵循依賴倒置原則**,框架層只操作介面,上層可以傳遞物件的實現類。kotlin object SDKProxy : ISDK {

//代理SDK A
private var sdka: ISDK? = null

fun setSdk(sdk: ISDK) {
    this.sdka = sdk
}

override fun init() {
    //todo 方法執行前的處理
    sdka?.init()
    //todo 方法執行後的處理
}

override fun send() {

}

} ```

kotlin SDKProxy.setSdk(ASDKImpl()) SDKProxy.init()

這個在實際的開發中其實也會經常用到,但是靜態代理存在的一個問題就是,如果存在多種代理關係,即存在多個介面,那麼就需要建立多個代理類,有沒有可能只有一個代理類,就能夠實現所有介面的代理,那麼就引出了動態代理的概念。

其實動態代理出現的時候,面向的就是介面,只有通過接口才能完成動態代理, ```kotlin class SDKProxy2 {

fun <T> proxy(t: T): T? {

    return Proxy.newProxyInstance(
        t!!::class.java.classLoader,
        t!!::class.java.interfaces,
        ProxyHandler(t)
    ) as? T
}

private inner class ProxyHandler<T>(val target: T) : InvocationHandler {
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

        //todo 方法呼叫前的處理
        Log.e("TAG", "方法呼叫前的處理")
        val obj = if (args.isNullOrEmpty()) {
            method?.invoke(target)
        } else {
            method?.invoke(target, args)
        }
        //todo 方法呼叫後的處理
        Log.e("TAG", "方法呼叫後的處理")
        return obj
    }

}

} ``` 其實我們很容易就能看到和靜態代理的區別,動態代理我們也可以傳入一個例項物件,但是不需要像靜態代理那樣,還需要處理方法的執行,當通過newProxyInstance建立一個代理物件之後,呼叫其中的方法,例如init方法,那麼就會走到InvocationHandler中的invoke方法中,在這裡也可以做方法執行前的邏輯處理。

kotlin val sdkA = SdkFactory().createSdkA() SDKProxy2().proxy(sdkA)?.init()

使用動態代理,很大程度上也是為了呼叫與實現的隔離,只不過是動態代理的靈活性更強,甚至能夠影響方法執行的邏輯;而且只需要一個代理類就可以完成所有介面的代理。

2.5 橋接模式

橋接模式,其實就是為了遵循合成複用的原則,避免通過靜態的繼承關係造成類與類之間的強耦合關係。

舉個例子: ```kotlin interface ICar {

fun getName(): String

} kotlin class SimpleCar : ICar { override fun getName(): String { return "普通的車" } } 這是一輛普通的車,在此基礎上,我想給它染成紅色,變成紅色的車,這樣就需要繼承(為啥要繼承SimpleCar,實現ICar介面不可以嗎?其實繼承父類目的肯定也是想要使用父類中的一些屬性或者方法)kotlin class RedCar : SimpleCar() {

override fun getName(): String {
    return "紅色的車"
}

} ``` 那麼這樣一來,其實違背了里氏替換原則,相當於把父類的方法完全重寫了;而且還有一個問題就是,如果屬性註解增加,就會一直需要繼承,因此橋接模式出現,就是為了解決這個問題。

因為顏色是一個屬性,而且是一個抽象的屬性,具體有紅色、白色、藍色等等,因此可以把顏色也做一次抽象。 ```kotlin interface IColor {

fun getColorName():String

} kotlin class RedColor : IColor { override fun getColorName(): String { return "紅色" } } 對原先的ICar介面也做一次改造,增加一個設定顏色的方法。kotlin interface ICar {

fun getName(): String
fun setColor(color: IColor)

} 最終的實現如下:kotlin open class SimpleCar : ICar {

private var color: IColor? = null

override fun getName(): String {
    return "普通的${color?.getColorName()}車"
}

override fun setColor(color: IColor) {
    this.color = color
}

} ``` 其實橋接模式還是比較簡單的,整體的思想還是避免過度的繼承,與下面要介紹的裝飾器模式有點兒類似。

2.6 裝飾器模式

裝飾器模式,目的在於不改變當前物件結構的情況下,動態地為該物件增加一些職責,常見的就是InputStream和OutputStream,它們有很多對應裝飾物件,例如FileInputStream、BufferedInputStream等。

我們舉一個經典的例子,珍珠奶茶

kotlin interface IMilkTea { fun create() } kotlin class SimpleMilkTea : IMilkTea { override fun create() { Log.e("TAG","這是一杯原味奶茶") } } 接下來,我們需要一個抽象的裝飾器,用來擴充套件奶茶類的實現。 kotlin abstract class AbsDecorate(val milktea: IMilkTea) : IMilkTea { override fun create() { milktea.create() } } 珍珠奶茶的具體實現類。 ```kotlin class ZhenzhuMT(milktea:IMilkTea) : AbsDecorate(milktea) {

override fun create() {
    super.create()
    Log.e("TAG","這杯加了珍珠")
}

} ```

kotlin val mt = ZhenzhuMT(SimpleMilkTea()) mt.create() 這樣的話,我們就是在原有SimpleMilkTea的基礎上了,新增了ZhenzhuMT的業務,而且並沒有影響到SimpleMilkTea業務的原有邏輯。

2.7 門面設計模式

門面設計模式,又稱為外觀設計模式,也是我們在日常開發中最常用的一個設計模式之一,只不過我們並沒有認知到。當我們在使用第三方的SDK的時候,其實有很多細節的實現,但對於外層的呼叫者來說,它不需要關心內部的實現邏輯,例如網路庫,呼叫者不需要關係它是OkHttp還是Retrofit,一個好的門面甚至都不能讓使用者知道這個是什麼東西,只需要暴露一些介面,使用者發起請求拿到服務端資料即可。

image.png

``` class Facede {

private val asdk:ASDKImpl = ASDKImpl()

fun init(){
    asdk.init()
}

} ``` 詳細的程式碼就不寫了,Facede屬於唯一對外暴露的類,當ASDKImpl中的init發生修改之後,其實上層的呼叫是不受影響的。

2.8 享元模式

享元模式,目的就是為了避免重複地建立物件,像在使用代理模式時,每次建立物件都是通過new出來一個新的物件。 ```kotlin class SdkFactory : IAbsFactory { override fun createSdkA(): ISDK { return ASDKImpl() }

override fun createSdkB(): ISDK {
    return BSDKImpl()
}

} ``` 雖然這也是臨時變數,方法執行完畢之後就會被虛擬機器回收,但是在GC之前,這些臨時物件依然佔用JVM的記憶體,會導致GC提前,因此享元模式就是為了解決這些問題。

```kotlin class SharedSDKFactory {

private val sharedMap: MutableMap<String, ISDK> by lazy {
    mutableMapOf()
}

fun getComponent(key: String): ISDK? {
    if (sharedMap.containsKey(key)) {
        return sharedMap[key]
    } else {
        //建立新的SDK
        val sdk = ASDKImpl()
        sharedMap[key] = sdk
        return sdk
    }
}

} ``` 在SharedSDKFactory中,採用Map將註冊過的實現類儲存起來,每個SDKImpl對應一個Key,在沒有獲取到例項的時候,需要重新建立一個;如果存在,那麼就直接從快取中獲取。

```kotlin class SdkFactory : IAbsFactory { override fun createSdkA(): ISDK? { return SharedSDKFactory.getComponent("A") }

override fun createSdkB(): ISDK {
    return BSDKImpl()
}

} ```

前面這8種設計模式,主要介紹了建立型模式和結構型模式中一些比較經典的設計模式,還有一些可能平時用到的設計模式就沒有在這裡寫,在下篇文章中,我會介紹最後一個大類就是行為型的設計模式,其中像策略設計模式、責任鏈設計模式、觀察者設計模式、迭代器等都是經常會用到的,好了,這篇文章就到這裡,未完待續~