Swift. Object-Oriented Programming(OOP)
讓我們稍微理解一下什麼是 OOP 吧!
▸ 前言:
在學習程式的初期,可能常常會聽到物件導向程式設計(Object-Oriented Programming、OOP),在剛開始聽到的時候你可能會覺得是一個很複雜的概念,但我認為大部分的狀況可能是你已經在有實做物件導向的相關操作,但你不知道這樣稱為物件導向。
我這邊會著重在維基百科上面提到的特性和範例,有興趣的讀者也可以先科普一下關於物件導向設計的概念:
此篇文章程式碼使用 Swift 編寫。
▸ 沒有物件的世界
在沒有 OOP 以前或者像是你還沒學習到物件(struct、class)的概念以前,你所編寫出的程式碼可能像是下圖這樣:
看起來似乎也沒有什麼太大的問題,可能就是一個簡單的計算矩形面積的程式碼。但請試著思考一下,如果今天你需要計算多個矩形面積的內容存在,你該怎麼處理?
這時候你可能會透過 width1
、height2
、area3
的命名方式來區份是哪個矩形的寬、高和面積,而你也可能透過創建陣列來存放這些內容,如此一來就不需要額外透過索引(index)來命名。
但透過這些方式常常會有意外的事情產生,像是你可能誤將 width1 * height2
(或是 width[1] * height[2]
),或者把 area1
設置為 width * height
的結果。而當你 widths
和 heights
如果數量有差異時,除了會有計算錯誤的問題以外,可能還會意外導致程式崩潰。
但從以上這些行為你可以發現,我們似乎想透過某些方式來結合這些資訊(寬、高、面積),使他成為某個事物(矩形)。而當我們在考慮這件事情的時候,其實我們同時也在進行物件的設計。
▸ 設計你的物件
在設計物件之前,首先你要知道物件導向中的物件(object)通常是被類別(class)所定義的,但是在 iOS 中我們還有一個結構(struct)能夠使用,而這兩者在使用上是有差異的,你應該根據需求使用不同方式定義。
若還不知道 classes 與 structures 的差異,或是還不認識這兩者的讀者,可以參考我以下的文章:
從上面的範例可以知道我們可能需要設計一個「矩形」的物件,而根據上面的內容我們知道我們的矩形具有寬、高以及面積,因此我們會根據這些內容設計出我們的矩形物件,這個行為稱為「抽象」。
你也可以反著去理解,抽象行為就是我們認為「這個物件」需要什麼內容,才稱為「這個物件」。而其中我們所定義的特性(屬性)與方法(函式)這些內容我們稱為「抽象特點」。
因此,我們來嘗試定義一下我們的物件:
這邊我們使用定義一個 Rectangle
類別,並且我們在其中添加 width
和 height
屬性,並且透過計算屬性的方式定義我們的 area
,讓他在調用時能夠計算出面積。
而我們所定義的 Rectangle
以物件導向的角度稱為類別,可以將它視為我們物件的藍圖或是模板,而不是物件本身。透過初始化所實例化的 rectangle
常數,才是我們的物件本身。
因此,假設我們以物件的方式,取代上面的寫法,應該會像以下這樣:
由此可知,我們抽象出一個矩形的概念,並且根據它定義出一個 Rectangle
的結構,此結構包裝了 width
、height
和 area
屬性,假使之後我們需要一個矩形的物件,我們就透過 Rectangle
的初始化器實例化即可。
▸ 物件特性
這邊的物件特性一樣主要適用於類別(class),某些特性在 struct 並不存在。
▸ 封裝(Encapsulation)
透過物件的封裝性,我們可以隱藏物件中某些部分的具體實現。並且封裝可以讓我們限制只有特定類別的物件可以存取或操作這一特定類別的內容,他們通常透過接口(interface)的方式實作訊息的傳遞。
在 iOS 中,我們可以透過 Access Control 來為我們的內容添加訪問級別,讓我們可以隱藏部分實作細節,單純暴露外部需要的接口即可。這邊我用 API 請求來作為示範。
關於 Access Control 可以參考我的文章:
首先我們定義一個簡單的 post 請求,但我們把它拆成三個函式:
我們主要會使用 sendRequest
發送我們的 post 請求,但 sendRequest
函式主體裡面會先呼叫 buidRequest
建立 URLRequest
,接著在呼叫 fetchData
來發送剛剛建立的 URLRequest
,並且透過閉包方式傳遞回 sendRequest
的閉包。
因此,假設我們今天要發送某個 post 請求,你的函式應該會這樣使用:
看起來似乎沒有太大的問題,能夠正常發送網路請求,也能收到訊息。但你可以嘗試思考看看,會不會有什麼問題產生?
在最後 doSomething
的函式中,我們僅僅呼叫 sendRequest
就能夠發送網路請求,並且獲得相對應的結果,因此 buildRequest
和 fetchData
兩個函式我們似乎不會單獨呼叫(或者不希望被單獨呼叫),因此某個程度上我們應該需要隱藏它。如果我們沒有隱藏此兩者,可能與你合作的對象會各別使用 buildRequest
和 fetchData
達成與 sendRequest
類似的操作,似乎有些多此一舉。因此,如果你希望與你合作的對象都依照你的方式使用,那你必須考慮這點。
接著就讓我們創建一個稱為 API 的物件,來封裝這些函式實現細節吧:
我們所建立的 API 類別一樣有 buildRequest
、fetchData
與 sendRequest
函式,但其中我們將 buildRequest
與 fetchData
的訪問級別設置為 private,如此一來外部的成員將無法調用這兩者(只能調用我們的 sendRequest
)
如此一來你就有一個幫你發送 API 的物件可以使用了,並且你也不需要知道他如何實現發送 API 的細節,只要知道呼叫 sendRequest
就能發送請求:
當然我們也可以封裝整個 doSomething
函式,來封裝獲取 data 後的細節。這邊我們將他封裝於 PostmanEchoAPI
類別中:
在最後我們的物件只需要使用 PostmanEchoAPI
物件就能達到相同的效果,並且他也不需要了解細節 doSomething
的實現細節,也不知道 PostmanEchoAPI
中透過 API
物件來幫他發送請求。
其實,我們在許多部分都會碰到所謂的封裝,舉例像是加密框架,你可能大概了解他的加密原理,但你其實不知道他背後實際處理了什麼,只知道可以透過某個函式或方法,就能達到加密的效果。像是我們常用的 xxxDelegate
也算是一種封裝,像是 UITableViewDelegate
,我們只需要告訴他有幾行,每一行的 Cell 是哪個,它就會生成該有的畫面到 TableView 中。
▸ 繼承(Inheritance)
在類別中,我們可以透過繼承的方式,繼承某個類別中的所有內容,被繼承的類別我們稱為父類別(Superclass),而繼承它的類別稱為子類別(Subclass),子類別除了具有父類別中的所有內容,也可以再添加額外的屬性以及方法等等,並且還能夠覆寫(override)父類別中的內容。
而在物件導向的描述中,子類別應該要比父類別更為具體化。像是動物 > 狗 > 狼,動物 > 貓 > 獅子。
這邊我們定義一些類別來作為使用,分別有 Dog
、Chihuahua
、Wolf
:
接著我們創建幾個物件實例,並且操作他們:
這邊因為我們 Dog
類別中有 bark
方法,因此繼承 Dog
類別的任何子類別都有此方法,而在 Wolf
類別中它 override
了 bark
方法,因此能夠做出與父類別不同的操作。
而這兩個子類別也分別額外定義了 bite
與 scream
方法,並且你無法在 Wolf
物件中使用 scream
,也不能在 Chihuahua
中使用 bite
。
物件導向中有提到,當一個類別繼承多個父類別時,稱為「多重繼承」。但是這種方式會令人難以理解(像是某隻狗同時是吉娃娃又是野狼),也很難使用。然而,多重繼承並不總是被支援的,假使你在 iOS 中嘗試繼承多個類別時,會出現 Multiple inheritance from classes 的編譯錯誤,在 iOS 中只有允許有一個父類別。
如果你想要達成類似效果,可以使用 protocol 的方式處理,讓某個物件遵循多個協議,但這種方式就會偏向協議導向(POP)的方式。
關於 Swift 中更多繼承相關的內容可以參考我以下的文章:
▸ 多型(Polymorphism)
多型是指由繼承而產生的相關的不同的類別,其物件對同一訊息會做出不同的反應。例如,上面範例中的 Chihuahua
和 Wolf
都繼承 Dog
,因此都有 bark
的方法,但我們呼叫 bark
時,他們的執行結果不一定相同。
這邊我們可以把這些 Dog
相關的物件放到類型為 [Dog]
的陣列中,並透過 for in 迴圈執行 bark
:
因為這些物件都是 Dog
類別或是繼承 Dog
類別的物件,因此我們可以將他們放置於一個 [Dog]
的陣列中,也能夠呼叫 Dog
的 bark
方法,但這時候我們是無法呼叫 Chihuahua
的 scream
方法或 Wolf
的 bite
方法,因為目前我們只將他們視為 Dog
類別。
要檢查我們陣列中 Dog
物件實際上是什麼類別,我們可以透過 is
方式來檢查陣列中每個物件的類別:
我們還可以透過 as
運算符來轉換成實際的子類別,這個操作我們稱為向下轉換,而我們可以透過 as?
嘗試轉換或是 as!
強制轉換他們:
關於更多類型轉換(Type casting)的內容,可以查看我的文章:
▸ 後記:
在物件導向開發中,物件應該是我們程式中的基本單位,基本上我們常見的 ViewController
,URLSession
、UserDefault
等內容幾乎都是物件。我們透過將屬性、方法和其他內容封裝成一個物件,來提高程式的靈活度、覆用性,在合作上也容易許多。最後的程式就是一堆物件的組合。
抽象概念使我們更好抽離每個物件的職責,你可以想像我們的物件就是一台機器或是電腦,你可以根據你的需求任意設計他的功能,使所設計出的物件能夠接收、傳遞、儲存資訊,並且也提供它人需要的功能。並且將不必要的資訊或是實作細節隱藏於其中。而透過這種方式開發我們的關注點也就更小(只有物件),在碰到問題的時候就相對容易調整、修正。