Dart 103: มารู้จัก Class และ Object สไตล์ภาษาDartกันเถอะ

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 กัน

18 Total Views 3 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...