大家應該有發現我們一天一天往進階的內容邁進,很快我們就要開始談高大上的架構問題了。
但是在開始討論如何建構一個好的 Android app 架構之前,想先跟大家分享一下 Uncle Bob 的 SOLID Principle。
SOLID 是以下五個 Principle 縮寫合起來的名稱,可能是命中注定也可能是為了好記硬掰出來無從得知,但這五個 Principle 的確都是蠻值得一談的:
就像學武功會有內功跟招式二條技能樹,筆者認為寫程式本身也是有所謂內功(理論)跟招式(技術)的區別的,二者有時密不可分,有時有因果關係,但絕對不該像笑傲江湖裡華山派的氣宗跟劍宗那麼水火不容。
雖然我們在這系列文章前半段多半講的是招式,如何用 XXX 技術解決問題等,但這些招式(技術)也往往都是來自於某些內功(理論),比如說我們一直提到的 Hollywood Principle 、Annotation Processing 很多流程設計跟 Android 也很像等,所以學習技術的時候如果可以同時學習它背後所蘊含的理論,絕對會讓你很快就當上武林盟主的喔。
讓我們一起來看看什麼是 SOLID Principle 吧~
單一職責原則,一個 class 應該只負責一個職責,好處是這樣的 class 會越單純、越好測試,當一個 class 負責太多不同面向的邏輯時,可以試著拆分成不同的 class。
有趣的是一個職責要怎麼拿捏就是每個工程師的本事了,但基本上越大型的軟體需要越多層的分工,這樣才會比較好維護。
比如說如果我們在 Activity
裡面直接使用 Retrofit 來做 api 連線,並修改 layout,雖然第一次寫很輕鬆,但之後 Activity
就會變得很臃腫肥大難以維護與測試。
開放封閉原則,class 必須對擴展(繼承)開放,對修改封閉。
假設我們有以下的程式碼:
class Dog {
fun eat() {
println("dog eat")
}
fun walk() {
println("dog walk")
}
}
有一天我們想建立一種會飛的 Dog
,所以我們第一直覺就想回頭去修改 Dog
,但這樣有什麼缺點呢?
Dog
都無意識的得到會飛的本領。Dog
必須重新測試能正常運作。真正比較安全的做法是另外建立一個 FlyDog
繼承原本的 Dog
,這樣就不會影響到原本正常運作的 Dog
。
class FlyDog : Dog {
fun fly() {
println("dog fly")
}
}
val dog = Dog()
dog.fly() // error
里氏替換原則,這是個比較難理解但卻通常不太容易被打破的原則,如果說 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 ,另一方面不會飛的鳥還是鳥嗎?是不是該改一下繼承關係,怎麼做比較好通常不會有固定的答案,就留給大家好好思考囉。
接口分離原則,class 實作 interface 時不應該依賴於它不需要的 function,當有這種情形有可能是 interface 太過於龐大需要在拆分。
一樣舉我們的 Bird
跟 Chicken
為例子:
interface Bird {
fun eat()
fun fly()
}
class Chicken : Bird {
override fun eat() {
println("chicken eat")
}
override fun fly() {
// Do nothing
}
}
如果 Bird
是 interface 而且提供二個 function ,eat
跟 fly
,而 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
,這樣是不是更清楚了呢。
依賴反轉原則,高階物件不應該依賴低階物件的實作,二者都應該依賴於介面(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
}
}
這個檔案已經寫的相當不錯了, ink
跟 paper
都有獨立成 interface,但缺點是 MyPrinter
只能固定只用 DoubleAPaper
跟 EpsonInk
,等於我們依賴了這二個具體的 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