Dart ถือเป็นภาษาสมัยใหม่ (ที่มีรูปแบบsyntaxแบบค่อนข้างเก่า) ดังนั้นขาดไม่ได้เลยที่จะต้องมีฟีเจอร์ของ Object-Oriented Programming ใส่เข้ามาอย่างแน่นอน
สิ่งที่บทความนี้จะโฟกัสคือการใช้ OOP ในภาษา Dart เท่านั้น แต่ไม่ได้มาสอน OOP ในบทความนี้นะ
Note: ภาษา Dart มีหลักๆ 2 เวอร์ชัน ในบทความนี้จะพูดถึง Dart version 2 เป็นหลัก
Class and Object
การสร้าง class และ object ใน Dart ถือว่าไม่ค่อยมีอะไรแปลกจากภาษาทั่วไป โดยเฉพาะถ้าใครเคยเขียน Java มาก่อนจะเรียนรู้ได้ไม่ยากเลย
class People {
int id;
String name;
void hello(){
print('Hi, my name is $name');
}
}
ส่วน object เวลาสร้างนั้นเราสามารถละคีย์เวิร์ด new
ก็ได้ แบบนี้
var people = new People(); // จะใส่ new ก็ได้
var people = People(); // ตามมาตราฐานจะละ
people.name = 'Ana';
people.hello();
Note: หากเราสร้างตัวแปรแบบ local scope ที่มีชื่อเดียวกับ properties (แต่สำหรับDartจะเรียกว่า field) เช่น
class People {
int id; // <-- [id1]
void foo(){
int id; // <-- [id2]
id = 1; // id ตรงนี้จะหากถึง [id2]
}
}
เนื่องจากตัวแปรประเภท local scope หรือตัวแปรระดับฟังก์ชันจะถูกหาก่อนตัวแปรภายนอกที่เป็นของคลาสเสมอ วิธีแก้คือใช้คีย์เวิร์ด this
ซึ่งหมายถึงตัว object เข้ามาช่วย
class People {
int id; // <-- [id1]
void foo(){
int id; // <-- [id2]
this.id = 1; // id ตรงนี้จะหากถึง [id1]
}
}
private vs public
ในภาษา Dart ไม่มีคีย์เวิร์ด private
หรือ public
ที่เอาไว้กำหนดว่า field หรือ method นี้สามารถเรียกใช้งานจากภายนอกคลาสได้หรือเปล่า นั่นหมายความว่าค่าทุกค่าจะถูกกำหนดเป็น public ยกเว้นกรณีเดียวคือเราตั้งชื่อ field หรือ method นั้นให้ขึ้นต้นด้วย _
(underscore)
class MyClass {
int _privateField;
int publicField;
void _privateMethod() => 0
void publicMethod() => 0
}
แล้วตามคอนเซ็ปของ OOP นั้นจะมีหลักการ encapsolution ข้อมูลภายใน โดยหากเราต้องการจะ access เข้าไปใช้ข้อมูลพวกนั้นเราจะต้องสร้าง method ที่เรียกว่า getter และ setter ขึ้นมา แบบนี้
class People {
String _name;
void setName(String name) => _name = name;
String getName() => _name;
}
อย่างไรก็ตามล่ะ ... การสร้างทั้ง field แล้วเอา method มาครบมันอีกครั้งเป็นการเขียนสไตล์ของภาษา OOP ยุคเก่านิดนึง (เช่น Java )
สำหรับใน Dart จะมีสิ่งที่เรียกว่า...
Getter Setter fields
ทั้งสองตัวนี้คล้ายๆ กัน "getter setter method" จะต่างกันตรงที่ Getter Setter field ถือว่าเป็น field (ก็ตามชื่อมันละนะ)
field พวกนี้จะถือเป็นค่าพิเศษที่สร้างในรูปแบบของ method แต่ยังถือว่าเป็น field ด้วยคียเวิร์ด get
และ set
class People {
String _name;
// Getter
int get name => _name;
// Setter
void set name(String name) => _name = name;
// หรือแบบย่อ คือไม่ต้องใส่ void (Recommend)
set name(String name) => _name = name;
}
ข้อสังเกตคือการสร้าง Getter, Setter จะมีรูปแบบเหมือน method มากกว่า field แต่ในส่วนของ Getter นั้นจะถูกบังคับให้ไม่เขียน parameter หรือ ()
ที่ตามหลัง ส่วน Setter นั้นมีข้อแนะนำว่าไม่ต้องใส่ return-type void
และเนื่องจากมันมีรูปแบบการเขียนแบบฟังก์ชัน เราสามารถเพิ่มโค้ดอย่างอื่นลงไปอีกก็ได้ เช่น
class People {
String _name;
int get name {
print('call get name with value = $_name');
return _name;
}
set name(String name){
print('call set name with value = $name');
_name = name;
}
}
เมื่อทำแบบนี้ทุกครั้งเวลาเราเซ็ตค่าหรือขอค่า people.name
ก็จะมีการทำงาน print
เกิดขึ้นด้วย ซึ่งสามารถเอาไปประยุกต์ใช้งานกับอย่างอื่นได้อีกมากมายนะ
Constructor
คอนสตรักเตอร์คือการกำหนดค่าเริ่มต้น ตั้งแต่ครั้งแรกที่สร้าง object ขึ้นมาเลย ซึ่งในภาษา Dart สามารถทำได้หลายวิธีมากๆ
แต่แบบมาตราฐานจะเหมือนกับภาษา Java เป๊ะๆ นั่นคือสร้าง method ที่ไม่มี return-type และชื่อเหมือนกับชื่อคลาส
Initializing Field
class People {
int _id;
String _name;
People(int id, String name){
_id = id;
_name = name;
}
}
var people = People(1, 'Ana');
แต่ในกรณีแบบนี้ ถ้าคลาสเรามี field หลายตัวมากๆ เราจำเป็นต้องเขียนการกำหนดค่าเยอะมาก ดังนั้นDartเลยมี syntax แบบ่อซึ่งสั้นเป็นพิเศษ ซึ่งจะแปลงโค้ดจากข้างบนให้เหลือแค่
class People {
int _id;
String _name;
People(this._id, this._name);
}
var people = People(1, 'Ana');
นั่นคือบอกไปเลยว่า ค่าที่ได้มาให้เอาไปใส่ใน field ที่กำหนดตรงๆ เลย (ต้องมี this
นำหน้าตัวแปร)
หรือจะเอามาผสมกับ Named Parameter ก็ได้ เช่น
class People {
int id;
String name;
People({this.id, this.name});
}
var people = People(id: 1, : 'Ana');
⚠️ Warning!: สำหรับ Named Paremeter นั้นไม่สามารถใช้กับ field แบบ private ได้ (พวกตัวแปรที่ขึ้นด้วย _
) ในเคสนี้อาจจะต้องเขียนยาวหน่อยหากต้องการสร้าง field แบบ private
class People {
int _id;
String _name;
People({int id, String name}){
_id = id;
_name = name;
}
}
var people = People(id: 1, : 'Ana');
แต่! ก็ยังมีปัญหาอยู่ดี ถ้าเราประกาศตัวแปรภายนอกเป็น final
!!
Initializer
ตามหลักการเขียนโค้ดที่ดีสไตล์ Functional Programming (ใครยังไม่รู้จักว่า FP คืออะไร, ต่างกับ OOP รึเปล่า ไปอ่านกันได้ที่นี่) เราควรจะใช้ตัวแปรแบบ immutable หรือเปลี่ยนแปลงค่าไม่ได้ เพื่อป้องกันการมี state ของโค้ดเยอะเกินไปจนเกิดบั๊ก!
เราก็เลยแก้โค้ดด้านบนอีกรอบเป็นแบบนี้
class People {
final int _id;
final String _name;
People({int id, String name}){
_id = id;
_name = name;
}
}
และผลที่ได้ก็คือ Compile Error!! ด้วยสาเหตุว่า All final variables must be initialized, but _id and _name are not
ก็งงกันไปสิ!? ประกาศ final
แถมกำหนดค่าใน constructor ด้วยแต่ทำไมบอกว่าไม่ได้กำหนดค่า
"คำตอบคือ construction state ของ Dart นั้นจบก่อนจะทำงาน body ของ constructor"
เพื่อความเข้าใจลองดูนี่
class People {
final int _id;
final String _name;
// constructor header
People({int id, String name})
// constructor body
{
_id = id;
_name = name;
}
}
Dart ถือว่าถ้าต้องมีการ initialize หรือประกาศค่าที่จำเป็น (เช่นในกรณีของเรา คือการกำหนดค่าให้ตัวแปร final
) จะต้องทำให้เสร็จในส่วนของ header ก่อนที่ body จะเริ่มทำงาน
วิธีการแก้คือ ภาษาDartอนุญาตให้เรากำหนดค่าได้ในส่วน header เลย แบบนี้
class People {
final int _id;
final String _name;
People({int id, String name}) :
this._id = id,
this._name = name ;
}
หลัง header ให้เราใส่ :
ลงไป หลังจากนั้นจะเป็น statement ที่อนุญาตให้เรากำหนอค่า initialize ตัวแปรยังไงก็ได้ (และใช่! ถ้าคุณคิดว่า syntax มันแปลกๆ ก็อาจจะต้องลองใช้ซักพักกว่าจะชิน)
Named constructors
ภาษา Dart ไม่สามารถทำ Method Overloading หรือการสร้างเมธอดหลายตัวแต่ต่าง parameter กันได้ (ตัวอย่างภาษาที่ทำได้คือ Java) เพราะDartมี Optional Parameter ให้ใช้งานแทน
แต่สำหรับคอนสตรักเตอร์ถ้าเราต้องการสร้าง constructor หลายๆ ตัวเอาไว้สำหรับการสร้างวัตถุหลายแบบ เราสามารถสร้างสิ่งที่เรียกว่า Named constructors ขึ้นมาใช้งานได้
class Point {
int x, y;
Point(this.x, this.y);
Point.origin() {
x = 0;
y = 0;
}
}
var point1 = Point(10, 20);
var point0 = Point.origin();
เช่นตัวอย่างนี้เราสร้าง origin
ขึ้นมาเป็น constructor อีกหนึ่งตัว เวลาจะสร้าง object ก็สามารถเลือกได้ว่าจะใช้ default constructor แบบธรรมดาหรือใช้ origin
ที่เราสร้างขึ้นมาเองก็ได้
Factory constructors
นอกจากเราจะสร้าง object จากการใช้ constructorแบบธรรมดาได้แล้ว ในภาษานี้ยังมีวิธีสร้าง object อีกแบบนั่นคือการใช้ factory
ซึ่งใช้หลักการเดียวกับ Factory Pattern นั่นคือทำยังไงก็ได้ให้ตอบ object กลับไป
class Logger{
String name;
factory Logger(){
return Logger.withName('MyLogger');
}
Logger.withName(this.name);
void log(String message){
print('LOG: $message');
}
}
var logger1 = Logger();
var logger2 = Logger.withName('logger-2');
จะเห็นว่า factory
นั้นทำงานเหมือนกับ constructor ทุกอย่างนั่นคือกำหนดและสร้าง object แต่ข้อแตกต่างคือ constructor จะกำหนดค่าให้ตัวเอง (เพราะตัว constructor ถือเป็นเมธอดตัวหนึ่งของ object อยู่แล้ว) แต่ว่า factory จะเป็นการสร้าง object ชิ้นใหม่ขึ้นมาเลย
⚠️ Warning!: สำหรับ constructor นั้นสามารถเรียกใช้ this
ได้เพราะถือว่าเป็นส่วนหนึ่งของ object อยู่แล้ว แต่ factory นั้นจะไม่สามารถเรียกใช้ this
ได้เลย
อาจจะประยุกต์เอาไปใช้กับ Singleton Pattern ก็ได้ มาดูตัวอย่างกัน
//logger.dart
class Logger{
void log(String message){
print('LOG: $message');
}
}
//module1.dart
var logger = Logger();
logger.log('log from module 1');
//module2.dart
var logger = Logger();
logger.log('log from module 2');
สมมุติให้เรามีคลาส Logger (หน้าที่คือเอาไว้ปริ๊น log ข้อมูลนั่นแหละนะ) ปัญหาคือถ้าเราต้องการเอา Logger ไปใช้หลายไฟล์เราจะต้องสร้าง object ใหม่ทุกรอบ
ในกรณีนี้ ถ้าเราต้องการให้ Logger มี instance ได้แค่ตัวเดียวเท่านั้น เราอาจจะนำ Single Pattern มาใช้งาน แบบนี้
//logger.dart
class Logger{
static Logger _instance;
static Logger getInstance() => _instance ??= Logger();
void log(String message){
print('LOG: $message');
}
}
//module1.dart
var logger = Logger.getInstance();
logger.log('log from module 1');
//module2.dart
var logger = Logger.getInstance();
logger.log('log from module 2');
ซึ่งก็สามารถทำได้ ไม่ผิดอะไร แต่เราสามารถเปลี่ยนไปใช้ factory
แทนได้แบบนี้
//logger.dart
class Logger{
static Logger _instance;
factory Logger() => _instance ??= Logger();
void log(String message){
print('LOG: $message');
}
}
//module1.dart
var logger = Logger();
logger.log('log from module 1');
//module2.dart
var logger = Logger();
logger.log('log from module 2');
สังเกตว่าคำสั่ง factory
นั้นจะทำงานเหมือนกับ constructor แบบในกรณีนี้ ในmodule1และmodule2นั้นจะไม่รู้เลยว่าการที่เขาสร้าง object ขึ้นมานั้นจริงๆ ไม่ได้เป็นการสร้างใหม่ แต่เป็นการเรียกใช้จาก shared instance นั่นเอง
equals และการทำ Operator Overriding
สำหรับ object ในภาษาโปรแกรมส่วนใหญ่จะเก็บค่าแบบ reference (คือเก็บเป็น pointer ไม่ได้เก็บเป็น value) การจะเช็กว่า object เท่ากันไหมเลยไม่สามารถใช้ ==
ได้
เช่นถ้าเป็นภาษา Java ก็จะต้องใช้ equals
แทน เช่น
String string1 = "Centrillion Tech";
String string2 = "Centrillion Tech";
string1 == string2 //maybe true or false
string1.equals(string2) //true
สำหรับภาษา Dart นั้นจะใช้การสร้าง equals
ขึ้นมาก็ได้ แต่โดยมาตราฐานของภาษาจะใช้การ override แบบพิเศษ นั่นคือ "Operator Overriding" แทน โดยใช้คีย์เวิร์ด operator
นำหน้า
class People {
int id;
String name;
//แบบสร้าง equals
bool equals(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.id == id &&
person.name == name);
}
//แบบ override ==
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.id == id &&
person.name == name);
}
}
ซึ่งพอเรา override ==
ไปแล้วเราสามารถจับ object มาเช็กว่ามันเท่ากันรึเปล่าโดยใช้ ==
ตรงๆ ได้เลย
⚠️ Warning!: ตามหลักการของภาษาที่ใช้โครงสร้าง OOP แนวเดียวกับภาษา Java ทุกครั้งที่เราเขียน equals เราจะโดยบังคับให้เขียน
hashCode
ด้วยเสมอ ไม่งั้นเวลา object ไปใช้งานใน HashMap จะเกิดปัญกาขึ้นได้
class People {
int id;
String name;
@override
int get hashCode {
int result = 17;
result = 37 * result + id.hashCode;
result = 37 * result + name.hashCode;
return result;
}
//แบบ override ==
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.id == id &&
person.name == name);
}
}
เอาจริงๆ คือถ้าต้องมาเขียนทั้ง equals
และ hashCode
เองทั้งหมดทุกคลาสน่ะ ไม่ไหวแน่ๆ ... ดังนั้นเราขอแนะนำให้คุณ generate code ด้วยฟีเจอร์ของ IDE แทนจะดีกว่านะ
เนื้อหา Class-Object ยังมีอีกมาก แต่ในเนื้อหาบทความนี้เลือกมาเฉพาะที่เห็นว่าแตกต่างจากภาษาอื่นๆ ในบทต่อไปเราจะมาพูดกันเรื่องการทำ Inheritance และ Abstract Class กัน