--- title: Liskov Substitution Principle tags: mentorship, Design Pattern description: 2019/11/11 --- # SOLID 之 L — 里氏替換原則 SOLID 的第三個原則是 Liskov Substitution Principle,里氏替換原則。 它的定義: > “Subtypes must be substitutable for their base types.” > 「子類別要可以替換掉父類別」 這個定義的重點是,子類替換掉父類後不會影響原有功能。 所以子類不能做跟父類不一致的事,所謂的不一致,是就**功能**而言。 是**功能、功能、功能**喔,不是說實作要完全一樣,如果實作內容完全一樣,不就失去多型的功能嗎? 比方說父類的有一個 function 叫 save(),其功能是把 user 資料存到 user default,它的子類如果覆寫 save(),它的實作不能失去「把 user 資料存到 user default」 的功能,但子類可以對 user 資料先作處理再存到 user default,這樣當 client 端用子類實體取代父類實體時才不會出現意料之外的 bug。 以上例子大概會是這樣: ``` class User { func save() { // 把 user 資料直接存到 user default } } class ProtectedUser: User { override func save() { // 把 user 資料加密後存到 user default } } class HashedUser: User { override func save() { // 把 user 資料 hash 後存到 user default } } var user: User = User() user.save() // 資料存到 user default user = ProtectedUser() user.save() // 資料存到 user default ``` 如果子類和父類的實體的功能不一致的,client 不一定會知道子類物件的實作內容是什麼,這樣很容易會出現意料之外的 bug。 我們在寫的當下可能會覺得我多加一個判斷去處理不一致的行為就好啦~ 可是當時間久了我們一定會忘了這一段或接手的人根本不知道這情況,再修改的時候可能會出現 bug。 ## 從 UIViewController 看里氏替換原則的必要性 我們都知道 UIViewController 有生命週期對應的各個 function: viewDidLoad 、 viewWillAppear 、 viewDidAppear等等,它們會按順序被呼叫。 像這邊,當畫面出現時我們可以順利印出: > view did load > view will appear > view did appear > ``` class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print("view did load") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) print("view will appear") } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("view did appear") } } ``` 我們都知道在這些 function 裡面我們需要寫 super 去呼叫它們的父類方法,不知道你有沒有想過,為什麼當我們覆寫這些 functions 時都必需先呼叫它們的父類方法? 在這邊的父類方法幫我們做了一些我們不知道的工作,如果我們沒有呼叫 super 或沒有正確呼叫就可能會產生問題。 譬如我們在 viewWillAppear(_:) 中呼叫的是 viewDidAppear(_:) 的父類方法,程式就會在 viewWillAppear(_:) 停止,不會再往下執行 viewDidAppear(_:) 中的內容: ``` override func viewWillAppear(_ animated: Bool) { super.viewDidAppear(animated) print("view will appear") } ``` consol 只會印出: > view did load > view will appear 從這邊可以看到,如果子類方法沒有跟父類方法功能一致的話,程式的執行結果可能會出現預期之外的效果。 ## 客製化元件的繼承 我們可能對 viewDidLoad() 的感受沒有很深,因為我們不知道它具體到底幫我們處理了什麼事項,所以我們可以再看我們自己客製化的元件。 在做客製化元件時,我們常會做一些相似但功能或排版不一樣的客製化元件。 這時我們可能會用到繼承。 比方說我們要做一個有外框、輸入時有刪除按鈕的文字輸入框,和一個多了 icon、其他效果一樣的文字輸入框,效果如下: ![](https://i.imgur.com/RhZ9vbU.gif) 以上元件完整的程式碼在[這裡](https://github.com/lumanmann/liskov-substitution-principle)。 我們可以寫好第一個元件後用繼承去寫第二個元件,有 icon 的輸入框 ImageTextField 可以繼承 CustomTextField 只需寫新增的 view 和其 layout,以減少重複的程式碼,因為兩個元件要做的事基本上是一樣的。 ```swift class ImageTextField: CustomTextField { var image: UIImage? { didSet { imageView.image = image } } private var imageView: UIImageView = { let iv = UIImageView() iv.translatesAutoresizingMaskIntoConstraints = false iv.contentMode = .scaleAspectFit return iv }() override func setupSubviews() { addSubview(inputTF) addSubview(clearButton) addSubview(imageView) setImageViewLayout() setInputTFLayput() setClearButtonLayout() } func setImageViewLayout() { NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10), imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10), imageView.widthAnchor.constraint(equalTo: self.heightAnchor), imageView.leftAnchor.constraint(equalTo: self.leftAnchor) ]) } override func setInputTFLayput() { NSLayoutConstraint.activate([ inputTF.centerYAnchor.constraint(equalTo: self.centerYAnchor), inputTF.bottomAnchor.constraint(equalTo: self.bottomAnchor), inputTF.leftAnchor.constraint(equalTo: imageView.rightAnchor), inputTF.rightAnchor.constraint(equalTo: clearButton.leftAnchor) ]) } } ``` 如果我們要讓 ImageTextField 輸入完以後的字變成橘色,我們要 override didEdit(), 因為 didEdited() 中是寫輸入結束後要做的事: ``` override func didEdited() { inputTF.textColor = .orange // 改變 inputTF 的字的顏色 } ``` 但這裡我們在 ImageTextField 覆寫了 didEdited() 去改變 inputTF 的字的顏色,我們會發現元件的外框沒有恢復沒有在輸入的樣式: ![](https://i.imgur.com/sT5Lxfk.gif) 這是因為父類方法中有復原成沒有在輸入的狀態: ``` func didEdited() { isEditing = false } ``` 我們覆寫後沒有做復原狀態的動作導致樣式沒有被恢復,所以要讓他們行為一致,可以改成這樣: ``` override func didEdited() { isEditing = false inputTF.textColor = .orange // 改變 inputTF 的字的顏色 } ``` 或是: ``` override func didEdited() { super.didEdited() inputTF.textColor = .orange // 改變 inputTF 的字的顏色 } ``` 這樣就可以得到我們想要的效果: ![](https://i.imgur.com/CfGR6Gc.gif) ## 總結 「里氏替換原則」就是要做到父類出現的地方,它的子類就能代替它,而且不會出現任何錯誤或異常。