Protocol Oriented Programming:POP 真的比 OOP (物件導向編程) 更好嗎?


本篇文章將利用 Swift 4 深入討論 “Protocol Oriented Programming” (POP,協定導向程式設計)。本文是 POP 系列文章的第二篇、亦是最後一篇文章,如你尚未讀過這篇簡介文章請先看過之後再繼續這篇教程

今天,我們將討論為什麼 Swift 被認為是「協定導向 (Protocol-oriented)」的程式語言、比較 POP 和 OOP (Object Oriented Programming,物件導向程式設計)、比較數值語義 (Value semantic) 和參考語義 (Reference semantics)、研究 Local reasoning、利用協定實現委任 (Delegation)、使用協定為類型或實現多型 (Polymorphism)、並檢視我在現實世界中以 POP 實作的 Swift 程式碼,最後討論為什麼我沒有完全投向 POP 的懷抱。

關於 WWDC 影片連結的注意事項

在這兩篇 POP 的系列文章中,我引用了至少三個連結到蘋果全球開發者大會 (WWDC) 影片,如果你使用的是 Safari,點擊這些連結可直接進入影片中的特定片段 (並會從特定時間指標位置開始播放);但如果你不是使用 Safari,你必須從連結本身收集時間指標位置,查找整段影片,及/ 或查看每段影片的腳本。

為何 Swift 是「協定導向」程式語言

還記得嗎?在我這篇 POP 的簡介文章中,我提到了蘋果的觀點:「協定導向是 Swift 的核心?」。相信我,它真的是。但為什麼呢?在回答這個的問題之前,先談一下 Comparative languages。

對其他語言有一定的了解,在某些時候很有幫助,例如在串接 C++ 函式庫到 iOS 應用程式時。因為部分 iOS 和 OS X 應用程式,我亦有開發其 Windows 版本,所以會與 C++ 函式庫串連。多年來,我一直在支援維護一些「跨平台 (Platform-independent)」的應用程式。

OOP 語言已經支持了各種接口 (Interfaces) 多年,這些接口與 Swift 協定相似而不相同。

這些語言的接口指定,必須由採用特定接口的類別和/ 或結構實作的方法和/ 或屬性。我使用了「和/ 或」,因為 C++ 沒有接口的概念,它是使用抽像類別的,而 C++ 的 struct 可以從類別繼承。C# 的接口得以指定屬性和方法,而且一個 struct 可以採用一個接口。Objective-C 使用術語 “Protocol (協定)” 而不是 “Interface (接口)”,因為協定可能需要方法和屬性,但只有類別可以採用協定,struct 則無法做到。

這些接口和 Objective-C 協定不能實作任何東西,它們只是指定了需求,為採用該協定的類別和/或結構作「藍圖 (Blueprints)」

協定是構成 Swift 函式庫的基礎,正如我在 POP 系列文章第一篇中展示的一樣,協定是實現 POP 的關鍵要素。

Swift 協定有些東西不為其他語言支持,就是Protocol Extensions。引述 Apple 官方說明:

Protocols can be extended to provide method, initializer, subscript, and computed property implementations to conforming types. This allows you to define behavior on protocols themselves, rather than in each type’s individual conformance or in a global function. …

By creating an extension on the protocol, all conforming types automatically gain this method implementation without any additional modification. …

You can use protocol extensions to provide a default implementation to any method or computed property requirement of that protocol. If a conforming type provides its own implementation of a required method or property, that implementation will be used instead of the one provided by the extension. …

上一篇文章中我展示過如何使用 Protocol extensions,今天也會再次示範,它們正是讓 Swift POP 如此強大的秘訣

即使在 Swift 之前,協定在 iOS 中也非常重要,還記得前一篇文章提過UITableViewDataSourceUITableViewDelegate 的協定嗎?它們是 iOS 開發者已經接觸多年的、也是日常的 Swift 編程語言程式碼。

編寫 Swift 程式碼時,不可能不使用標準函式庫 (Standard Library) 的協定。例如是 Array (繼承自 10 個協定struct)、Bool (繼承自 7 個協定struct)、Comparable (此協定繼承自另一個協定,並且是許多其他 Swift 類型的 Ancestor) 以及 Equatable (為一個許多 Swift 協定和類型都繼承自它的協定)。

花一些時間閱讀 Swift 標準函式庫,查看所有類型、協定、運算子 (Operators)、全局變量 (Globals)、函數的相關連結,並在頁面上查找 “Inheritance” 部分、點擊 “VIEW PROTOCOL HIERARCHY ->” 連結,你將看到許多協定和它們之間的繼承關係圖。

請記住一個重點:iOS (和 OS X) SDK 中大多數程式碼都是以類別層級的形式出現。我相信許多我們使用的核心框架仍然是用 Objective-C (以及一些 C ++ 和 C) 編寫的,例如:FoundationUIKit。查看 UIKit 中的 UIButton 類別,使用 Apple 的文檔頁面中的 “Inherits From” 連結,從葉節點 UIButton 直到根節點 NSObject,查看繼承樹的階層關係:UIButton UIControl,至 UIView,至 UIResponder,至 NSObject。在視覺上,它看起來像這樣:

UIButton Inheritance

POP 與 OOP

OOP 的優點已眾所周知,在此就不再多做解釋,我在這裡提供一個連結,內容是以 Swift 編寫 OOP 的詳細解釋,點擊此處前往。

提醒:如果你不了解 OOP,建議先學習 OOP 的相關知識,再考慮學習 POP。

使用 OOP 的好處包括可重用性、繼承性、可維護性、隱藏複雜性 (封裝)、抽象、多態性,還有對類別和對類別屬性和方法的存取控制。另外,OOP 可能還有更多我沒有提及的優點。

簡單來說,OOP 和 POP 大多數共享這些屬性,但有一個例外:類別只能從另一個類別繼承,而協定則可以繼承自多個協定。

根據 OOP 繼承的運作方式,限制單一繼承可能是最佳的做法,因為多重繼承很快就會變得非常混亂。

另一方面,協定可以採用一到多種其他協定。

為什麼推薦協定?當建立龐大的類別層級結構時,許多屬性和功能都可以繼承。開發人員傾向於添加一般功能 (Features) 到層級結構的頂部——主要針對 High-level 的 Ancestor classes,Mid-level 和 Leaf-level 的類別傾向具體實作,而不是當做功能容器。Ancestor classes 通常作為新功能宣告的置放容器,但往往會由於太多額外、無關和/或不相關的功能而被「污染」或「膨脹」。Mid-level 和 Leaf-level 類別最終會繼承一些不必要的功能。

這些關於 OOP 的擔憂並不是無法改變的,一位優秀的開發人員就可以避免我剛列舉的許多 OOP 陷阱,但這需要時間、練習和實作經驗。例如,開發人員可以將其他類別的實例 (Instances) 作為成員,添加到它們正在構建的類別中,而不是從其他類別繼承 (即 “Composition over inheritance” 的概念),來克服功能膨脹問題。

以 Swift 實作 POP 有一個優勢:協定可以被 structenum 這樣的數值類型 (Value types) 採用,而不僅僅被類別採用。接下來,我們將討論使用數值類型的一些優點。

我確實對多協定的一致性有點疑慮,我們會不會只是將原本程式碼中複雜且難以理解的類型,換成另一種類型而已?即是只將 OOP 繼承的「垂直」複雜性與 POP 繼承的「水平」複雜性交換?

將前面提到 UIButton 的類別繼承層級,與 Swift Array 的多重協定繼承進行比較:

Local reasoning 不適用於這些複雜的實體 (Entities),有太多的面向和關係要考慮。

比較數值語義與引用語義

正如上一篇文章提到,蘋果正在推廣 POP 和數值語義的相關概念。上回教程已向讀者展示程式碼,今天我將再次示範,這應該使引用語義和數值語義的意義更明顯。請參考上回教程ObjectThatFlies 協定,與今天 ListFIFOLIFO 和相關協定的實作程式碼。

蘋果工程師 Alex 認為,我們應該使用“數值類型和協定來讓你的 App 更好”,請參考這個 Playground 範例中名為 “Understanding Value Semantics” 的代碼文檔中所引述:

Sequences and collections in the standard library use value semantics, which makes it easy to reason about your code. Every variable has an independent value, and references to that value aren’t shared. For example, when you pass an array to a function, that function can’t accidentally modify the caller’s copy of the array.

當然,這適用於所有使用數值語義的類型,希望你下載並看一次整個 Playground 範例。

我不會、亦不能捨棄具有引用語義的類別,我自己有太多基於類別 (Class-based) 的程式碼,且用這數以百萬計的 Class-based 程式碼來服務我的客戶。我同意數值語義通常比引用語義更安全,當我撰寫的新程式碼或重構現有程式碼時,會基於不同個案考慮使用它。

引用語義可能因類別實例 (引用) 導致資料產生「無預期的影響」。 有人稱這為 “Unintended mutation (意外突變)”。我們可以參考此連結影片的一些技巧,來盡量減少引用語義的副作用,但我承認我越來越重視數值語義。

數值語義讓我們能避免對變量進行無預期的更改,這是一件非常好的事情。我們避免了因 Unintended mutation 而產生的副作用,因為「每個變量都有獨立的數值,並且對該數值的引用不會共享。」

由於 Swift struct 是一種數值類型,並且可以採用協定,加上 Apple 比起 OOP 更推薦使用 POP,所以你有理由支持「協定和數值面向編程 (Value Oriented Programming)」。

Local reasoning

讓我們來談談一個值得讚揚的目標,Apple 稱之為 “Local reasoning”。它是由蘋果工程師 Alex 在 WWDC 2016 – Session 419 “Protocol and Value Oriented Programming in UIKit Apps” 中所介紹的,這是蘋果推廣 POP 的第三個概念。

我認為這並不是新的概念,多年前,教授、同事、導師、開發者等都在談論這個技巧,例如:永不編寫長於一頁的程式碼,而且越短越好、拆分大型函式為多個小型函式、拆分大型程式碼檔案為多個小型程式碼檔案、使用有意義的變數名稱、在實作前花時間來設計架構、嚴謹且一致地使用空格及縮排、將相關連的屬性及行為分組到類別及結構中,並將相關的類別和/或結構整理到同一個組織單位中,如框架或函式庫。但它在蘋果解釋 POP 時被提出來了。

引述 Alex 的話:

Local reasoning means that when you look at the code, right in front of you, you don’t have to think about how the rest of your code interacts with that one function. You may have had this feeling before and that’s just a name for that feeling. For example, maybe when you just joined a new team and you have tons of code to look at but very little context, can you understand what’s going on in that single function? And so the ability to do that is really important because it makes it easier to maintain, easier to write, and easier to test, and easier to write the code the first time.

嗯⋯⋯ 這情境有沒有曾經發生在你身上?我閱讀過某些開發者寫得很好的程式碼,也寫過一些非常易讀的程式碼。但是說實話,在這行業 30 年後,大多數我所維護和/ 或優化的現有程式碼中,並沒有給我 Alex 描述的那種感覺。很多時候我非常沮喪,常常會看著一段程式碼而不知所云。

Swift 原始碼是公開的,請快速瀏覽一下下列函式,但不要太久時間試圖理解它:

老實說,你可以看一眼就明白這段程式碼嗎?我不可以。我不得不多閱讀幾次,並查找函數定義之類等。根據我的經驗,這樣的程式碼無處不在,而且通常是不可避免的。

現在讓我們考慮理解一個 Swift 類型 (當然,它不是一個函數)。看看 Swift 內 Array定義。我的天啊。它繼承自 11 個協定,包括 BidirectionalCollectionCollectionCustomDebugStringConvertibleCustomReflectableCustomStringConvertibleExpressibleByArrayLiteralMutableCollectionRandomAccessCollectionRangeReplaceableCollection、和 Sequence。點擊 “VIEW PROTOCOL HIERARCHY – >” 連結按鈕,並看看這複雜的繼承關係圖

最重要的是,如果你能夠開始一個新專案,並讓整個團隊自願購買同一套軟件開發最佳實踐指南,那麼實現 Local reasoning 就容易多了。如果一次重構少量的程式碼,則為實現 Local reasoning 提供了另一個機會。對於我來說,像其他事情一樣,重構需要小心謹慎地完成,並且要根據自身需求選擇最適合的方案。

請記住,你總會面對一些非常複雜的商業邏輯,當它們轉譯成程式碼時,團隊新成員將會需要接受某種形式的領域知識訓練和/或灌輸,才能夠流暢地閱讀你的程式碼,她/他很可能需要查找一些函數、類別、結構、列舉、變數等的定義。

委任 (Delegation) 與協定 (Protocols)

在整個 iOS 中,委託設計模式無處不在,而協定則扮演著很重要的角色。我們不再在這裡重新講述相關內容,讀者可以在此處閱讀我在 AppCoda 的相關教程。

協定類型 (Protocols as types) 和協定多態性 (Protocol Polymorphism)

本篇教程不會在這個主題上花太多時間,我已經說了很多關於協定的知識,並向你展示了很多範例代碼。這邊給讀者一項任務,我希望你研究使用 Swift 協定作為類型 (如委託) 的重要性,以及它們實現多態性 (例如:你建置一個擁有許多 struct 的工廠模型,它們都符合同個家族協定中的協定),為我們提供了多大的靈活性。

協定類型
請記住,在我關於 Delegates 的文章中,我定義過這個屬性:

在這裡,LogoDownloaderDelegate 是一個協定,然後我呼叫協定中的一個方法

協定多態性

就像在 OOP 中一樣,藉由遵從同族的父協定 (Families’ Parent Protocol),我們可以讓同族協定下的多種類型進行互動。以下的程式碼就是最佳範例:

如果你在 Playground 運行這段程式碼,控制台將輸出下列訊息:

實際於 UIKit 應用協定

讓我們開始大膽嘗試編寫一些 Swift 4 代碼,用於建置 iOS 應用程式,希望這段程式碼能讓你透過協定構建和/ 或擴展程式碼的角度開始思考。我們稱之為 “Protocol-oriented programming” 或 POP,也是我在兩篇系列文章中一直持續探討的應用。

我選擇展示如何擴展優化 UIKit 類別,因為 1) 你可能已經很習慣使用它們,而且 2) 要從自訂的類別中,擴展 iOS SDK 類別 (例如:UIView) 是更棘手的。

我在 Xcode 9 專案中以 Single View App 模板,編寫這些 UIView 優化 (Enhancement) 程式碼。

請注意,我正在使用 default 協定擴展來優化 UIView——要安全地執行這項操作,關鍵在於調用 “Conditional Conformance” (參考此處)。由於我只對擴展 UIView 類別感興趣,因此讓編譯器執行它,並將其設為必要條件。

我經常將 UIView 類別當成容器,來組織應用程式螢幕中的其他 UI 元素。有時候,我會以裝飾的方式使用這些容器視圖,以改善 UI 的外觀和佈局。

我將在下文創建三個協定,以定制 UIView 的外觀。這個 ani-GIF,展示了應用該三個協定的結果:

請注意,我在此遵循 “Local reasoning” 原則,每個基於協定 (Protocol-based) 的解決方案不超過一頁。我希望你可以看看每一個方案,它們包含的程式碼不太多,但仍然非常強大。

替 UIView 添加一個 Default border

假設我想要有很多 UIView 的實例,並全都具有相同的邊框 (Border),就像在有顏色主題的 App 一樣。此類別的範例,就是上圖中綠色最頂的視圖。

為了創建、配置並顯示 SimpleUIViewWithBorder 的實例,我將以下程式碼放在 ViewController 子類的 IBAction 中:

我不用為 UIView 這個子類,創建一個特定的初始化函式。

替 UIView 添加預設的背景顏色

假設我想要 UIView 的幾個實例都具有相同的背景顏色。此類別的範例,就是上圖中藍色置中的視圖。注意,我正在實現可配置的 UIView,你看到了嗎?

為了創建、配置並顯示 UIViewWithBackground 的實例,我將以下程式碼放在 ViewController 子類的 IBAction 中:

我不用為 UIView 的這個子類創建一個特定的初始化函式。

替 UIView 添加可配置的邊框顏色

現在我希望能夠為 UIView 的多個實例,配置邊框的顏色和粗幼。通過以下實作,我可以創建任意數量的視圖,並根據需要使用不同的邊框顏色和粗幼。此類別的範例,就是上圖中紅色最底的視圖。在協定添加可配置的屬性需要付出一些代價,我要能夠初始化那些屬性,所以我替協定添加了一個 init,這意味著我必須能調用 UIView 初始化函式,你可以在下列程式碼中看到:

為了創建、配置並顯示 UIViewWithBorder 的實例,我將以下程式碼放在 ViewController 子類的 IBAction 中:

我不想做的事

不想創建一個通用的代碼塊,如下所示:

在某些情況下,這可能是有用的;但它影響的範圍過大,會失去很多精細控制。這種類型的結構傾向成為每個 UIView 優化作業的聚集地,換句話說,這是一個龐大的代碼塊。它越龐大,其可讀性和可支持性就越差。

我使用類別——引用類型——讓 UIView 子類,遵循上述以 UIKit 為基礎的 Helper protocols (輔助協定)。通過子類化,我可以直接訪問父級 UIView 類別中的所有內容,讓我的代碼清晰、簡潔、易讀。如果我使用 struct,程式碼會更加冗長。你可以把它當成練習,試著透過實作來了解原因。

我可以做的事

請記住,所有這些 default 協定的 extensions,都可以在擴充類別 (Extended class) 被淩駕。下列範例和圖片將更詳細地解釋情況:

請注意在上方 SimpleUIViewWithBorder 類別中定義的註釋,看看這個 ani-GIF 的頂層視圖:

現實世界中,基於協定的 Generic (泛型) 資料結構

我為我創建的極簡化 POP 程式碼感到自豪,這些程式碼用於我的應用程式中,創建 Fully-functional 與 Generic stack 和 queue 資料結構,有關使用 Swift 泛型的背景知識,請在此閱讀我的 AppCoda 文章。

請注意,我使用協定繼承來幫助我從更抽象的 List 協定中,構建專門的 FIFOLIFO 協定。然後,我利用協定擴展實現了 QueueStack 數值類型。你可以透過 Xcode 9 Playground 看到這些 struct 類型的實例。

蘋果建議透過採用其他協定來實現協定定制化,為了向你展示這個建議,我創建了 ListSubscriptListPrintForwardsListPrintBackwardsListCount 協定,它們在這邊看起來很簡陋,但在實際的應用程式中相當實用。

這種採用多個協定的目的,是允許開發人員添加功能到程式碼基底 (Codebase),且不會因為有太多額外、無關和/ 或不相關的功能,而造成「污染」或「膨脹」。這些 Helper 協定內,每一個成員都是獨立的。如果在繼承層級結構中,添加到 Leaf-level 以上的類別,這些功能將自動由家族樹中的其他類別繼承,具體取決於它們在樹中的位置。

我已經充分地解釋了 POP,以便你閱讀並理解此代碼。我會給你一個提示,關於如何讓我的資料結構類型變為 Generic: “Associated types:” 的定義

When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.

相關程式碼如下:

從程式碼片段輸出的控制台畫面

我沒有 100% 轉投 POP

在其中一段關於 POP 的 WWDC 視頻中,一位工程師/ 主持人說道 “We have a saying in Swift. Don’t start with a class. Start with a protocol”。嗯,也許吧,接著他開始關於使用協定來實現二分查找 (Binary search) 的冗長討論⋯⋯ 這是現在讀者最關心的問題嗎?

這聽起來像是人為設計的 POP 解決方案,也許它是有效的,也許這個解決方案有優點;我不知道。我的時間十分寶貴,沒時間來為象牙塔理論化。如果一些程式碼需要超過 5 分鐘去理解,我就知道它不符合蘋果 “Local reasoning” 原則。

如果你是像我這樣的軟體開發人員,對新方法 (技術) 保持開放態度是件好事,而管理複雜性是維持生計的中流砥柱。我絕不反對賺錢,但能全面性評估事情才是好的。請務必記住,蘋果是一家企業,賺取大筆資金是其任務之一,它們不久前總市值達到約 837 億美元,其中包含數千億現金和約當現金。他們當然想讓每個人都加入 Swift,企業想要將人們引入其生態系統,其中一個方法就是提供獨有的產品和服務。沒錯,Swift 是開源的,但蘋果從 App Store 賺了大錢,而且這些應用程式讓 Apple 裝置變得實用,許多應用程式開發人員正轉向 Swift。

我看不到有任何理由讓開發者侷限於 POP。我認為 POP 亦有一些問題,就如我評價其他技術 (甚至 OOP) 一樣。我們正在模擬實際情境,但充其量只能讓我們接近現實,沒有完美的解決方案。就如過去多年來其他人提出的好想法般,請將 POP 放入開發人員的工具箱中。

總結

我已經有 30 年軟體開發經驗,可以冷靜將各面向的觀點提出。你應該去仔細了解協定和 POP。最重要的是,設計並編寫自己的 POP 程式碼。

我已經花了很多時間體驗 POP,並且在應用程式中使用了本文介紹的協定,例如 SimpleViewWithBorderViewWithBackgroundViewWithBorderListFIFO 以及 LIFO。POP 是個相當強大的技巧。

正如我在上一篇介紹文章提到,學習及使用像 POP 的新技術並不是一個需要取捨的命題,POP 和 OOP 不僅可以共存,而且可以相互輔助。

所以踏出一步吧,試驗、練習、學習⋯⋯ 最重要的是,讓自己充分享受生活和工作!

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文Protocol Oriented Programming in Swift: Is it better than Object Oriented Programming?


熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Objective-C 和 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Oracle、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This