程式碼的維護

重新學習

Posted by willsbor Kang on 2018-12-08

為什麼要這樣寫?

某日下午,和同事討論了一下我發的 PR 內容。

「看程式碼,我是能理解你想要幹嘛,不過『未來維護的人』看得懂嗎?」這樣說道…

「你這樣設計未來可能會發生…的問題」

每種設計方式都有他的問題,還有可能是相同的問題

我的程式理念,慢慢的往依賴能切分乾淨靠近,但是相對於 structured programming 的直覺使用,依賴反轉過後,尋找實際實作者時,往往會出現難以接受的事實。

例如:iOS 中拿取 bundle identifier

基本上你可以透過 類 system function 拿到。以 structured programming 下,開發者會設計一個 Utils 的物件,包裝這個功能,然後對外宣稱,「如果要拿 bundle identifier,問 Utils 就好了。」

恩,合理!一個 system 的功能找一個包裝好 system 的工具 (Utils) 來取得。

但是,如果是用 object-oriented programming 的角度呢?他的主要的描述反而是:「我這個 manager 想要拿到 bundle identifier,只要能提供給我就行。」「Utils 正好能提供,你問他吧!」

同樣的目的,但是不同的解釋

看起來好像沒什麼,但延伸出了什麼問題呢?

變數為什麼都放給同一個物件管理,用 shared instance 分別取用不是很好嗎?

如果不同屬性的變數,會分開成不同的物件去控制。

ex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Utils {
var osVersion: Int
}

class BuyTicketFunction {
static let shared = BuyTicketFunction()
var serivceEnabled: Bool
}

class UIViewController {
func displayBuyTicketView() {
guard Utils.shared.osVersion > 10 else {
return
}

guard BuyTicketFunction.shared.serivceEnabled else {
return
}

...
}
}

如果有一個功能要同時看系統版本,以及後端服務是否有開啟功能,最直接的寫法會長上面這樣。

參數分別給不同的 manager 管理,看起來也很合理!

如果以 UIViewController 的角度去描述呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Utils {
var osVersion: Int
}

class BuyTicketFunction {
static let shared = BuyTicketFunction()
var serivceEnabled: Bool
}

class Controller: ViewControlling {
var buyTicketSerivceEnabled: Bool {
guard Utils.shared.osVersion > 10 else {
return false
}

guard BuyTicketFunction.shared.serivceEnabled else {
return false
}

return true
}
}

protocol ViewControlling {
var buyTicketSerivceEnabled: Bool { get }
}

class UIViewController {
var controller: ViewControlling = Controller()

func displayBuyTicketView() {
guard controller.buyTicketSerivceEnabled else {
return
}

...
}
}

「誰能告訴我 buyTicketSerivceEnabled ?」如果條件參數分別是兩個物件提供,那必定要寫一個中介的物件來實踐這件事情。因此就會多一個 Controller

想像上 UIViewController 應該會有自己的 ViewControlling,且會有一個自己實作的 Controller 物件,這個物件應該就是「商業邏輯」的一部份。

但是當 UIViewController 變多了呢?就可能會產生多個 Controller,每個 Controller 都包含了一部分「商業邏輯」。

但悲傷的是,商業功能通常會在多個頁面都沾染一些。如果每個 Controller 都是各自實現,那就會造成「code repeat yourself」。為了避免這樣的事情,又會把功能抽出來,進化成一個物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Utils {
var osVersion: Int
}

class BuyTicketFunction {
static let shared = BuyTicketFunction()
var serivceEnabled: Bool
}

class ProjectBuyTicketManager {
var buyTicketSerivceEnabledAndOSSupported: Bool {
guard Utils.shared.osVersion > 10 else {
return false
}

guard BuyTicketFunction.shared.serivceEnabled else {
return false
}

return true
}
}

class Controller: ViewControlling {
var buyTicketSerivceEnabled: Bool {
return ProjectBuyTicketManager.shared.buyTicketSerivceEnabledAndOSSupported
}
}

protocol ViewControlling {
var buyTicketSerivceEnabled: Bool { get }
}

class UIViewController {
var controller: ViewControlling = Controller()

func displayBuyTicketView() {
guard controller.buyTicketSerivceEnabled else {
return
}

...
}
}

這裡多了一個 ProjectBuyTicketManager,並且多了一個 func 叫做 buyTicketSerivceEnabledAndOSSupported,因為他比 buyTicketSerivceEnabled 還考慮了 OS。如果名稱沒有區別,那未來的維護者可能會誤會用底層的 BuyTicketFunction 就好了。

增加新的頁面,收納同群的商業邏輯

因此若有多個 UIViewController 跟 Buy Ticket 有關,
-「如果商業邏輯相同」則可以共用 ProjectBuyTicketManager
-「如果略有不同、大部分相同」,則可以整理 ProjectBuyTicketManager 內的邏輯,共用相同的區塊
-「如果商業邏輯不同」… 大哥,這樣應該就是完全不同的程式碼了吧。

這就是 UI 可以先考量自身的使用情境,用單一 protocol 下的 function 的描述,然後再詢問一個商業邏輯的實作,獲得真正的功能。

為了測試、為了維護

商業功能不會永遠不變

變化是在所難免,即使多一個小小的邏輯,也有可能需要調整資料結構。但重點是,調整的當下,你有多少的信心之前的功能還是對的?

理想上可以單單針對 UI 物件作單元測試 (Unit Test),Mock 商業邏輯層,來達到驗證頁面正確。
因此:

  • 單單調整 UIViewController 物件的內容,接口 UIViewControlling 沒有變化,那基本上可以確保原來的功能還是會正確的。
  • 當商業邏輯層 (ProjectBuyTicketManager) 內容有變化時,而最終 UI 上發現有誤時,
    就可以先初步排除 UI 層內容的問題,而可以先從「商業邏輯層」和「商業邏輯實作 UIViewControlling」的這兩部分下手。

這裡只是先描述切分外層依賴的概念,範例裡的「商業邏輯層」用了許多 shared instance,會大大影響程式的可測性。但是一樣可以用 protocol 做相依性的切離,這裡先不贅述了。

測試不能保證不會出現未知的錯誤,但是能強化已知項目的穩健度

PS. 最近設計的 iOS Whoscall SDK 慢慢完成了。這樣的結構、想法,在最後做功能微調和完成 TODO 上有不少的幫助。有空會再把這些心得記錄下來!