iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
0
Mobile Development

Android 十全大補系列 第 18

[Android 十全大補] SOLID Principle

大家應該有發現我們一天一天往進階的內容邁進,很快我們就要開始談高大上的架構問題了。
但是在開始討論如何建構一個好的 Android app 架構之前,想先跟大家分享一下 Uncle Bob 的 SOLID Principle。

SOLID Principle

SOLID 是以下五個 Principle 縮寫合起來的名稱,可能是命中注定也可能是為了好記硬掰出來無從得知,但這五個 Principle 的確都是蠻值得一談的:

  1. Single Responsibility Principle
  2. Open/closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

就像學武功會有內功跟招式二條技能樹,筆者認為寫程式本身也是有所謂內功(理論)跟招式(技術)的區別的,二者有時密不可分,有時有因果關係,但絕對不該像笑傲江湖裡華山派的氣宗跟劍宗那麼水火不容。

雖然我們在這系列文章前半段多半講的是招式,如何用 XXX 技術解決問題等,但這些招式(技術)也往往都是來自於某些內功(理論),比如說我們一直提到的 Hollywood Principle 、Annotation Processing 很多流程設計跟 Android 也很像等,所以學習技術的時候如果可以同時學習它背後所蘊含的理論,絕對會讓你很快就當上武林盟主的喔。

讓我們一起來看看什麼是 SOLID Principle 吧~

Single Responsibility Principle

單一職責原則,一個 class 應該只負責一個職責,好處是這樣的 class 會越單純、越好測試,當一個 class 負責太多不同面向的邏輯時,可以試著拆分成不同的 class。
有趣的是一個職責要怎麼拿捏就是每個工程師的本事了,但基本上越大型的軟體需要越多層的分工,這樣才會比較好維護。

比如說如果我們在 Activity 裡面直接使用 Retrofit 來做 api 連線,並修改 layout,雖然第一次寫很輕鬆,但之後 Activity 就會變得很臃腫肥大難以維護與測試。

Open/closed Principle

開放封閉原則,class 必須對擴展(繼承)開放,對修改封閉。
假設我們有以下的程式碼:

class Dog {

    fun eat() {
        println("dog eat")
    }

    fun walk() {
        println("dog walk")
    }
}

有一天我們想建立一種會飛的 Dog,所以我們第一直覺就想回頭去修改 Dog,但這樣有什麼缺點呢?

  1. 所有的 Dog 都無意識的得到會飛的本領。
  2. 所有的 Dog 必須重新測試能正常運作。

真正比較安全的做法是另外建立一個 FlyDog 繼承原本的 Dog,這樣就不會影響到原本正常運作的 Dog

class FlyDog : Dog {
    fun fly() {
        println("dog fly")
    }
}

val dog = Dog()
dog.fly() // error

Liskov Substitution Principle

里氏替換原則,這是個比較難理解但卻通常不太容易被打破的原則,如果說 a 跟 b 二個 class 都是繼承自同個 class,那我們應該可以直接替換二者而不應該影響其他原有的行為。

如果說我們有 Bird 這個 class ,我們預期所有的鳥都會飛所以加上一個 fly 的 function,而有一天我們新增了一個 Chicken 繼承自 Bird,但因為它不會飛所以我們在 function 內拋出了一個例外,如下:

class Bird {
    fun fly()
}

class Chicken : Bird {
    override fun fly() {
        throw RuntimeException()
    }
}

這樣會有什麼風險呢?

假設我們在某處有這樣的程式碼:

fun makeBirdFly(bird: Bird) {
    bird.fly()
}

如果今天傳入的參數變成了 Chicken ,那程式就會有問題。
面對這種設計問題我們就必須回頭看 Bird 是不是真的要有 fly 這個 function ,另一方面不會飛的鳥還是鳥嗎?是不是該改一下繼承關係,怎麼做比較好通常不會有固定的答案,就留給大家好好思考囉。

Interface Segregation Principle

接口分離原則,class 實作 interface 時不應該依賴於它不需要的 function,當有這種情形有可能是 interface 太過於龐大需要在拆分。

一樣舉我們的 BirdChicken 為例子:


interface Bird {
    fun eat()
    fun fly()
}

class Chicken : Bird {
    override fun eat() {
        println("chicken eat")
    }

    override fun fly() {
        // Do nothing
    }
}

如果 Bird 是 interface 而且提供二個 function ,eatfly,而 Chicken 並不需要依賴 fly 這個 function ,所以我們應該回頭改我們的 interface 如下:

interface Bird {
    fun eat()
}

interface Flyable {
    fun fly()
}

interface FlyableBird : Bird, Flyable {

}

class Chicken : Bird {
    override fun eat() {
        println("chicken eat")
    }
}

我們多了一個 Flyable 的 interface ,所以 Bird 可以分為二種,FlyableBird 是會飛的鳥,而 Chicken 只是一般基本形態的 Chicken,這樣是不是更清楚了呢。

Dependency Inversion Principle

依賴反轉原則,高階物件不應該依賴低階物件的實作,二者都應該依賴於介面(interface)。
有沒有覺得似曾相似呢?

首先先定義高/低階物件是什麼,低階物件相比高階物件會依賴於較少的其他物件,而高階物件就是依賴較多物件的一種存在。

舉個印表機跟墨水的例子:

interface Ink {

}

interface Paper {
    fun addText(text: String, ink: Ink)
}

class DoubleAPaper : Paper {
    //......
}

class EpsonInk : Ink {

}

class MyPrinter {

    fun print(text: String): DoubleAPaper {
        val ink = EpsonInk()
        val paper = DoubleAPaper()
        paper.addText(text, ink)
        return paper
    }
}

這個檔案已經寫的相當不錯了, inkpaper 都有獨立成 interface,但缺點是 MyPrinter 只能固定只用 DoubleAPaperEpsonInk ,等於我們依賴了這二個具體的 class 本身,將來很難置換修改,應該怎麼改呢,可以把 dependency 改由外部設定,而且保持依賴 interface 的機制如下:

class MyPrinter(val ink: Ink, val paper: Paper) {

    fun print(text: String): Paper {
        paper.addText(text, ink)
        return paper
    }
}

val printer = MyPrinter(EpsonInk(), DoubleAPaper())

這樣是不是變得乾淨許多呢?

以上就是今天的內容了,咱們明天華山論劍不見不散!

Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786


上一篇
[Android 十全大補] RxJava Scheduler
下一篇
[Android 十全大補] Clean Architecture
系列文
Android 十全大補30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言