การเขียนโปรแกรมแบบ Protocol-Oriented ในภาษา Swift 2
() translation by (you can also view the original English article)
บทนำ
จากการเปิดตัวของ Swift 2, Apple ได้เพิ่มฟีเจอร์และความสามารถของภาษา Swift ให้เพิ่มมากขึ้น หนึ่งในนั้นที่สำคัญที่สุด นั่นคือการปรับปรุง Protocols. จากการปรับปรุงการทำงานของ protocols ใน Swift นี้ ทำให้เกิดรูปแบบการเขียนโปรแกรมแบบใหม่ นั่นคือ protocol-oriented programming ซึ่งเราอาจจะรู้สึกแตกต่างจากที่เราเคยเขียนโปรแกรมแบบ object-orientated พอสมควร
ในบทความนี้ เราจะสอนวิธีการเขียนโปรแกรมแบบ protocol-oriented ของภาษา Swift ในขั้นพื้นฐาน รวมถึงข้อแตกต่างระหว่างการเขียนโปรแกรมแบบ object-oriented
เงื่อนไขเบื้องต้น
ในตัวอย่างของบทความนี้ เราจะใช้ Xcode เวอร์ชั่น 7 หรือสูงกว่า เพราะเป็นเวอร์ชั่นที่รองรับภาษา Swift 2
พื้นฐานของ Protocol
คุณอาจจะยังไม่คุ้นเคยกับ protocols, มันคือทางหนึ่งที่ช่วยกำหนดทิศทางการทำงานของ class หรือ structure. Protocol นั้นเปรียบเสมือนพิมพ์เขียว, แม่แบบ, กรอบการทำงานทั้งของ properties และ methods. Class หรือ Structure ที่เราสร้างโดยการอ้างอิง protocol นั้น จะต้องมีการ implement ทั้ง properties และ methods ตามที่เราได้กำหนดไว้ใน protocol อันนั้น.
สิ่งที่ต้องพิจารณาอย่างหนึ่งก็คือ properties และ method พวกนี้ สามารถจะถูกออกแบบให้เป็นตัวเลือกได้(optional), ซึ่งหมายความว่า เราไม่จำเป็นต้องไปเรียกใช้มัน. ตัวอย่างการสร้าง protocol และการเรียกใช้งานจาก class ใน Swift ก็จะเป็นประมาณนี้:
1 |
protocol Welcome { |
2 |
var welcomeMessage: String { get set } |
3 |
optional func welcome() |
4 |
}
|
5 |
|
6 |
class Welcomer: Welcome { |
7 |
var welcomeMessage = "Hello World!" |
8 |
func welcome() { |
9 |
print(welcomeMessage) |
10 |
}
|
11 |
}
|
ตัวอย่าง
เริ่มกันเลย เราจะเปิด Xcode และสร้างโปรเจคที่เป็น Playground ขึ้นมาสักอัน จะเป็น iOS หรือ OS X ก็ได้ เมื่อเราสร้าง Playground เสร็จแล้ว เราลองพิมพ์โค้ดลงไปตามโค้ดตัวอย่างด้านล่าง
1 |
protocol Drivable { |
2 |
var topSpeed: Int { get } |
3 |
}
|
4 |
|
5 |
protocol Reversible { |
6 |
var reverseSpeed: Int { get } |
7 |
}
|
8 |
|
9 |
protocol Transport { |
10 |
var seatCount: Int { get } |
11 |
}
|
ตอนนี้เราก็จะมี protocol ทั้งหมด 3 protocol ซึ่งแต่ละ protocol ก็จะมีแต่ละพร๊อพเพอร์ตี้ของตัวเอง หลังจากนั้นเราจะมาสร้าง structure ชื่อ Car ที่เป็นไปตามข้อกำหนดของทั้ง 3 protocols เรามาลองสร้างกันเลยตามโค้ดด้านล่าง
1 |
struct Car: Drivable, Reversible, Transport { |
2 |
var topSpeed = 150 |
3 |
var reverseSpeed = 20 |
4 |
var seatCount = 5 |
5 |
}
|
เราอาจจะสังเกตเห็นว่าทำไมเราสร้าง structure ที่เป็นไปตามข้อกำหนดของทั้ง 3 protocol แทนที่จะสร้าง class ?. ที่เราทำแบบนี้ เพราะเราต้องการที่จะหลีกเลี่ยงปัญหาหนึ่งของการโปรแกรมแบบ object-oriented ซึ่งนั่นก็คือ object references(การอ้างถึงออปเจ็ค)
เราลองจินตนาการว่าตอนนี้เรามีออปเจ็คอยู่สองตัว นั่นคือ A และ B. ในออปเจ็ค A ได้มีการสร้างค่าบางค่าและได้เก็บ reference ของค่านั้นไว้(ไม่ได้เก็บค่าของตัวแปลโดยตรง) ซึ่งเมื่อ A ได้ทำการแชร์ค่านั้นกับ B นั่นจะหมายความว่าทั้งสองออปเจ็คได้ reference มาที่ออปเจ็คเดียวกัน ซึ่งหากเมื่อ B ได้ทำการเปลี่ยนค่านั้น ค่าในออปเจ็ค A ก็จะถูกเปลี่ยนไปด้วย(เพราะ reference อยู่ที่เดียวกัน)
ถึงแม้มันอาจจะดูไม่ใช่ปัญหาใหญ่ แต่มันก็สามารถเกิดขึ้นโดยที่เราไม่รู้ได้ โดยที่ออปเจ็ค A ไม่ได้คาดการณ์ว่าข้อมูลจะถูกปรับเปลี่ยน ซึ่งนี่คือความเสี่ยงที่เราอาจเจอได้บ่อยๆของ object references
ในภาษา Swift, structures จะเก็บค่าที่ถูก assign ให้โดยตรง(passed by value) ซึ่งจะต่างจากการเก็บค่าแบบ reference หมายความว่าจากตัวอย่างที่เราเห็นในด้านบน, ถ้ามีข้อมูลที่ถูกสร้างขึ้นโดย A ในรูปแบบของ(value type) sturcture แทนที่จะสร้างเป็นแบบออปเจ็คที่แชร์ reference กับ B, ข้อมูลที่ถูกสร้างขึ้นใหม่นั้น จะถูกคัดลอกมาใช้แทนการอ้างถึง(reference type). และส่งผลให้ A และ B นั้น มีข้อมูลที่ copy เป็นของตัวเอง. คือการเปลี่ยนแปลงข้อมูลโดย B นั้น จะไม่ส่งผลกระทบต่อข้อมูลที่ถูกจัดการโดย A.
การทำให้ Drivable
, Reversible
, และ Transport
กลายมาเป็นแต่ละ protocols นั้น จะทำให้เราสามารถปรับเปลี่ยนได้ดีกว่าแบบที่ใช้งานแบบ class. ถ้าคุณได้อ่านตัวอย่าง GameplayKit framework in iOS9, จะพบว่าโมเดลแบบ protocol-oriented นั้นมีความคล้ายคลึงกับโครงสร้างของ Entities และ Components ที่ใช้ใน GameplayKit framework.
โดยวิธีการแบบนี้, เราจะสามารถสืบถอดฟังชั่นได้จากหลายๆที่ ไม่เหมือนกับที่เราใช้จาก superclass ซึ่งมี superclass ได้เพียงอันเดียว. โอเค เราเก็บเรื่องพวกนั้นไว้ในหัวก่อน, ตอนนี้เรามาลองสร้าง class ตามนี้ดู:
- คลาสที่มี components(ส่วนประกอบ) ของ protocols
Drivable
และReversible
- คลาสที่มี components(ส่วนประกอบ) ของ protocols
Drivable
และTransportable
- คลาสที่มี components(ส่วนประกอบ) ของ protocols
Reversible
และTransportable
จากคลาสด้านบน ในทางของการเขียนโปรแกรมแบบ object-oriented นั้น เราควรจะสืบทอดจาก 1 superclass โดยที่ superclass นั้น ยอมรับข้อตกลงของทั้ง 3 protocols ที่เราสร้างขึ้น. อย่างไรก็ตามวิธีการนี้ ที่เกิดขึ้นใน superclass จะมีความซับซ้อนมากกว่าในแต่ละ subclass เพราะมันสืบทอดฟังก์ชั่นมาเกินความจำเป็น.
Protocol Extensions
สิ่งที่เราได้บอกคุณมาทั้งหมดนั้น สามารถทำได้จริงในภาษา Swift ตั้งแต่ที่มันเปิดตัวในปี 2014. แนวคิดของ protocol-oriented นี้ ยังสามารถถูกนำไปใช้กับ protocols ใน Objective-C อีกด้วย. เนื่องจากข้อจำกัดที่เคยมีอยู่ใน protocols, การเขียนโปรแกรมแบบ protocol-oriented ยังไม่สามารถใช้ได้จริงใน Swift เวอร์ชั่นก่อนๆ จนกระทั่งได้มีการถูกเพิ่มฟีเจอร์เข้ามาใน Swift เวอร์ชั่น 2. ซึ่งหนึ่งในฟีเจอร์ที่สำคัญที่ถูกเพิ่มเข้ามาก็คือ protocol extensions ประกอบไปด้วย conditional extensions .
อย่างแรก, เราจะขยายการทำงานของ Drivable
protocol และเพิ่มฟังก์ชั่นที่จะทำการตรวจสอบ top speed ของ Drivable
ว่าเร็วกว่า Drivable อันอื่นหรือไม่. เราจะเพิ่มโค้ดส่วนนี้เข้าไป:
1 |
extension Drivable { |
2 |
func isFasterThan(item: Drivable) -> Bool { |
3 |
return self.topSpeed > item.topSpeed |
4 |
}
|
5 |
}
|
6 |
|
7 |
let sedan = Car() |
8 |
let sportsCar = Car(topSpeed: 250, reverseSpeed: 25, seatCount: 2) |
9 |
|
10 |
sedan.isFasterThan(sportsCar) |
อย่างที่เราเห็น, เมื่อเพลย์กราวน์นั้นได้รันโค้ดและแสดงผลลัพธ์เป็น false
นั่นเพราะว่าตัวออปเจ็ครถ sedan
นั้นมี topSpeed
แค่ 150
, ซึ่งน้อยกว่า topSpeed ของ sportsCar
.



คุณจะสังเกตได้ว่า เราได้ definition ของฟังก์ชั่น มากกว่าที่จะ declaration ฟังก์ชั่น. อาจจะดูแปลก, เพราะ protocols นั้นมีเพียงแค่ส่วนของการ declarations เท่านั้น. ถูกไหม ? default behaviors คือฟีเจอร์ที่สำคัญอีกอย่างหนึ่งของ protocol extensions ในภาษา Swift 2 ในการเพิ่มการทำงานของ protocol, เราสามารถสร้าง default functions หรือ properties ซึ่งเราไม่จำเป็นต้อง conform protocol มาที่ class เลย.
ต่อไป, เราจะอธิบายถึง protocol extension อันอื่นของ Drivable
, แต่ตอนนี้เราจะพูดถึง value types ที่ได้ conform จาก Reversible
protocol. ใน extension นี้ จะประกอบไปด้วยฟังก์ชั่นที่ตัดสินว่า object ไหนที่มี speed range(ช่วงความเร็ว) ที่ดีกว่า. เราสามารถทำได้โดยโค้ดด้านล่างนี้:
1 |
extension Drivable where Self: Reversible { |
2 |
func hasLargerRangeThan(item: Self) -> Bool { |
3 |
return (self.topSpeed + self.reverseSpeed) > (item.topSpeed + item.reverseSpeed) |
4 |
}
|
5 |
}
|
6 |
|
7 |
sportsCar.hasLargerRangeThan(sedan) |
คีย์เวิร์ด Self
, ที่สะกดด้วยตัว "S" ใหญ่นั้น, ถูกใช้แทน class หรือ structure ที่ได้ทำตาม protocol นั้นๆ. ซึ่ง Self
ในตัวอย่างด้านบน, นั่นก็คือ structure ที่ชื่อว่า Car
.
หลังจากที่เพลย์กราวน์ของเราได้รันโค้ดแล้ว, เราจะเห็นผลลัพธ์ได้ในฝั่งขวามือ แบบในตัวอย่างด้านล่าง. เช่นรถ sportsCar
มีขนาดความกว้างของความเร็วมากกว่ารถ sedan
.



การทำงานร่วมกับไลบรารี่พื้นฐานของ Swift
ซึ่งการกำหนดและการขยายการทำงานของ protocol จะทำให้เราสามารถใช้งานได้จริง เมื่อเราทำงานกับไลบรารี่หลักของ Swift. สิ่งเหล่านี้จะยอมให้เราเพิ่มฟังก์ชั่นหรือคุณสมบัติจาก protocols อันเดิม, เช่น CollectionType
(พวก arrays และ dictionaries) และ Equatable
(พวกเปรียบเทียบความเท่ากัน ใช่ หรือ ไม่). ด้วยเงื่อนไขของ protocol extensions นั้น, เราจะสามารถกำหนดฟังก์ชั่นเฉพาะของ object ที่ conforms(สอดคล้อง) กับ protocol นั้น.
มาลุยต่อกันใน playground, เราจะขยายขอบเขตการทำงานของ CollectionType protocol และเราจะเพิ่มฟังก์ชั่นเข้าไปอีก 2 ฟังก์ชั่น อย่างแรกคือ ฟังก์ชั่นหาค่าเฉลี่ย top speed ของ Car ที่อยู่ใน array และอีกอันก็จะเป็นฟังก์ชั่นหาค่าเฉลี่ยนของ reverse speed. ลองเพิ่มโค้ดส่วนนี้ดูครับ :
1 |
extension CollectionType where Self.Generator.Element: Drivable { |
2 |
func averageTopSpeed() -> Int { |
3 |
var total = 0, count = 0 |
4 |
for item in self { |
5 |
total += item.topSpeed |
6 |
count++ |
7 |
}
|
8 |
return (total/count) |
9 |
}
|
10 |
}
|
11 |
|
12 |
func averageReverseSpeed<T: CollectionType where T.Generator.Element: Reversible>(items: T) -> Int { |
13 |
var total = 0, count = 0 |
14 |
for item in items { |
15 |
total += item.reverseSpeed |
16 |
count++ |
17 |
}
|
18 |
return (total/count) |
19 |
}
|
20 |
|
21 |
let cars = [Car(), sedan, sportsCar] |
22 |
cars.averageTopSpeed() |
23 |
averageReverseSpeed(cars) |
ใน protocol extension ของเรานั้น เราได้สร้าง averageTopSpeed
method ซึ่งเป็นเทคนิคที่ใช้ประโยชน์จากการใช้ extensions ในภาษา Swift 2. ซึ่ง averageReverseSpeed
ฟังก์ชั่นนั้น เราได้ใช้อีกเทคนิคหนึ่ง นั่นคือการใช้ generics โดยเราก็จะได้ผลลัพธ์ที่คล้ายคลึงและถูกต้องเช่นกัน. ในความคิดส่วนตัว เราชอบมุมมองที่เป็นระเบียบ CollectionType
protocol extension, แต่ทั้งนี้ทั้งนั้นก็ขึ้นอยู่กับความชอบส่วนบุคคล.
ในฟังก์ชั่นทั้งสอง,เราได้ใช้ array ในการเพิ่มค่ารวมทั้งหมด และคืนค่าเฉลี่ยของค่าที่เราต้องการ. สังเกตได้ว่า เราใช้การนับวัตถุใน array, เพราะมันสามารถใช้งานได้ร่วมกับ CollectionType
ได้ดีกว่า Array
type, ซึ่งค่าของ Count
จะเป็น Self.Index.Distance
แทนที่ type ของมันจะเป็น Int
.
และเมื่อตอนที่โค้ดของเราถูกรัน, เราจะเห็นผลลัพธ์ค่าเฉลี่ยของ top speed คือ 183
และค่าเฉลี่ยของ reverse speed คือ 21
.



ใจความสำคัญของคลาส
ถึงแม้การเขียนโปรแกรมแบบ protocol-oriented จะมีประสิทธิภาพและขยายขอบเขตในการเขียนโปรแกรมในภาษา Swift, แต่ก็ยังมีเหตุผลที่ดีในการใช้ class สำหรับการเขียนโปรแกรมในภาษา Swift.
ความเข้ากันได้
SDK ส่วนใหญ่ ทั้งของ iOS, watchOS และ tvOS นั้นถูกเขียนขึ้นจากภาษา Objective-C, โดยใช้เทคนิคการเขียนแบบ object-oriented. ถ้าเราต้องการที่จะเรียกใช้งาน APIs ที่อยู่ใน SDKs เหล่านั้น, เราจะถูกบังคับให้ใช้ class ที่ถูกกำหนดใน SDKs เหล่านั้นด้วย.
การอ้างอิงถึงของ External File หรือวัตถุ
ตัวคอมไพลเลอร์ของ Swift นั้น จะจัดการ lifetime ของ objects โดยขึ้นอยู่กับว่ามันถูกใช้เมื่อไรและถูกใช้ที่ไหน. ตามรูปแบบของ class objects นั้น จะหมายความว่าแต่ละ objects ที่คุณอ้างถึงไปในแต่ละไฟล์ ก็ยังคงจะถูกอ้างถึงอยู่เหมือนเดิม.
Object References(การอ้างอิงของวัตถุ)
บางเวลาคุณอาจต้องใช้งานแบบ Object references, ตัวอย่างเช่น ถ้าคุณต้องการที่จะส่งข้อมูลเฉพาะบางอย่างไปให้ออปเจ็ค อย่างเช่นการเรนเดอร์กราฟิคบางอย่าง ที่ต้องอ้างถึงในทันที. คือเราจะใช้ class ในเหตุการณ์ประมาณนี้, เพราะเราต้องการที่จะมั่นใจว่าค่าที่เราส่งไปหรือแก้ไขนั้น จะยังเป็นค่าเดียวกับที่เราได้ทำไปก่อนหน้านี้.
บทสรุป
เราหวังอย่างยิ่งว่า เมื่อจบบทความนี้คุณจะได้เห็นแนวทางในการเขียนโปรแกรมแบบ protocol-oriented ในภาษา Swift แม้ว่ารูปแบบการเขียนโปรแกรมแบบนี้ จะไม่สามารถทดแทนรูปแบบการเขียนโปรแกรมแบบ object-oriented ได้ทั้งหมด, แต่มันก็แสดงให้เห็นแล้วว่ามันมีประโยชน์มากๆ.
จากพฤติกรรมพื้นฐานของทั้ง protocol extensions และการเขียนโปรแกรมแบบ protocol-oriented กำลังจะถูกนำไปใช้พัฒนาอีกหลายๆ API และจะเปลี่ยนวิธีคิดในการพัฒนาซอฟต์แวร์ไปโดยสิ้นเชิง.
ท้ายสุดนี้ ถ้าคุณมีข้อสงสัย หรือฝากคำแนะนำ ติชม ได้ที่ด้านล่างนี้เลยครับ