Protocol Oriented Programming

Protocol Oriented Programming:簡介 Swift 的協定導向程式設計

Protocol Oriented Programming:簡介 Swift 的協定導向程式設計
Protocol Oriented Programming:簡介 Swift 的協定導向程式設計
In: Protocol Oriented Programming, Swift 程式語言

軟體開發者最大的敵人就是程式複雜度,所以當知到有新技術可以保證幫我處理這個混亂狀況,我便聽聽這是什麼新玩意。在 Swift,近年(至少自 2015 年以來)最「火熱」的方法論中,獲得最多關注的莫過於 “Protocol Oriented Programming” (POP,協定導向程式設計) 了。本篇文章將使用 Swift 4。在我撰寫程式碼的時候,我發現 POP 的確可以保證處理遇到的混亂狀況。有趣的是蘋果官方宣稱「從核心來看,Swift 其實是協定導向 (Protocol-Oriented) 的程式語言 」。這篇文章將分享我使用 POP 的經驗,為這個嶄露頭角的技術提供一個清晰簡潔的教學。

我將會解釋 POP 的核心概念、提供大量的範例程式碼、不可避免地比較 POP 和 OOP (Object Oriented Programming,物件導向程式設計) ,並向那些跟隨潮流、以為 POP 是萬能的程式設計者潑潑冷水。

協定導向程式設計是個可以加到你現有程式庫中的新工具,但並不能取代過去的基礎知識,像是拆分大型函式為多個小型函式、拆分大型程式碼檔案為多個程式碼檔案、使用有意義的變數名稱、在實作前花時間來設計架構、嚴謹且一致地使用空格及縮排、將相關連的屬性及行為分組到類別及結構中——這所有的常識可以讓世界變得更不一樣。如果你的程式碼無法讓同事們讀懂,那就是沒用的程式碼。

學習及使用像 POP 的新技術並不是一個需要取捨的命題。POP 和 OOP 不但可以共存,更可以互補的。對大多數開發者來說,包括我自己,要好好掌握 POP 需要很多時間及熱情。因為 POP 絕對不簡單,所以教學會分成兩部。本篇文章主要集中在介紹及解釋 Swift 的協定 (Protocol) 和 POP。第二部,我們將會深入解構 POP 的進階應用 (像是以協定開始建構 App 功能)、泛型協定 (Generic Protocol)、從引用語義 (Reference Semantics) 轉移到數值語義 (Value Semantics) 的動機、列舉 POP 與 OOP 的優缺點、比較 POP 及 OOP、判定為什麼「Swift 是協定導向程式語言」、以及深入了解透過 POP 來增強的 “Local Reasoning” 概念。我們今天將會接觸到一些上面提到更進階的主題,但只是表面上的而已,別擔心。

介紹

作為軟體開發者,管理程式的複雜度是我們本能上最關注的問題。當我們探索 POP 時,你雖然投入了時間來學習新技術,但未必馬上得到回報。雖然我這樣說,但看下去你就會發現 POP 如何處理複雜度,並提供了另一種控制軟體系統內混亂狀況的工具。

我聽到越來越多關於 POP 的討論,但還沒看到很多使用這個方法來撰寫的程式碼——即是還沒有許多人從協定、而非類別開始製作 App 功能。這不只是因為人類抗拒改變,更是因為學習並實作一種全新的模式是一件知易行難的事。在我正在撰寫的新 App 中,我漸漸發現自己開始在設計及實作 App 功能時,很自然且直覺地使用 POP。

這股新潮流帶來不少興奮感,很多人說要用 POP 取代 OOP;但這是沒可能的,除非像 Swift 這樣的 POP 語言被廣泛地修改。我是一個實用主義者,不是一個追逐潮流的人。在開發新的 Swift 專案時,我採取相容並蓄的方法。我根據不同情況使用 OOP 和 POP,並了解到這兩個模式不是互斥的——我融合了兩種技術。當你讀到這個系列文章的第二部時,你就會知道我在說什麼。

我在很久以前深陷於 OOP。我在 1990 年買了 Turbo Pascal 的零售版本, (譯註:Turbo Pascal 由 Borland 公司推出,是基於 Pascal 程式語言的擴展產品。其於 1989 年推出的 Turbo Pascal 5.5 中加入物件導向程式設計特性。) 經過一年適應了新的語言模式後,我開始設計、開發、發佈 OOP 的程式產品。我成了個 OOP 的信徒。當發現我可以延展增強自己的類別,我興奮極了。隨著時間過去,許多公司如微軟和蘋果開始開發大量基於 OOP 的程式庫,像微軟基礎類別庫 (Microsoft Foundation Classes,MFC)、.NET、以及 iOS 與 OSX SDKs。現今,開發者在製作 App 時已幾乎不需要重新製造輪子了。雖然 OOP 就像所有的方法論一樣有些缺點,但其優點仍然比較多。我們將會花上一點時間來比較 OOP 和 POP。

了解協定

當開發者開始編排一個新 iOS App 的基礎架構時,他們幾乎都從軟體框架像是 FoundationUIKit 中現有的類別開始。幾乎所有 App 都需要某種使用者介面導覽系統,使用者需要一些 Entry point 來進入 App,以及一些指標來引導他們到 App 的功能。瀏覽一下你 iPhone 或 iPad 上的 App 吧。

你開啟那些 App 的時候看到什麼呢?我猜你應該看到了 UITableViewControllerUICollectionViewController 、和 UIPageViewController 的子類別。

你們都一定能認出下面的程式碼片段,就是當第一次建立專案時的程式碼,例如在 Xcode 中建立一個基於 Single View App 模版的新 iOS 專案:

...
import UIKit

class ViewController: UIViewController
{
...

一些開發者會就此打住,並建立完全客製化的介面,但大多數人會採取另一個步驟。

所以當 iOS 開發者開始製作新 App 時,其中一個明顯的特色就是 OOP,那 POP 要用在哪裡呢?

你猜到答案嗎?試想想大部分開發者下一個步驟是什麼?就是調用協定 (以及實作代理函式,但這方面我們已經討論過了) 。

來看一個範例你就會很清楚了。我肯定你們大部分都用過 UITableViews。雖然這並不是一個 UITableView 的教學,但你應該知道在 UIViewController實作 UITableView 時,協定扮演著一個很重要的角色。在將 UITableView 加進 UIViewController 時,你的 UIViewController 必須調用 UITableViewDataSourceUITableViewDelegate 協定,就像:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate

簡單來說,調用 UITableViewDataSource 允許你將資料填入 UITableViewCells 中,例如是希望使用者能夠瀏覽的選單項目名稱。調用 UITableViewDelegate 則讓你能夠在 UITableView 的使用者介面上作細微控制,例如是當使用者在特定的 UITableViewCell 上點擊時執行適合的動作。

定義

在深入技術定義及討論之前,先定義常用的術語,以幫助大家了解主題。讓我們先了解外行人對於「協定」 (Protocol) 的定義:

… The official procedure or system of rules governing affairs of state or diplomatic occasions. …

The accepted or established code of procedure or behaviour in any group, organization, or situation. …

A procedure for carrying out a scientific experiment…

– from Oxford Dictionaries

蘋果官方文件 “The Swift Programming Language (Swift 4.0.3)” 中這樣解釋「協定」

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol. …

協定是一個非常重要的工具,為軟體的混亂情況帶來秩序。有了協定,我們可以要求一個或多個類別及結構包含某些最少的必要屬性,並/或提供某些最少的必要行為/功能。藉由 協定擴展 (Protocol Extension),我們可以為一部分或所有的協定方法提供預設行為。

調用協定

我們將製作一個 Person 類別,遵循(調用)蘋果內建的 Equatable 協定,即是:

Types that conform to the Equatable protocol can be compared for equality using the equal-to operator (==) or inequality using the not-equal-to operator (!=). Most basic types in the Swift standard library conform to Equatable. …

class Person : Equatable
{
    var name:String
    var weight:Int
    var sex:String
    
    init(weight:Int, name:String, sex:String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
    
    static func == (lhs: Person, rhs: Person) -> Bool
    {
        if lhs.weight == rhs.weight &&
            lhs.name == rhs.name &&
            lhs.sex == rhs.sex
        {
            return true
        }
        else
        {
            return false
        }
    }
}

在蘋果官方文件中描述,「自定型別調用特定協定時,是藉由將協定的名稱放在型別名稱後面,並用冒號隔開,作為其定義的一部分。」我剛剛就這樣做了:

class Person : Equatable

你可以將協定概念化為一種合約保證,讓你可以應用在 classstructenum 上。我已經將 Person 類別輸入到 Equatable 協定的合約中 ,而 Person 類別保證完成合約,將會實作 Equatable 所要求要實現或達成的方法或成員變數。

Equatable 協定本身不會實作任何東西。協定只指定哪些方法及/或成員變數必須被實作調用(遵循) Equatable 協定的 classstructenum 上。有些協定透過 extensions 來實作功能,而我們很快就會談到這個。我不會花太多時間在 POP 使用 enum 上,我把這個留給你作為練習。

定義一個協定

要了解協定,最好的方式就是利用範例程式碼。我將會實作自己的 Equatable 協定,來讓你看看協定是怎麼運作的:

protocol IsEqual
{
    static func == (lhs: Self, rhs: Self) -> Bool
    
    static func != (lhs: Self, rhs: Self) -> Bool
}

記住,我的 “IsEqual” 協定並沒有實作 ==!= 運算子 (Operators)。”IsEqual” 要求這個協定的調用者實作它們自己==!= 運算子。

所有定義協定屬性和方法的規則總結於蘋果官方文件中。例如,在協定屬性的定義上,你不會使用 let,唯讀屬性只能使用 var 並接著 { get }來指定。如果你有一個方法來修改一個或多個屬性,你要標記這個方法為 mutating。你應該了解我的 ==!=運算子的覆寫為什麼被定義為 static;如果你不了解,這就是一個很好的練習讓你找出原因。

一個像我的 IsEqual (或 Equatable) 協定如何可以被廣泛適用?下文我們將使用協定來建立一個類別。但在開始之前,讓我們來談談關於引用語義 (Reference Semantics) 與數值語義 (Value Semantics)。

引用語義與數值語義

在我們繼續之前,你應該先閱讀蘋果 “Value and Reference Types” 這篇文章。這應該可以讓你思考引用語義及數值語義。我刻意地不詳細地闡述,希望你去認真思考及了解這個非常重要的題目,重要到蘋果同時在以下場合討論 POP 引用/ 數值語義:

  1. WWDC 2015 presentation on “Protocol-Oriented Programming in Swift”
  2. WWDC 2015 presentation on “Building Better Apps with Value Types in Swift”
  3. WWDC 2016 presentation on “Protocol and Value Oriented Programming in UIKit Apps”

讓我給你一個提示和作業:假設你有幾個引用至相同類別的實例要改變或變異 (mutating) 一個屬性,這些引用都指向相同的數據區塊,那稱它為「共享資料」亦不算誇張。在一些案例中,共享資料可能引起問題,就像下文提到的例子。那是否代表我們應該將程式碼都變更為數值語義嗎?不是!就如其中一位蘋果工程師指出:“So, for example, a Window. What would it mean to copy a Window? 看看下面的程式碼,然後思考一下。

引用語義 (Reference semantics)

以下是 Xcode Playground 的程式碼片段,片段呈現了當一個物件被複製、而其中一個屬性被更改時,一個有趣的困境。你可以找出問題所在嗎?我們將會在下篇文章中談談這點。

這段程式碼示範了如何定義一個協定,以及其 extension

// REFERENCE SEMANTICS: EVERYBODY HAS USED CLASSES
// FOR SO LONG -- AND THINK ABOUT ALL THE IMPLICIT
// COPYING THAT GOES ON IN COCOA.

protocol ObjectThatFlies
{
    var flightTerminology: String { get }
    func fly() // no need to provide implementation unless I want
}

extension ObjectThatFlies
{
    func fly() -> Void
    {
        let myType = String(describing: type(of: self))
        let flightTerminologyForType = myType + " " + flightTerminology + "\n"
        print(flightTerminologyForType)
    }
}

class Bird : ObjectThatFlies
{
    var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

class Bat : ObjectThatFlies
{
    var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// REFERENCE SEMANTICS

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

batCopy.flightTerminology = ""
batCopy.fly()
// just "Bat" prints to console

bat.fly()
// just "Bat" prints to console

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

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bat 

Bat

數值語義 (Value semantics)

在接下來的 Swift 範例程式碼片段中,我們使用 Struct,而不是 Class。跟之前的比起來,這裡的程式碼看起來安全多了,而且蘋果似乎正在推動數值語義和 POP。不過要注意的是,他們仍未放棄class

// THIS IS WHERE THE PARADIGM SHIFT STARTS, NOT JUST
// WITH PROTOCOLS, BUT WITH VALUE SEMANTICS

protocol ObjectThatFlies
{
    var flightTerminology: String { get }
    func fly() // no need to provide implementation unless I want
}

extension ObjectThatFlies
{
    func fly() -> Void
    {
        let myType = String(describing: type(of: self))
        let flightTerminologyForType = myType + " " + flightTerminology + "\n"
        print(flightTerminologyForType)
    }
}

struct Bird : ObjectThatFlies
{
    var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

struct Bat : ObjectThatFlies
{
    var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// VALUE SEMANTICS

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

// Here, it's obvious what we did to this INSTANCE of Bat...
batCopy.flightTerminology = ""
batCopy.fly()
// just "Bat" prints to console

// BUT, because we're using VALUE semantics, the original
// instance of Bat was not corrupted by side effects
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

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

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bat 

Bat flies WITHOUT feathers, and flaps wings differently than birds

程式碼範例

我已經寫了一些基於協定的程式碼。請閱讀一下這些程式碼,然後看看行內的註解和短文,並點點看附帶的連結,然後了解一下整個過程。你會在下一篇文章中用上它。

調用多個協定

當我開始撰寫這篇文章時,我有點貪婪地想要手動建立一個協定,包含兩個蘋果內建的 EquatableComparable 協定:

protocol IsEqualAndComparable
{

    static func == (lhs: Self, rhs: Self) -> Bool

    static func != (lhs: Self, rhs: Self) -> Bool
    
    static func > (lhs: Self, rhs: Self) -> Bool
    
    static func < (lhs: Self, rhs: Self) -> Bool
    
    static func >= (lhs: Self, rhs: Self) -> Bool
    
    static func <= (lhs: Self, rhs: Self) -> Bool

}

我發現我應該把程式碼拆分得越有彈性越好,那何不來試試呢?蘋果宣稱一個類別 (Class)、結構 (Structure) 或列舉 (Enumeration) 可以調用多個協定,就像我們將在下面看到的那樣。下面兩個協定是我自己想出來的:

protocol IsEqual
{
    static func == (lhs: Self, rhs: Self) -> Bool
    
    static func != (lhs: Self, rhs: Self) -> Bool
}

protocol Comparable
{
    static func > (lhs: Self, rhs: Self) -> Bool
    
    static func < (lhs: Self, rhs: Self) -> Bool
    
    static func >= (lhs: Self, rhs: Self) -> Bool
    
    static func <= (lhs: Self, rhs: Self) -> Bool
}

回憶一下演算法

你需要磨練的技能就是演算法的發展,然後將其轉換為程式碼。我肯定有天有人會口頭告訴你一些複雜的流程,然後要求你用程式碼寫出來。人類語言所描述的流程與在軟體中實際的流程之間,通常有很大的鴻溝。當我想要將 IsEqualComparable 協定用於一個類別中,好用來呈現一條線 (向量線) 時,我就想到這件事。我記得計算線條的長度是依據畢氏定理 (Pythagorean Theorem,參考說明 1說明 2),然後當以 ==!=<><=>= 運算子來比較向量時,線條的長度是必要的。例如一個繪圖 App 或遊戲中,當使用者點擊螢幕上兩個地方來建立一條連結兩點的直線時,我的 Line 類別就可以派上用場。

自定義類別調用多個協定

以下是我的 Line 類別,它調用了兩個協定:IsEqualComparable。這是一個多重繼承的形式。

class Line : IsEqual, Comparable
{
    var beginPoint:CGPoint
    var endPoint:CGPoint
    
    init()
    {
        beginPoint = CGPoint(x: 0, y: 0);
        endPoint = CGPoint(x: 0, y: 0);
    }

    init(beginPoint:CGPoint, endPoint:CGPoint)
    {
        self.beginPoint = CGPoint( x: beginPoint.x, y: beginPoint.y )
        self.endPoint = CGPoint( x: endPoint.x, y: endPoint.y )
    }
    
    // The line length formula is based on the Pythagorean theorem.
    func length () -> CGFloat
    {
        let length = sqrt( pow(endPoint.x - beginPoint.x, 2) + pow(endPoint.y - beginPoint.y, 2) )
        return length
    }

    static func == (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() == rightHandSideLine.length())
    }

    static func != (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() != rightHandSideLine.length())
    }
    
    static func > (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() > rightHandSideLine.length())
    }
    
    static func < (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() < rightHandSideLine.length())
    }
    
    static func >= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() >= rightHandSideLine.length())
    }
    
    static func <= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() <= rightHandSideLine.length())
    }

} // end class Line : IsEqual, Comparable

驗證你的演算法

我用了一個 Apple Numbers 試算表,並準備了這個兩條向量的視覺呈現,來簡單地測試 Line 類別的 length() 方法:

swift-pop-1

這是上圖顯示的點的測試程式碼:

let x1 = CGPoint(x: 0, y: 0)
let y1 = CGPoint(x: 2, y: 2)
let line1 = Line(beginPoint: x1, endPoint: y1)
line1.length()
// returns 2.82842712474619

let x2 = CGPoint(x: 3, y: 2)
let y2 = CGPoint(x: 5, y: 4)
let line2 = Line(beginPoint: x2, endPoint: y2)
line2.length()
// returns 2.82842712474619

line1 == line2
// returns true
line1 != line2
// returns false
line1 > line2
// returns false
line1 <= line2
// returns true

以 Xode Playground 的 “Single View” 模版來製作 UI 雛形並測試

你知道你可以使用 Xcode 9 Playground 中的 Single View 模版,來製作使用者介面 (UI) 的雛形並測試嗎?這功能非常棒,可以節省很多時間,並快速地建立雛形。為了穩健地測試我的 Line 類別,我便建立了這樣的一個 Playground。小作業:在我解說之前你先自己嘗試一下。我將會讓你看看我的 Playground 程式碼、模擬器輸出結果,以及我的 Swift 測試內容。

這就是我的 Playground 程式碼:

import UIKit
import PlaygroundSupport

class LineDrawingView: UIView
{
    override func draw(_ rect: CGRect)
    {
        let currGraphicsContext = UIGraphicsGetCurrentContext()
        currGraphicsContext?.setLineWidth(2.0)
        currGraphicsContext?.setStrokeColor(UIColor.blue.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 40))
        currGraphicsContext?.strokePath()
        
        currGraphicsContext?.setLineWidth(4.0)
        currGraphicsContext?.setStrokeColor(UIColor.red.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 60))
        currGraphicsContext?.strokePath()
        
        currGraphicsContext?.setLineWidth(6.0)
        currGraphicsContext?.setStrokeColor(UIColor.green.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 250, y: 80))
        currGraphicsContext?.strokePath()
    }
}

class MyViewController : UIViewController
{
    override func loadView()
    {
        let view = LineDrawingView()
        view.backgroundColor = .white

        self.view = view
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

這則是 Playground 模擬器畫面的視覺輸出:

swift-pop-2

這裡的 Swift 程式碼是用來測試 Line 類別實例,是否符合我在 Playground 所繪的向量:

let xxBlue = CGPoint(x: 40, y: 400)
let yyBlue = CGPoint(x: 320, y: 40)
let lineBlue = Line(beginPoint: xxBlue, endPoint: yyBlue)

let xxRed = CGPoint(x: 40, y: 400)
let yyRed = CGPoint(x: 320, y: 60)
let lineRed = Line(beginPoint: xxRed, endPoint: yyRed)
lineRed.length()
// returns 440.454310910905

lineBlue != lineRed
// returns true
lineBlue > lineRed
// returns true
lineBlue >= lineRed
// returns true

let xxGreen = CGPoint(x: 40, y: 400)
let yyGreen = CGPoint(x: 250, y: 80)
let lineGreen = Line(beginPoint: xxGreen, endPoint: yyGreen)
lineGreen.length()
// returns 382.753184180093
lineGreen < lineBlue
// returns true
lineGreen <= lineRed
// returns true
lineGreen > lineBlue
// returns false
lineGreen >= lineBlue
// returns false
lineGreen == lineGreen
// returns true

總結

希望你喜歡這篇文章,並密切留意此系列的「第二部」。下一部我們將深入了解 POP 的應用、從引用語義轉換至數值語義背後的動機、列舉 POP 和 OOP 的優劣、比較 OOP 與 POP、判斷為什麼「Swift 是協定導向的程式語言」,以及了解 “Local Reasoning” 的概念。

我們下次再會。

譯者簡介:楊敦凱-目前於科技公司擔任 iOS Developer,工作之餘開發自有 iOS App 同時關注網路上有趣的新玩意、話題及科技資訊。平時的興趣則是與自身專業無關的歷史、地理、棒球。來信請寄到:[email protected]

原文Protocol Oriented Programming in Swift: An Introduction

作者
Andrew Jaffee
熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。