Dart 104: การสืบทอดคลาส Inheritance และ Mixin
Tags: beginners, programming, dart, flutter
Published: 2020-03-28 08:53:43

---
canonical_url: https://www.tamemo.com/post/175/dart-104-inheritance-and-mixin/
---

จากบทที่แล้วเราพูดถึงการสร้างคลาสในภาษา Dart ไปแล้ว แต่ยังไม่ได้พูดถึงหลักการสำคัญหนึ่งของ OOP นั่นคือการทำ Inheritance เลย ซึ่งจะมาต่อกันในบทนี้

> ในบทความนี้จะเน้นพูดถึงความแตกต่างหลังๆ ของการทำ inheritance ในภาษา Dart เป็นหลัก ไม่ได้เน้นสอนเรื่องการทำ class inheritance นะครับ

## Inheritance การสืบทอดคุณสมบัติจากคลาสสู่คลาส
เวลาเราสร้างคลาส ตามหลักการ OOP บอกเอาไว้ว่าให้ออกแบบในรูปของ Abstraction ซึ่งจะทำให้เราสามารถส่งต่อความสามารถของคลาสหนึ่ง ไปให้อีกคลาสหนึ่งได้ ถ้าพวกมันเป็นของชนิดเดียวกัน (เรามักใช้คำว่า **is-a**)

เช่น
```dart
class Animal {

    String name = 'สัตว์';

    void eat(){
        print('สัตว์กิน');
    }
}

var animal = Animal();
animal.eat();
```
เราสร้างคลาส Animal ขึ้นมา โดยมีความสามารถหลักๆ คือ eat ได้

ต่อมา ถ้าเราต้องการสร้างคลาสนกหรือ Bird ซึ่งนกเนี่ย ก็จัดว่าเป็นสัตว์ จะต้อง eat ได้แบบที่สัตว์ทำได้ 

ในกรณีนี้ เราจะไม่เขียนเมธอด eat ซ้ำลงไปในคลาส Bird (ไม่ copy-paste นั่นแหละนะ) แต่เราจะใช้วิธีที่เรียกว่า **Inheritance** หรือการ**สืบทอดคุณสมบัติ**แทน ด้วยคีย์เวิร์ด `extends`

```dart
class Bird extends Animal{

    @override
    String name = 'นก';

    void fly(){
        print('นกบิน');
    }
}

var animal = Animal();  
print(animal.name);     // สัตว์
animal.eat();           // สัตว์กิน
animal.fly();           // Compile Error!

var bird = Bird();      
print(bird.name);       // นก
bird.eat();             // สัตว์กิน
bird.fly();             // นกบิน
```
> เราเรียกคลาสที่ `extends` คลาสอื่นมาว่า **Child Class** ส่วนคลาสที่เป็นต้นฉบับจะเรียกว่า **Parent Class**
> 
> เช่นในเคสนี้ Animal=**Parent**, Bird=**Child**

สังเกตว่า Bird สามารถเรียกใช้เมธอดของ Animal ได้ทั้งหมด โดยที่ไม่ต้องเขียนเมธอดนั้นซ้ำลงในคลาส Bird เลย

แต่ถึงแม้ Bird จะเรียกใช้งานเมธอดของ Animal ได้ แต่ Animal ไม่สามารถเรียกใช้งานเมธอดที่เขียนเพิ่มในคลาส Bird ได้นะ

## Method & Properties Overriding

ในบางกรณี ถึงแม้Parentจะมีการเขียนทั้ง **Method** และ **Properties** ไปแล้ว แต่คลาสChildก็อาจจะไม่ได้อยากได้ค่าแบบนั้น เราสามารถเขียนค่าใหม่ทับลงไปได้ เรียกว่าการ **Overriding**

```dart
class Vehicle {

    String name = 'vihicle';

    void move(){
        print('$name move');
    }

    void addFuel(int fuel){}
}

class Airplane extends Vehicle {

    int _fuel = 0;

    @override
    String name = 'Airbus A380';

    @override
    void addFuel(int fuel){
        _fuel = fuel;
    }

    void fly(){
        print('$name fly');
    }
}
```

จากโค้ดข้างบน **Vehicle** มี
- Properties: `name`
- Method: `move()`, `addFuel()`

เราทำการ extends ไปเป็นคลาส **Airplane** แต่ต้องการจะกำหนด `name` ซะใหม่ แล้วก็ต้องการกำหนดว่าเครื่องบินจะ `move` ยังไงซะใหม่ด้วย

การสามารถเขียนค่าพวกนั้นซ้ำอีกรอบได้ เรียกว่าการ **Override** เมื่อเขียนทับไปแล้ว โค้ดจาก Parent จะไม่ถูกนำมาใช้กับคลาสนี้ ซึ่งส่วนใหญ่จะมีการเติม annotation `@override` เอาไว้ก่อนชื่อเพื่อบอกว่าค่านี้เรากำลังโอเวอร์ไรด์อยู่นะๆ

## เกิดอะไรขึ้น? เมื่อเราextendsมากกว่า1ครั้ง

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

เริ่มจากตัวอย่างแรกเบสิกๆ กันก่อน

ขอกำหนดให้เรามีคลาสอะไรสักอย่างอยู่คลาสหนึ่ง มีเมธอด `f1()` ซึ่งเรียก `f2()`, `f3()`, `f4()` ต่อกันเป็นทอดๆ แบบนี้

![](https://1.bp.blogspot.com/-wUpXcJpiiAk/XnzKGVBGExI/AAAAAAAASLI/Intx9T-g8iAh4uOA0rV6DFUEzjBwOhugQCNcBGAsYHQ/s1600/dart5-1.png)

> เนื่องจากมีฟังก์ชันเชื่อเดียวกันอยู่คนละคลาส เดี๋ยวจะสับสนกัน เลยขอกำหนดว่า เราจะเรียก `f1()` ในคลาส `A` ว่า `A::f1()` นะ

สำหรับคลาส A นั้น ไม่ยาก!

เมื่อเราเรียก `A::f1()` มันก็จะเรียกใช้ตามลำดับนี้

`A::f1()` -> `A::f2()` -> `A::f3()` -> `A::f4()`

![](https://1.bp.blogspot.com/-yexJeZS3NBQ/XnzKGZ8EIfI/AAAAAAAASLM/bKs6eqTXMLMIS_2Z5DwJR8Wgf6W5pYKgACNcBGAsYHQ/s1600/dart5-2.png)

ทีนี้มาดูคลาส B กันบ้าง

คลาส B นั้นมีการ override เมธอดทับลงไป 2 ตัวคือ `B::f2()`, `B::f4()` ตามคอนเซ็ป คลาสจะหาเมธอดในตัวมันเองก่อน ถ้าไม่เจอถึงจะขยับขึ้นไปดูที่ Parent ต่อ เลยได้ลำดับการเรียกใช้งานเมธอดตามภาพข้างบน

`A::f1()` -> `B::f2()` -> `A::f3()` -> `B::f4()`

![](https://1.bp.blogspot.com/-xA7WWk9CFGY/XnzKGYD8eVI/AAAAAAAASLE/BVLO6h6cdD8qabghnrsJ0FtJsRDoNrSCACNcBGAsYHQ/s1600/dart5-3.png)

หลักการคิดนี้ก็ใช้กับคลาส C ได้เช่นกัน นั่นคือไม่ว่าเมธอดนั้นจะถูกเรียกที่ Parent ชั้นไหนก็ตาม เวลาหาว่าเมธอดนี้จะเรียกใช้จากใคร จะเริ่มที่คลาสตัวเองเสมอ (หาไม่เจอ แล้วค่อยขยับขึ้นไปยัง Parent ทีละชั้นๆ)

`C::f1()` -> `B::f2()` -> `A::f3()` -> `C::f4()`

> สำหรับรายละเอียดเพิ่มเติม สามารถอ่านได้ในบทความที่เราเคยเขียนไว้ เกี่ยวกับ OOP ที่ [All About OOP – ตอนที่ 2 เจาะลึก Inheritance เมื่อคลาสมีผู้สืบทอดได้](https://www.tamemo.com/post/123/all-about-oop-2-inheritance/) ได้เลย!

## Abstract Class คลาสไม่สมบูรณ์

ถ้าเราดูตัวอย่างที่ผ่านมากับคลาส **Vehicle** เรา จะเห็นว่ามีเมธอดที่ชื่อ `addFuel(int)` ถูกประกาศเอาไว้ โดยไม่ได้เขียนอะไรเอาไว้เลย!

```dart
class Vehicle {
    void addFuel(int fuel){}
    ...
}
```

เหตุผลนั่นก็คือตอนกำหนดความสามารถให้ **"ยานพาหนะ"** นั้น เรายังไม่รู้ว่ายานพาหนะนั้นจะมีการเติมเชื้อเพลิงยังไง? เลยเว้นว่างๆ เอาไว้ก่อน เดี๋ยวก็มีคลาสลูก override ความสามารถนี้ทับไปเองแหละ

ในการเขียนโปรแกรมแบบ OOP เรามักจะเจอเหตุการณ์ประมาณนี้อยู่เสมอๆ (เพราะต้องออกแบบคลาสให้เป็น Abstraction ไง บางทีเลยรู้ว่า**ต้องทำอะไร**ก่อนที่จะรู้ว่าท**ทำยังไง**)

เรื่องนี้เลยเป็นที่มาของ **Abstract Class** นั่นเอง!

> Abstract Class จะเน้นเล่นกับเมธอด เพราะเป็นส่วนที่มีทั้ง abstraction และ body

```dart
abstract class Vehicle {
    void addFuel(int fuel); //ในเมื่อไม่รู้ ก็ไม่ต้องเขียนลงไป
    ...
}
```

แอบสเตร็กคลาสคือคลาสที่อนุญาตให้เราประกาศแค่ชื่อของเมธอดได้โดยไม่ต้องเขียน body แต่อย่างใด

**คำถาม?** ถ้าเราแค่ประกาศชื่อเมธอด ไม่ได้เขียนว่ามันจะรันยังไง แล้วเวลาสั่งรันมันจะทำงานได้ยังไงล่ะ?

**คำตอบ!** ไม่ได้ยังไงล่ะ!!

```dart
var calculator = Calculator();
//Compile Error: Abstract classes can't ne instantiated!
```
เรามันเป็นคลาสที่ไม่สมบูรณ์ เลยมีกฎว่า **ห้ามสร้าง object จาก Abstract Class นะ**

วิธีการใช้งานแอบสเตร็กคลาสเราจะต้องทำการ extends มันไปเป็นคลาสลูก แบบนี้

```dart
class Airplane extends Vehicle {
}
//Compile Error: Missing concrete implementations of 'Vehicle.addFuel'
```
ซึ่งหากเรา extends มาแล้วแต่ไม่ยอมเติมเมธอดที่หายไป มันจะแจ้งเออเรอร์ประมาณนี้ ทำให้เราต้องเขียนเมธอดเพิ่มลงไปนั่นเอง

```dart
class Airplane extends Vehicle {
    @override
    void addFuel(int fuel){
        ...
    }
    ...
}
```
> `class` ธรรมดาที่เราเขียนๆ กันเราจะเรียกมันว่า **Concrete Class** ซึ่งไม่สามารถมี Abstract Method ได้ จะต้องเขียนวิธีการรันทั้งหมดลงไป



## Implementation การเติมเต็มส่วนที่ขาดหาย

สำหรับภาษาอื่นๆ **interface** คือสิ่งที่คล้ายๆ กับคลาส แต่จะมีประกาศไว้แค่ abstraction โดยไม่มี body (เอาง่ายๆ มันคือแอบสเตร็กคลาสที่ทุกเมธอดเป็น abstract ทั้งหมด)

```java
// ตัวอย่างในภาษา Java
class MyClass {
    // method: abstraction with body
    void f(){
        print("hello world!");
    }
}

interface MyInterface {
    // method: only abstraction
    void f();
}
```

แต่ภาษา Dart นั้น**ไม่มี interface** แต่ถือว่า **"คลาสทุกคลาส สามารถเป็น interface ได้"**

การใช้งานอินเตอร์เฟซจะคล้ายๆ กับการ `extends` แต่เปลี่ยนไปใช้คีย์เวิร์ด `implements` แทน

```dart
class A {
    int f() => 1;
}

class B extends A {}
B().f() //1

class C implements A {}
//Compile Error: Missing concrete implementations of 'A.f'
```

สังเกตดูว่า แม้คลาส A จะเป็น Concrete Class ที่สมบูรณ์แล้ว แต่ถ้าเราสั่ง `implements` มันจะแจ้งประหนึ่งว่า `A::f()` ของเรานั้นไม่ได้เขียน body อะไรเอาไว้เลย

```dart
class C implements A {
    int f() => 2
}
```

อาจจะคิดว่า แล้วแบบนี้ `implements` มันจะมีประโยชน์อะไรน่ะ?

ในแง่การสร้างคลาสก็ไม่ค่อยจะมีประโยชน์อะไรหรอก แต่เราสามารถใช้งานมันในมุมของ Polymorphism แทน

> เหมือนเดิมนะ คือถ้าใครยังไม่รู้จัก **Polymorphism** อ่านต่อได้ที่ [All About OOP – ตอนที่ 3 Polymorphism หลากรูป หลายลักษณ์](https://www.tamemo.com/post/125/all-about-oop-3-polymorphism/)




## Mixin จับพวกมันมาผสมกัน!

ในภาษาสมัยใหม่ส่วนมาก เรามักจะไม่สามารถทำสิ่งที่เรียกว่า **Multiple Inheritance** ได้ (ตัวอย่างภาษาที่ทำได้คือ C++)

เหตุผลคือการที่เราทำการ extends จากหลายๆ คลาสจะเป็นอะไรที่สร้างความมึนงงเวลาเขียนโปรแกรมมาก และมันนำมาสู่การเกิดบั๊กยังไงล่ะ!

มาดูตัวอย่างกัน

```dart
class Bird extends Animal {
    ...
}

class Airplane extends Vehicle {
    ...
}
```

เรามีคลาส 2 คลาสคือ Bird และ Airplane ซึ่งทั้งสองนั้นมันสามารถบินได้ทั้งคู่เลย

เราก็เลยสร้างคลาสใหม่ ชื่อว่า **FlyObject** เอาไว้เป็นตัวกลาง หวังว่าจะส่งต่อความสามารถนี้ให้กับทั้ง Bird และ Airplane

```dart
class FlyObject {
    void fly(){ print('fly~'); }
}
```

แต่มันก็เกิดปัญหาขึ้นจนได้!

นั่นคือเราไม่สามารถ extends คลาสหลายๆ คลาสซ้อนกันได้

```dart
// Compile Error!
class Bird extends Animal, FlyObject {
    ...
}

// Compile Error!
class Airplane extends Vehicle, FlyObject {
    ...
}
```

### extends หลายคลาสแล้วเป็นอะไรเหรอไง?

![](https://1.bp.blogspot.com/-S7isS1zZ9f8/Xnzc-ekGgWI/AAAAAAAASLk/JJYhFckEGHwD8wF-hK19_V0eXU9fKtmawCNcBGAsYHQ/s1600/dart5-4.png)

การอนุญาตให้ extends หลายคลาส อาจจะทำให้เกิดเหตุการแบบนี้ คือคลาส Parent ทั้ง 2 คลาสดันมีเมธอดชื่อเดียวกัน!

```dart
class A {
    int f() => 1;
}

class B {
    int f() => 2;
}

class C extends A, B {} //(จริงๆ เคสนี้ต้อง Compile Error! นะ)

C().f() ??
```

ทีนี้เวลาคลาสลูกเรียกใช้ `f()` ก็ไม่รู้แล้ว ว่าจะให้ใช้ `f()` ของคลาส `A::f()` หรือ `B::f()`

แต่ในภาษา Dart มีฟีเจอร์ที่เรียกว่า **mixin** ("มิกซ์-อิน") เอาไว้แก้ปัญหานี้ได้

![](https://3.bp.blogspot.com/-lqoXqjTkKOk/XnzeA2UzMtI/AAAAAAAASL0/jHPbNxmWxgsKtEVx5QSEuAcm-Yoq9cqjACNcBGAsYHQ/s1600/dart5-5.png)

ในตัวอย่างเรามีคลาส **Airplane** กับ **Bird** ที่ทำการ extends มาจากคลาสอื่นเรียบร้อยไปแล้ว (แปลว่า extends เพิ่มอื่นไม่ได้แล้ว) 

แต่เราสามารถเปลี่ยนจากคีย์เวิร์ด `extends` ไปเป็น `with` ก็สามารถเพิ่มลงไปกี่ตัวก็ได้

```dart
class Vehicle {
    void move(){ ... }
}
class Animal {
    void eat(){ ... }
}
class FlyObject {
    void fly(){ ... }
}

class Airplane extends Vehicle with FlyObject {}
class Bird     extends Animal  with FlyObject {}

var airplane = Airplane();
airplane.move();
airplane.fly();

var bird = Bird();
bird.eat();
bird.fly();
```

หรือจะทำการ `with` จากหลายๆ ตัวก็ยังไง

```dart
class Parent1 { void f1(){ ... } }
class Parent2 { void f2(){ ... } }
class Parent3 { void f3(){ ... } }

class Child with Parent1, Parent2, Parent3 {
    ...
}

var child = Child();
child.f1();
child.f2();
child.f3();
```
แต่ก็ไม่ใช่ว่าไม่มีข้อจำกัดนะ นั่นคือคลาสที่จะนำมาสร้างเป็น Mixin นั้นจะต้อง **ไม่ extends คลาสอื่นมา**

```dart
class Parent1 { ... }
class Parent2 extends Parent1 { ... }

class Child with Parent1, Parent2 {}
//Compile Error: class 'Parent2' cannot be use as mixin because it extends from other class
```

เรื่องสุดท้ายที่จะพูดถึงเกี่ยวกับ Mixin คือจะเกิดอะไรขึ้น เมื่อเรา `with` จาก 2 คลาสที่มีเมธอดเดียวกัน
```dart
class A {
    String f() => 'from A';
}

class B {
    String f() => 'from B';
}

class AfollowByB with A, B {}
class BfollowByA with B, A {}

AfollowByB().f() // 'from B'
BfollowByA().f() // 'from A'
```
การจะทำแบบนี้ได้มีข้อจำกัด (อีกแล้ว) นั่นคือเมธอดทั้ง 2 ตัวจะต้องมี return-type และ parameters ที่เหมือนกันเป๊ะทุกอย่าง

ตัวคอมไพเลอร์ของ Dart จะเลือกเมธอดหลังเสมอ (อ่านจากซ้ายไปขวา ดังนั้น เมธอดด้านขวาจะ override ทับตัวทางซ้าย)

เช่นถ้าเราสั่ง `with A, B` เมธอด `B::f()` จะทับ `A::f()`

---

### สรุป
สำหรับภาษา Dart ก็มีฟีเจอร์ในการทำ inheritance เทียบเท่ากับภาษาสมัยใหม่ทั่วๆ ไปแต่อาจจะมีรูปแบบการเขียนต่างกันเล็กน้อย เช่นการไม่มี `interface` แต่ก็แทนที่ด้วยการที่เราสามารถ `implements` จากคลาสตรงๆ ได้เลย