เปลี่ยนโค้ดธรรมดาให้อ่านง่ายขึ้นด้วย Enumerations (Swift)

Khemmachart Chutapetch
Nextzy
Published in
6 min readOct 26, 2017

ผมคาดว่าหลายๆ คนที่เข้ามาอ่านบทความนี้อาจจะเกิดปัญหาในใจ ว่าทำไมเพื่อนฉันมันเขียนโค้ดอ่านยากจังวะ? แต่อย่าเพิ่งไปว่าเพื่อนครับ ลองกลับไปอ่านโค้ดของตัวเองที่เขียนไว้เมื่อเดือนที่แล้ว ก็อาจจะเกิดคำถามขึ้นว่า เขียนเชี่ยไรไปวะเนี่ย ต้องเสียเวลานั่งไล่โค้ดอีกเป็นสิบนาที

Source: https://www.otssolutions.com/blog/wp-content/uploads/mobile-app.jpg

ทำไมต้องเขียนโค้ดให้อ่านง่าย

ขอยกตัวอย่างโค้ดที่ iOS Developer หลายๆ คนน่าจะคุ้นเคยกัน คือ การใช้ Table View Data Source ในการระบุจำนวน row ในแต่ละ section มาให้ดูกันก่อน

/* Return number of rows in each sections */func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
} else if section == 1 {
return 1
} else if section == 2 {
return 10
} else if section == 3 {
return comments.count
}
return 0
}

มองแว้บแรก คำถามที่เกิดขึ้นก็คือ แต่ละ Section คืออะไร? เราจะรู้ได้อย่างไรหากไม่ทำการไล่ดูโค้ด? แล้วทำไม ข้างล่าง if-else statement จะต้องมี return 0 ด้วยล่ะ ในเมื่อเราก็เช็คทุก If-else Statement ครบหมดแล้ว? แล้วมั่นใจรึเปล่าว่าเราเช็คครับ?

หากเกิดคำถามแบบนี้ขึ้นใจในเมื่อไหร่ ก็ถึงเวลาที่เราจะต้องมา Refactoring ให้โค้ดของเราอ่านง่ายขึ้นกันแล้วละครับ บางคนอาจจะบอกว่า แล้วทำไมไม่ใส่คอมเม้นท์เข้าไปล่ะ? สำหรับผมผมคิดว่าการเขียนคอมเม้นท์คือการอธิบายการทำงาน ของฟังก์ชั่นนั้นๆ ไม่ใช่การเขียนเพื่ออธิบายโค้ด ของเราครับ ดังนั้นในกรณีนี้ไม่ควรเขียนคอมเม้นท์แต่ควรเขียนโค้ดให้เข้าใจง่ายมากกว่า

“ Write code for humans, not machines “

แล้ว Enum จะเข้ามามีส่วนช่วยอย่างไร

ก่อนอื่นขอเกริ่นถึง Enum กันซักเล็กน้อย หากผู้อ่านมีความรู้มาบ้างแล้ว สามารถข้ามไปอ่านหัวข้อ Exaple — การประยุกต์ใช้งานกับ Table View ในหัวข้อสุดท้ายเพื่อดูการใช้ Enum เพื่อทำให้โค้ดอ่านง่านขึ้นได้เลย แต่ผมแนะนำให้อ่านทีละหัวข้อจะดีกว่า เพราะในหัวข้อสุดท้ายจำเป็นต้องใช้ความรู้ในหัวข้อส่วนอื่นๆ ด้วยครับผม

สำหรับ Enum นั้นมีชื่อเต็มๆ คือ Enumeration เป็นตัวแปรประเภท User-defined data type หรือ ข้อมูลที่ผู้ใช้สามารถกำหนดแบบข้อมูลตามที่ต้องการได้ โดยส่วนใหญ่ enum จะใช้เก็บ กลุ่มของข้อมูลที่มีความเกี่ยวข้องกัน ซึ่งจะทำให้เราเขียนโค้ดแบบ type-self ได้สนุกมากขึ้น โดยเราจะใช้คีย์เวิร์ด enum ในการประกาศตัวแปรประเภท Enum นี้ขึ้นมา

ถ้าใครเคยเขียนภาษา C มา น่าจะคุ้นเคยว่า Enum ในภาษา C นั้น คือการตั้งชื่อให้กับกลุ่มข้อมูลประเภท Interger value นั่นเอง จากนั้นก็เรียกใช้งานผ่านชื่อที่เราตั้งไปเมื่อซักครู่ ซึ่งมันก็เหมือนกับเราเรียกใช้งาน Interger value ภายใต้ชื่อตัวแปรที่เราตั้งขึ้นมา แต่ในภาษา Swift นั้นมีดีกว่าแน่นอน

Functionality — ความสามารถพิเศษ

Enum ในภาษา Swift นั้นง่ายกว่ามาก เพราะเราไม่จำเป็นต้องกำหนดค่าเริ่มต้น (Interger value) ให้กับแต่ละ case เหมือนภาษา C เลย — แต่ถ้าหากเราอยากจะกำหนดค่าให้แต่ละ case ก็สามารถทำได้ ซึ่งเราเรียกมันว่า “raw value” โดยค่าที่เรากำหนดให้นั้นก็สามารถเป็นได้หลายประเภทไม่ใช่แค่ Integer เท่านั้น เช่น String, Character, Interger, หรือแม้แต่ Floating-point ก็สามารถเป็นได้

นอกจากนั้นในแต่ละ Case ของ Enum ยังสามารถรับค่าจากภายนอกเข้ามาได้ เรียกว่า Associated values ซึ่งสามารถเป็น type อะไรก็ได้ หรือพูดได้ว่า Enum คือ การกำหนดกลุ่มของข้อมูลที่มีความเกี่ยวข้องกัน โดยในแต่ข้อมูลนั้นๆ ก็สามารถมีข้อมูลที่มีความเกี่ยวข้องกันได้อีก .. ฟังดูแล้วอาจจะงงๆ ยังไงรอดูตัวอย่างข้างล่าง จะทำให้เข้าใจได้มากขึ้นนะครับ

Enum ในภาษา Swift นั้นมีความสามารถเยอะมากๆ ไม่ใช่แค่เก็บข้อมูลที่มีความเกี่ยวข้องกันเพียงแค่นั้น แต่มันยังเอาความสามารถของ class ใส่เข้ามาด้วย เช่น Computed properties, Instance methods

นอกจากนี้ Enum ยังมี Default initializers สำหรับกำหนดค่าเริ่มต้นให้กับตัวมันเองมาให้ด้วย และแน่นอนว่าเราก็สามารถสร้าง initializers ขึ้นมาใช้งานเองก้ได้ ซึ่งเดี๋ยวจะมีอธิบายให้ภายในบทความนะครับ — สุดท้ายแล้ว enum ยังสามารถ conform protocols ต่างๆ ได้อีก ความสามารถมันจะเยอะไปไหนเนี่ยยย เอาเป็นว่าเรามาทำความรู้จักกับความสามารถของมันทีละอย่างดีกว่าเนอะ

Syntax — รูปแบบภาษา

อย่างที่กล่าวไปในตอนแรก เราจะต้องใช้คีย์เวิร์ด enum ในการสร้าง Enum ขึ้นมาจากนั้นก็ใส่ Enum values ภายในปีกา { .. }

enum EnumName {
// enumeration values are described here
}

ยกตัวอย่างเช่น การสร้าง enum สำหรับวันทั้งเจ็ดในหนึ่งอาทิตย์ โดยแต่ละ case คือ value ที่เรากำหนดขึ้นภายใต้ enum นั่นเอง — ในภาษา Swift 3 นั้นจะตั้งชื่อ case นำหน้าด้วยตัวอักษรพิมพ์เล็ก ส่วนชื่อของ enum นั้นจะตึ้นด้วยตัวอักษรพิมพ์ใหญ่

enum DaysOfWeek {
case sunday
case monday
case tuesday
case wendesday
case thursday
case friday
case saturday
}

Example — ตัวอย่างการใช้งาน

enum Season {
case summer
case rains
case winter
}

ในการกระกาศตัวแปรในภาษา Swift นั้น เราสามารถให้ Complier นั้นกำหนด Variable type ให้เหมาะสมกับ Value ได้ โดยเราเรียกตัวแปรประเภทนี้ว่า Inferred type — และถ้าหากเราต้องการกำหนดค่า Enum ให้กับตัวแปรประเภท Inferred type นั้นเราจะต้องเรียกผ่านชื่อของ Enumeration ด้วยเพื่อเป็นการอ้างอิงถึง enum case เช่น

let currentSeason = Season.summer  // Inferred type

แต่ถ้าหากว่าเป็นการกำหนดค่าให้กับตัวแปรประเภท Explicit type หรือตัวแปรที่มีการระบุ Type เราสามารถกำหนด value ของ enum ให้กับตัวแปรนั้นๆ ได้เลย

let nextSeason: Season = .rains    // Explicit type

Enumeration กับ Switch Statement

Switch statement คือ คำสั่งสำหรับการสร้างเงื่อนไขแบบทำหลายทิศทาง โดยรับตัวแปรมาหนึ่งตัวแปรจากนั้นค่อยทำการตรวจสอบตามเงื่อนไขต่างๆ ที่เรากำหนดไว้ ซึ่งถ้าหากไม่ตรงกับเงื่อนไขใดๆ เลยก็จะเข้า ‘default’

enum Climate {
case india
case america
case africa
case australia
}
let season: Climate = .americaswitch season {
case .india:
print("Climate is Hot")
case .america:
print("Climate is Cold")
case .africa:
print("Climate is Moderate")
case .australia:
print("Climate is Rainy")
}

และเมื่อเราลองนำโค้ดตัวอย่างข้างบนไปรันใน Playground ก็จะได้ผัลลัพธ์ดังนี้

Climate is Cold

ลองมาดูคำอธิบายโค้ดข้างบนกันครับ ในขั้นตอนแรกนั้นโปรแกรมจะทำการสร้าง Enum ชื่อ Climate ขึ้นมา และทำการประกาศสมาชิกของ Enum คือ india, america, africa, และ australia ขึ้นมา

จากนั้นได้ทำการประกาศค่าคงที่ชื่อ season และกำหนดค่าเริ่มต้นเป็น .america และ Switch statement ก็จะทำการตรวจสอบว่าตัวแปร seasion นั้นว่าเป็น case ไหน จากนั้นก็ทำตาม statement ที่ได้ระบุไว้ใน switch-case นั้นๆ คือการพิมพ์ข้อความ ‘Climate is Cold’ ออกมาทางหน้าจอ

Associated Value ใน Enumeration

Enum ในภาษา Swift สามารถรับและเก็บค่าต่างๆ ไว้ในแต่ละ case โดยที่ค่านั้นๆ สามารถเป็นประเภทอะไรก็ได้ตามแต่เราจะระบุลงไป รวมถึงสามารถเรียกใช้งานค่าเหล่านั้นได้ในภายหลัง

เราเรียกค่าเหล่านี้ว่า Associated value ลองดูตัวอย่างข้างล่าง สมมติว่าเรามี Enum ชื่อว่าสำหรับแบ่งประเภทของบุคลากรชื่อว่า Personnel โดยในแต่ละบุคลากรก็จะแบ่งเป็นหน้าที่ต่างๆ เช่น Intern, Content writer, Manager ความพิเศษก็คือเราสามารถ เก็บค่าคงที่ต่างๆ ไว้ในแต่ละ case ของ enum ได้

enum Personnel {
case intern(name: String, advisor: String)
case contentWriter(name: String, contentType: String)
case manager(name: String, departmentNo: Int)
}

จากโค้ดข้างต้นเราสามารถแปลได้ว่า “สร้าง Enumeration ชื่อว่า Personnel โดยสามารถระบุเป็น 1. Intern ที่ประกอบไปด้วยชื่อและชื่อที่ปรึกษา 2. Content writer ประกอบไปด้วย ชื่อและประเภทของเนื้อหาที่เขียน 3. Manager ที่ประกอบไปด้วย ชื่อและแผนกที่ดูแล”

ซึ่ง Associated value นั้นมีคุณสมบัติคล้ายกับการรับส่ง Parameters ของ Function เพียงแต่ว่าเราสามารถเรียกใช้งาน Assciated value นั้นได้ในภายหลัง

let personnel = Personnel.contentWriter(name: "Khemmachart", contentType: "iOS Tutorial")

เราสามารถเก็บค่าต่างๆ ไว้ใน enum case เหล่านั้นได้ผ่าทางการประกาศ constant หรือ variable ซึ่งจำเป็นจะต้องระบุให้ตรงกับ Type ของ Associated values นั้นๆ

switch personnel {case .intern(let name, let advisor):
print("The intern name is " + name + " has advisor name, " + advisor)
case .contentWriter(let name, var contentType):
print("The content writer name " + name + " is writing content " + contentType)
case .manager(let name, let departmentNo):
print("The manager of department number \(departmentNo) is name " + name)
}

หลังจากนำค่าคงที่ชื่อ personnel มาเข้า Switch-Case และทดลองรันใน Playground ก็จะได้ผลลัพธ์ออกมาทางหน้าจอดังนี้

The content writer name Khemmachart is writing content iOS Tutorial

จากโค้ดข้างบนจะเห็นเราสามารถส่งผ่านและเรียกใช้งานค่าต่างๆ ผ่าน Associated values ที่สร้างขึ้นมาได้ โดยในการเรียกใช้งานนั้น จะต้องสร้างตัวแปรขึ้นมาให้ตรงกับจำนวนของ Associated values หรือสร้างมาแค่ตัวเดียวหากต้องการรับเป็น Tuple เช่นในกรณีข้างล่าง value ที่ได้จะเป็นประเภท Type

case .contentWriter(let value):
print("The content writer name " + value.name + " is writing content " + value.contentType)

และถ้าหากว่าเราต้องการเปลี่ยนค่าที่ถูกเก็บไว้ใน Associated values นั้นเราสามารถเปลี่ยนตัวแปรที่เราสร้างมารับ จาก Constant เป็น Variable ได้ เช่น

case .contentWriter(var name, let contentType):
name = "Mr. " + name
print("The content writer name " + name + " is writing content " + contentType)
// Print The content writer name Mr. Khemmachart is writing content iOS Tutorial

หรือถ้าหากว่าเราต้องการเปลี่ยนแปลงค่าทุกค่าใน case นั้นๆ เราสามารถระบุ var ไว้ข้างหน้า case นั้นๆ ได้เลย เช่น

case var .contentWriter(name, contentType):
contentType = contentType + " Content"
name = "Mr. " + name
print("The content writer name " + name + " is writing content " + contentType)
// Print The content writer name Mr. Khemmachart is writing content iOS Tutorial Content

Raw Value ใน Enumeration

นอกจาก Associated value แล้ว enum ยังสามารถเก็บค่าได้ในตัวของมันเอง ในแต่ละ case แต่จะแตกต่างกับ Associated value ตรงที่ค่าเหล่านี้จะเป็น ค่าเริ่มต้น ไม่สามารถเปลี่ยนแปลงได้ และจำเป็นต้องเป็นประเภทเดียวกัน เราเรียกค่าเริ่มต้นนี้ว่า Raw values

Raw values สามารถเป็นได้หลายประเภททั้ง String, Characters, Integer หรือ Floating-point ซึ่ง Raw value นั้นไม่สามารถซ้ำกันได้ภายใต้ Enumeration นั้นๆ เพราะจำเป็นต้องใช้ในการอ้างถึงในแต่ case เมื่อเราต้องการ Initialize enumeration

หากเราประกาศ enum เป็นประเภท Int หรือ String เราไม่จำเป็นต้นระบุค่าเริ่มต้นให้กับ enum นั้นๆ Swift complier สามารถระบุค่าเริ่มต้นให้กับ enum ประเภท Int หรือ String โดยอัตโนมัติ ยกตัวอย่างเช่น

enum Months: Int {
case january
case february
case march
}
enum CompassPoint: String {
case north
case south
case east
case west
}
Months.january.rawValue // Get raw value as 0
CompassPoint.north.rawValue // Get raw value as "north"

จะเห็นได้ว่า Raw values กับ Associated values นั้นต่างกันโดยสิ้นเชิง — Raw values นั้นคือ Set ของตัวแปรประเภทเดียวกัน ที่ถูกกำหนดไว้ตั้งแต่เริ่มต้น ตั้งแต่ตอนสร้าง Enumeration ขึ้นมา และเมื่อเราเรียกใช้งาน Raw value ในขณะรันโปรแกรม เราจะได้ค่าเดิมเสมอ ต่างกับ Associated values ตรงที่เราสามารถเก็บค่าอะไรก็ได้ลงไปใน Enumeration case และสามารถแตกต่างกันได้ในแต่ละครั้งที่เรียกใช้งาน

Initializing กับ Raw Value

แน่นอนว่าเราสามารถประกาศตัวแปรหรือค่าคงที่ประเภท Enum และเรียกใช้งาน Raw value ของ case นั้นๆ ได้ ในทางตรงกันข้ามเราก็สามารถประกาศตัวแปรหรือค่าคงที่ Enum นั้นๆ ได้ผ่าน Enumeration initializer ของตัวมันเอง โดยรับ Raw value เข้าไปเป็น Parameters — แหละถ้าหาก Raw value ที่เราส่งเข้าไปนั้นไม่ตรงกับ case ใดเลยก็จะได้ nil กลับออกมา แต่ถ้าหากตรงกับ case ใดๆ ก็จะ return case นั้นๆ กลับมาให้เราเป็น Optional

enum Months: Int {
case january
case february
case march
}
enum CompassPoint: String {
case north
case south
case east
case west
}
let possibleMonth = Months(rawValue: 0)
// Get Optional(Months.january)
let possibleCompassPoint = CompassPoint(rawValue: "centre")
// Get nil

สาเหตุที่ค่าที่ได้กลับมาจาก initializer นั้นเป็น Optioanl type เพราะว่า ไม่ใช่ทุก Raw value ที่เราส่งเข้าไปจะตรงกับ Default value ของ enum นั้นๆ ดังนั้นหากจะนำ possibleMonth หรือ possibleCompassPoint ไปใช้งานควรจะทำ Optional binding เสียก่อน

นอกจากการใช้ Default initializer ที่ทาง Swift จัดไว้ให้แล้ว เรายังสามารถสร้าง Initializer ขึ้นมาเองได้ — ยกตัวอย่าผมจะทำการค่าคงที่ที่เป็นประเภท Months.january ซึ่ง Months.januar เองนั้นเป็น Enumeration ประเภท Int แต่ผมต้องการใส่ rawValue เป็น String ผมจึงทำการสร้าง Initializer ขึ้นมาอีกอันดังนี้

enum Months: Int {
case january
case february
case march
init?(string: String) {
switch string.lowercased() {
case "january":
self = .january
case "february":
self = .february
case "march":
self = .march
default:
return nil
}
}
}
let possibleMonthWithStringValue = Months(string: "january")
// Get Optional(Months.january)
let possibleMonthWithIntValue = Months(rawValue: 0)
// Get Optional(Months.january)

หลังจากที่ผมนำโค้ดไปรันใน Playground และลองใช้ Custom initializer ก็จะได้ผลลัพธ์ออกมาเป็น Months.january ทั้งคู่

Example — การประยุกต์ใช้งานกับ Table View

จากตอนแรกของบทความที่ผมได้แสดงให้เห็นถึงปัญหาของโค้ดที่อ่านยาก และเราก็ได้ทำความรู้จักกับ Enum กันไปบ้างแล้ว ทีนี้ผมจะเสนอวิธิการแก้ปัญหา และทำให้โค้ดเราอ่านง่ายขึ้นกันครับ

อันดับแรกหากลองนั่งไล่โค้ดในส่วนของฟังก์ชั่น numberOfRowsInSection เราจะเห็นว่าจะมีทั้งหมด 4 Sections ดังนั้นผมก็จะสร้าง enum ขึ้นมาสำหรับ Sections ทั้ง 4

enum Sections: Int {
case cover
case profile
case photos
case comments
}

ถึงแม้เราจะไม่เคยเห็น View หรือ Storyboard มาก่อนแต่จากการอ่านโค้ดข้างบนเราก็สามารถเดาได้ว่าแต่ละ Section นั้นคืออะไรบ้าง ซึ่งในส่วนนี้เราต้องประกาศประเภทของ Enum เป็น Int เพราะเราจะนำมาใช้กับ section และเนื่องจากเราประกาศเป็นประเภท Int จะทำให้ Sections เรียงลำดับจากบนลงล่างคือ cover, profile, photos, comments ตามลำดับ

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Sections(rawValue: section) else { return 0 }
switch section {
case .comments:
return comments.count
case .cover:
return 1
case .photos:
return 10
case .profile:
return 1
}

ทีนี้มาลองดูการใช้งานในฟังก์ชั่น numberOfRowsInSection ในการการเรียกใช้งานนั้นเราต้องนำ parameter section ที่เป็นประเภท Int ไปสร้างเป็น enum section ผ่าน Initializer และจากนั้นจึงจะนำมาใช้กับ Switch-case

จะเห็นว่าในตัวอย่าง ผมไม่ได้เรียงลำดับ case เป็น cover, profile, photos, comments เนื่องจาก switch-case จะเลือก case ที่ตรงกับ section ของเราให้เอง

ทีนี้เราจะสามารถรู้ได้ทันทีว่าแต่ละ section นั้นคืออะไรและมีจำนวน rows เท่าไหร่ เช่น section cover มี 1 row, section profile มี 1 row, section photos มี 10 rows, และสุดท้าย section comments จะมีจำนวน rows ขึ้นอยู่กับจำนวน comments อาจจะลำบากนิดหน่อยตอนเริ่มต้นเขียน แต่ผมเชื่อว่าการใช้เวลาเพียงไม่กี่นาทีในการสร้าง Enumertaion นั้นจะทำการ Maintenance มีความสุขมากยิ่งขึ้นครับ

สรุปบทความ

ในภาษา Swift นั้น Enumeration มาพร้อมกับความสามารถต่างๆ มากมาย และเนื่องจากการทำงานเป็นทีมนั้นสำคัญ เราเขียนโค้ดเพื่อให้เพื่อนอ่านไม่ใช่ให้คอมพิวเตอร์อ่าน ดังนั้นเราควรนำ Enumertaion มาใช้งาน เช่น เปลี่ยนจากการใช้ Index ที่เป็น Integer ให้เป็น Enumertion หรือการใช้ Enumertaion แทน String เพื่อป้องกันการพิมพ์ผิดพลาดและเป็นการระบุจำนวน Possible cases ที่มีโอกาสเกิดขึ้นได้

if index == 0 { 
...
} else if index == 1 {
...
}

เป็น

guard let type == IndexType(rawValue: 0) else { return }if type == .profile {
...
} else if type == .profile {
...
}

และตัวอย่างการนำ Enumeration มาใช้แทน String เพื่อป้องกันการพิมพ์ผิดพลาด เช่น

if statusString == "authorized" { 
...
} else if statusString == "undefined" {
...
}

เป็น

guard let status == StatusType(rawValue: "authorized") else { return }if status == .authorized { 
...
} else if status == .undefined {
...
}

Where To Go From Here?

จากตัวอย่างข้างต้น จะเห็นว่าผมนำ Guard มาใช้ — เหตุผลเพราะหลังจาก Guard ยังมี If-Statement หรือ Switch-case อีก ในส่วนนี้ถ้าหาก ใช If-Statement ในการทำ Optional binding อาจจะทำให้เกิด Nested-If ซึ่งทำให้อ่านโค้ดอยากขึ้น โดยเพื่อนๆ สามารถศึกษาเรื่อง Guard และการทำ Optional Binding ได้ที่บทความต่อไปนี้

นอกจากการ Refactoring โค้ดให้อ่านง่ายขึ้นแล้ว Enumerations ยังมีประโยชน์อีกมาก ยกตัวอย่างเช่น การนำมาเขียน Request service ให้กับ Alamofire ใช้ในการเรียก Web service ภายในโปรเจคของเรา โดยใช้ Router design patter — สามารถศึกษาได้ที่นี่

References:

  1. Enumerations
  2. Swift — Enumerations
  3. Enumerations in Swift for Newbies.
  4. สร้างตัวชุดแปรเพื่อใช้งานเฉพาะทาง Enumeration

--

--