[TypeScript#2] การใช้งานคลาสใน TypeScript
แม้เราจะสามารถใช้งานคลาสใน ES2015 ได้แล้ว ถึงอย่างนั้นผู้ใช้ OOP จากภาษาอื่นก็อาจต้องการอะไรที่มากกว่าสิ่งที่ ES2015 จัดมาให้ TypeScript จาก Microsoft ผู้พัฒนาภาษา C# จึงอัดคุณสมบัติของคลาสและ OOP ใส่มาให้ใน TypeScript แบบแรงชัดจัดเต็ม เรียกได้ว่าใช้ TypeScript คล่องแล้วก็อย่าลืมกลับไปอุดหนุน C# จากเฮียเขาบ้างแล้วกัน
ก่อนที่เพื่อนๆจะอ่านบทความนี้ เราขอแนะนำให้อ่านสองบทความนี้ก่อนครับ
- [TypeScript#1] TypeScript คืออะไร? เรียนรู้ชนิดข้อมูลพื้นฐานของ TypeScript
- พื้นฐาน ES2015 (ES6) สำหรับการเขียน JavaScript สมัยใหม่ ในหัวข้อของคลาส
เพิ่มชนิดข้อมูลให้คลาสใน ES2015
ในเบื้องต้นนั้นคลาสของ TypeScript ก็คือการต่อยอดคลาสมาจาก ES2015 ครับ โดยเพิ่มชนิดข้อมูลและความสามารถอื่นใส่เข้าไปด้วย พิจารณาคลาสที่แปะชนิดข้อมูลดังนี้ครับ
1class Human {2 // เกิดเป็นคนไม่มีชื่อได้ยังไง3 name: string45 // พารามิเตอร์ของ constructor ฟังก์ชันก็ต้องมีการระบุชนิดข้อมูลเช่นกัน6 constructor(name: string) {7 this.name = name8 }9}1011// ประกาศ somchai แบบนี้ TypeScript จะอนุมานได้ว่า12// somchai มีชนิดข้อมูลเป็น Human โดยอาศัยดูจากค่าขวามือ13const somchai = new Human('Somchai')14// หรือ15// บอกให้ชัดเจนไปเลยว่า somchai เป็น**คน**นะเออ16const somchai: Human = new Human('Somchai')
การสืบทอดหรือ Inheritance
เนื่องจาก TypeScript มีไวยากรณ์ที่ครอบทับ ES2015 อีกที เราจึงใช้ extends เพื่อสืบทอดคลาสได้เช่นเดียวกัน ข้อจำกัดของการสืบทอดคลาสคือ ถ้าคลาสลูกมีการนิยาม constructor ไว้ต้องใส่ super เพื่อทำการเรียก constructor ของคลาสแม่ก่อน ก็ยังคงอยู่เช่นกัน
1class Human {}23class Woman extends Human {4 constructor() {5 // เมื่อคลาสลูกมี constructor อย่าลืมเรียก super นะครับ6 super()7 }8}
Access Modifiers
ยังจำพวกเราได้ไหม public, private, protected พวกเราคือระดับการเข้าถึง (Access Modifiers) ไง อย่าคิดว่าหนีมาใช้ JavaScript แล้วพวกเราจะไม่ตามมาหลอกหลอนนะ จงกลับมารู้จักกับพวกเราใหม่อีกครั้งใน TypeScript!
โดยปกติสมาชิกภายใต้คลาสจะมีสถานะเป็น public สถานะนี้เป็นการบอกว่าสมาชิกตัวดังกล่าวสามารถเข้าถึงได้จากใครก็ได้ ที่ไหนก็ได้
1class Human {2 // สมาชิกของคลาสตัวนี้ ไม่มีการระบุอะไรทั้งสิ้น จึงมีค่าเท่ากับ3 // public name: string4 // เมื่อเป็น public จึงสามารถเข้าถึงจากที่ไหนก็ได้5 name: string67 constructor(name: string) {8 this.name = name9 }1011 // เมธอดนี้ไม่ได้ระบุ Access Modifiers ใดๆ จึงมีค่าเท่ากับ12 // public printName()13 public printName() {14 // name ซึ่งเป็น public เข้าถึงจากภายในคลาสเองก็ได้15 connsole.log(this.name)16 }17}1819const somchai = new Human('Somchai')20// เข้าถึงจากภายนอกคลาสก็ย่อมได้21somchai.name = 'Somsree'
public นั้นไม่ปลอดภัยเมื่อข้อมูลนั้นเป็นคุณสมบัติของอ็อบเจ็กต์ในคลาส ถ้าใครๆก็เข้าถึงได้ง่ายการแก้ไขข้อมูลก็เป็นไปได้โดยง่าย นึกถึงอายุของมนุษย์ครับ ถ้าเราอนุญาตให้โค๊ดจากภายนอกเข้าไปแก้ไขอายุได้โดยตรง จะเกิดความเสี่ยงเมื่อมีใครซักคนทะลึ่งไปใส่อายุให้เป็น -99
1class Human {2 age: number34 constructor(name: string) {5 this.name = name6 }7}89const somchai = new Human('Somchai')10// จงย้อนวัยไป 99 ปี~~11// อ้า ตีนกาข้าหายไปแล้ว ฟินฝุดๆ12somchai.age = -99
private เป็นระดับการเข้าถึงที่อนุญาตให้เข้าถึงสมาชิกของคลาสตัวนี้ได้จากข้างในคลาสเท่านั้น ฉะนั้นแล้วใครหน้าไหนก็จะแก้ไขมันจากภายนอกไม่ได้
1class Human {2 // จงเป็น private เสียเถอะ3 private age: number45 constructor(name: string) {6 this.name = name7 }89 // เปิดเมธอดนี้ให้คนภายนอกเข้าถึงเพื่อตั้งค่าอายุ10 setAge(age: number) {11 // เช็คอายุก่อน ถ้าน้อยกว่าเท่ากับศูนย์หรือเกินหนึ่งร้อย คุณไม่ได้ไปต่อครับ12 if (age > 0 && age <= 100) this.age = age13 }14}1516const somchai = new Human('Somchai')17// Error: อย่ามาโกงอายุนะสมชาย18somchai.age = -9919// อายุไม่มากกว่าศูนย์เป็นไปไม่ได้ แก้ไขไม่ได้ครับ20somchai.setAge(-99)21// เยี่ยมอายุถูกต้องแล้ว22// แต่คุณชราภาพระดับสูงเลยหละ23somchai.setAge(99)
ระดับการเข้าถึงตัวสุดท้ายคือ protected ที่อนุญาตให้สมาชิกของคลาสเข้าถึงได้แค่จากตัวมันเอง และจากคลาสลูกของมัน
1class Human {2 protected name: string34 constructor(name: string) {5 this.name = name6 }78 printName() {9 // เข้าถึงจากภายในคลาสเองก็ได้10 console.log(this.name)11 }12}1314class Man {15 constructor(name: string) {16 super(name)17 }1819 ordain() {20 // เข้าถึงจากคลาสลูกก็ได้21 console.log(`${this.name} has already been a Buddhist monk!`)22 }23}2425const somchai = new Man('Somchai')26// แต่เข้าถึงจากภายนอกไม่ได้27somchai.name
Parameter properties
ในกรณีที่เรารับค่าผ่าน constructor เพื่อกำหนดค่านั้นให้เป็น property ของคลาส เราสามารถประกาศส่วนของ constructor ที่รับค่านั้นเข้ามา จากนั้นจึงกำหนดส่วนของ property ภายในคลาสอีกทีเพื่อเป็นตัวเก็บค่าที่รับเข้ามาผ่าน constructor
1class Human {2 private name: string34 constructor(name: string) {5 // รับ name เข้ามาแล้วตั้งค่าให้ name ที่เป็นสมาชิกของคลาสนี้6 this.name = name7 }8}
แค่นี้ก็ปาไป 8 บรรทัดแล้ว ทำใจลำบากเพราะโปรแกรมเมอร์อย่างเราเกิดมาพร้อมกับสกิลยาวไปไม่อ่าน TypeScript จึงอนุญาตให้เราระบุทั้ง modifier และชื่อของ property ใน constructor ซะเลย แล้วมันจะจัดการสร้าง property พร้อมกำหนดค่าให้อย่างสวยงาม
1class Human {2 // มีค่าเท่ากับตัวอย่างบนครับ3 constructor(private name: string) {}4}
Static Properties
เราใช้ new
operator เพื่อสร้างอ็อบเจ็กต์ (instance) จากคลาสจึงกล่าวได้ว่า property ทั้งหลายเป็นของอ็อบเจ็กต์
1class Human {2 constructor(private name: string) {}3}45// somchai เป็นอ็อบเจ็กต์ที่ name เป็น Somchai6const somchai = new Human('Somchai')7// ส่วน name ของ somsree ก็คือ Somsree8// name ทั้งสองไม่เกี่ยวข้องกันเลย9// จึงกล่าวได้ว่า name เป็นคุณสมบัติของอ็อบเจ็กต์10const somsree = new Human('Somsree')
แล้วถ้าเราอยากให้มี property ที่ถือว่าเป็นคุณสมบัติของคลาส ไม่ใช่ของอ็อบเจ็กต์หละ? นั่นหละครับคือสิ่งที่เราจะไปรู้จักกัน... static
1class Circle {2 // ค่า PI ในทุกวงกลมเหมือนกันหมด3 // เราจึงไม่ให้ค่านี้เป็นคุณสมบัติของอ็อบเจ็กต์4 // แต่ให้เป็นคุณสมบัติของคลาสแทนเลย5 static PI = 3.146}78// เพราะมันเป็นคุณสมบัติของคลาส9// เราจึงเข้าถึงได้โดยตรงจากคลาสโดยไม่ต้องผ่านการ new10Circle.PI
มีจุดที่น่าสนใจเกี่ยวกับการระบุชนิดข้อมูลนิดนึงครับ โดยปกติเมื่อเราสร้างอ็อบเจ็กต์จากคลาส เราจะระบุว่าอ็อบเจ็กต์นั้นมีชนิดข้อมูลเป็นอ็อบเจ็กต์ของคลาสอะไร
1class Circle {2 static PI = 3.143}45// เราใช้ circle1: Circle เพื่อบอก TypeScript ว่า6// circle1 ของเรามีชนิดข้อมูลเป็นอ็อบเจ็กต์ของคลาส Circle7const circle1: Circle = new Circle()
แล้วถ้าโจทย์ของเราเปลี่ยนไปหละ เราต้องการประกาศตัวแปรเพื่อเก็บค่าของคลาส Circle ไว้เลย? วิธีการคือเราจะใช้ typeof <คลาส> แทนครับ เพื่อบอกว่าตัวแปรนี้มีชนิดข้อมูลเป็นคลาสดังกล่าว ไม่ใช่มีชนิดข้อมูลเป็นอ็อบเจ็กต์ของคลาสนี้
1class Circle {2 static PI = 3.143}45// circleClass : Circle แบบนี้ผิด6// เพราะเป็นการบอกว่า circleClass มีชนิดข้อมูลเป็๋นอ็อบเจ็กต์ของคลาส Circle7const circleClass: Circle = Circle89// ต้องทำแบบนี้ circleClass : typeof Circle10// เป็นการบอกว่า circleClass ของเรามีชนิดข้อมูลเป็นคลาส Circle เองเลย11const circleClass: typeof Circle = Circle
Accessors
จากตัวอย่างก่อนหน้าเรามีคลาส Human ประกอบด้วย age ที่เป็น private และมีเมธอด setAge เพื่อตั้งค่าให้อายุ
1class Human {2 private age: number34 setAge(age: number) {5 if (age > 0 && age <= 100) this.age = age6 }7}89const somchai = new Human()1011// ด้วยวิธีนี้เมื่อเราจะตั้งค่าอายุเราต้องใช้12somchai.setAge(99)
ถ้าเราไม่อยากมีเมธอดที่ขึ้นต้นด้วย set หรือ get เพื่อเข้าถึงข้อมูลที่ปกปิดภายใน เราสามารถประกาศ getter และ setter แทนได้ดังนี้
1class Human {2 private _age: number34 get age(): number {5 return this._age6 }78 set age(age: number) {9 if (age > 0 && age <= 100) this.age = age10 }11}1213const somchai = new Human()14// เรียกใช้จากชื่อได้โดยตรง15somchai.age = 9916somchai.age
Abstract Classes
Abstract Classes คือคลาสที่ไม่สามารถสร้างอ็อบเจ็กต์หรืออินสแตนด์ของมันได้โดยตรงผ่าน new นั่นเป็นเพราะมันมักประกอบด้วย abstract method หรือเมธอดที่ไม่ได้นิยามการทำงานเอาไว้ เมื่อไม่นิยามการทำงานไว้แล้วจะสร้างมันขึ้นได้อย่างไร abstract class จึงใช้เป็นคลาสแม่เพื่อให้คลาสอื่นสืบทอดจากมันอีกที ตัวอย่างเช่น ถ้าเราบอกว่าบัญชีธนาคารมีสองประเภทคือบัญชีออมทรัพย์ (savings account) กับบัญชีฝากประจำ (fixed deposit account) ทั้งสองตัวนี้ล้วนสามารถถอนเงินได้ทั้งคู่ แต่บัญชีฝากประจำต้องฝากตั้งแต่ 6 เดือนขึ้นไปจึงถอนได้ เราสามารถออกแบบคลาสได้ดังนี้
1abstract class Account {2 // ให้ตั้งค่าเงินในบัญชีเริ่มต้นผ่าน constructor3 constructor(protected balance: number) {}45 // การฝากเงินไม่มีเงื่อนไขและเหมือนกันทั้งสองประเภทบัญชี6 // จึงแยกมาอยู่ในคลาสแม่7 deposit(amount: number) {8 this.balance += amount9 }1011 // แต่การถอนเงินนั้นแตกต่างออกไปในสองประเภทบัญชี12 // จึงเป็น abstract method รอคลาสลูกไปเพิ่มโค๊ด (implementation)13 abstract withdraw(amount: number): void14}1516class SavingsAccount extends Account {17 constructor(balance: number) {18 super(balance)19 }2021 withdraw(amount: number): void {22 // ถ้าถอนเงินออกไปแล้วบัญชีไม่ติดลบ จึงให้ถอนเงินได้23 if (this.balance - amount >= 0) {24 this.balance -= amount25 }26 }27}2829class FixedDepositAccount extends Account {30 private openDate: Date3132 constructor(balance: number) {33 super(balance)34 this.openDate = new Date()35 }3637 withdraw(amount: number): void {38 // ถ้าถอนเงินออกไปแล้วบัญชีติดลบ ไม่ต้องทำอะไร39 if (this.balance - amount < 0) return40 // ถ้าเปิดบัญชีไม่ครบ 6 เดือน ถอนไม่ได้เช่นกัน41 if (new Date().getMonth() - this.openDate.getMonth() < 6) return4243 this.balance -= amount44 }45}
TypeScript นั้นช่วยให้เราเขียนคลาสตามรูปแบบของ OOP ได้ง่ายขึ้น โปรแกรมเมอร์ที่มาจาก C# หรือ Java น่าจะคุ้นเคยกับวิธีการเหล่านี้ดี ส่วนตัวแล้วเฉยๆกับคลาสของ TypeScript ครับ เพราะส่วนตัวแล้วไม่ค่อยนิยมการสืบทอดซักเท่าไหร่ protected จึงไม่จำเป็นมากนัก ส่วน private ใน TypeScript นั้นชอบครับ ไม่ต้องไปทำ IIFE หรือใช้ WeakMap จำลอง private ขึ้นมาใน JavaScript อีกต่อไปแล้ว
สารบัญ
- เพิ่มชนิดข้อมูลให้คลาสใน ES2015
- การสืบทอดหรือ Inheritance
- Access Modifiers
- Parameter properties
- Static Properties
- Accessors
- Abstract Classes