Dart 103: มารู้จัก Class และ Object สไตล์ภาษาDartกันเถอะ
Tags: beginners, programming, dart, flutter
Published: 2020-03-01 17:39:13

---
canonical_url: https://www.tamemo.com/post/174/dart-103-oop/
---

Dart ถือเป็นภาษาสมัยใหม่ (ที่มีรูปแบบsyntaxแบบค่อนข้างเก่า🤔) ดังนั้นขาดไม่ได้เลยที่จะต้องมีฟีเจอร์ของ Object-Oriented Programming ใส่เข้ามาอย่างแน่นอน

> สิ่งที่บทความนี้จะโฟกัสคือการใช้ OOP ในภาษา Dart เท่านั้น แต่ไม่ได้มาสอน OOP ในบทความนี้นะ

> **📢 Note:** ภาษา Dart มีหลักๆ 2 เวอร์ชัน ในบทความนี้จะพูดถึง Dart version 2 เป็นหลัก

## Class and Object

การสร้าง **class** และ **object** ใน Dart ถือว่าไม่ค่อยมีอะไรแปลกจากภาษาทั่วไป โดยเฉพาะถ้าใครเคยเขียน Java มาก่อนจะเรียนรู้ได้ไม่ยากเลย

```dart
class People {
    int id;
    String name;

    void hello(){
        print('Hi, my name is $name');
    }
}
```
ส่วน object เวลาสร้างนั้นเราสามารถละคีย์เวิร์ด `new` ก็ได้ แบบนี้
```dart
var people = new People();  // จะใส่ new ก็ได้
var people = People();      // ตามมาตราฐานจะละ 

people.name = 'Ana';
people.hello();
```

**📢 Note:** หากเราสร้างตัวแปรแบบ local scope ที่มีชื่อเดียวกับ properties (แต่สำหรับDartจะเรียกว่า **field**) เช่น
```dart
class People {
    int id;     // <-- [id1]
    void foo(){
        int id; // <-- [id2]
        id = 1; // id ตรงนี้จะหากถึง [id2]
    }
}
```
เนื่องจากตัวแปรประเภท local scope หรือตัวแปรระดับฟังก์ชันจะถูกหาก่อนตัวแปรภายนอกที่เป็นของคลาสเสมอ วิธีแก้คือใช้คีย์เวิร์ด `this` ซึ่งหมายถึงตัว object เข้ามาช่วย
```dart
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)
```dart
class MyClass {
    int _privateField;
    int publicField;

    void _privateMethod() => 0
    void publicMethod() => 0
}
```
แล้วตามคอนเซ็ปของ OOP นั้นจะมีหลักการ encapsolution ข้อมูลภายใน โดยหากเราต้องการจะ access เข้าไปใช้ข้อมูลพวกนั้นเราจะต้องสร้าง method ที่เรียกว่า **getter** และ **setter** ขึ้นมา แบบนี้
```dart
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`
```dart
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`

และเนื่องจากมันมีรูปแบบการเขียนแบบฟังก์ชัน เราสามารถเพิ่มโค้ดอย่างอื่นลงไปอีกก็ได้ เช่น
```dart
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
```dart
class People {
    int _id;
    String _name;

    People(int id, String name){
        _id = id;
        _name = name;
    }
}

var people = People(1, 'Ana');
```
แต่ในกรณีแบบนี้ ถ้าคลาสเรามี field หลายตัวมากๆ เราจำเป็นต้องเขียนการกำหนดค่าเยอะมาก ดังนั้นDartเลยมี syntax แบบ่อซึ่งสั้นเป็นพิเศษ ซึ่งจะแปลงโค้ดจากข้างบนให้เหลือแค่
```dart
class People {
    int _id;
    String _name;

    People(this._id, this._name);
}

var people = People(1, 'Ana');
```
นั่นคือบอกไปเลยว่า ค่าที่ได้มาให้เอาไปใส่ใน field ที่กำหนดตรงๆ เลย (ต้องมี `this` นำหน้าตัวแปร)

หรือจะเอามาผสมกับ Named Parameter ก็ได้ เช่น
```dart
class People {
    int id;
    String name;

    People({this.id, this.name});
}

var people = People(id: 1, : 'Ana');
```
**⚠️ Warning!:** สำหรับ Named Paremeter นั้นไม่สามารถใช้กับ field แบบ private ได้ (พวกตัวแปรที่ขึ้นด้วย `_`) ในเคสนี้อาจจะต้องเขียนยาวหน่อยหากต้องการสร้าง field แบบ private
```dart
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 รึเปล่า ไปอ่านกันได้[ที่นี่](https://www.tamemo.com/post/156/fp-00-intro/)) เราควรจะใช้ตัวแปรแบบ **immutable** หรือเปลี่ยนแปลงค่าไม่ได้ เพื่อป้องกันการมี state ของโค้ดเยอะเกินไปจนเกิดบั๊ก!

เราก็เลยแก้โค้ดด้านบนอีกรอบเป็นแบบนี้
```dart
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"**

เพื่อความเข้าใจลองดูนี่
```dart
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 เลย แบบนี้
```dart
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** ขึ้นมาใช้งานได้
```dart
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 กลับไป 
```dart
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 ก็ได้ มาดูตัวอย่างกัน

```dart
//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 มาใช้งาน แบบนี้

```dart
//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` แทนได้แบบนี้
```dart
//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` แทน เช่น
```java
String string1 = "Centrillion Tech";
String string2 = "Centrillion Tech";
string1 == string2 //maybe true or false
string1.equals(string2) //true
```
สำหรับภาษา Dart นั้นจะใช้การสร้าง `equals` ขึ้นมาก็ได้ แต่โดยมาตราฐานของภาษาจะใช้การ override แบบพิเศษ นั่นคือ **"Operator Overriding"** แทน โดยใช้คีย์เวิร์ด `operator` นำหน้า
```dart
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 จะเกิดปัญกาขึ้นได้

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