DEV Community

Ta for tamemo

Posted on • Edited on • Originally published at tamemo.com

Dart 104: การสืบทอดคลาส Inheritance และ Mixin

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

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

Inheritance การสืบทอดคุณสมบัติจากคลาสสู่คลาส

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

เช่น

class Animal {

    String name = 'สัตว์';

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

var animal = Animal();
animal.eat();
Enter fullscreen mode Exit fullscreen mode

เราสร้างคลาส Animal ขึ้นมา โดยมีความสามารถหลักๆ คือ eat ได้

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

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

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();             // นกบิน
Enter fullscreen mode Exit fullscreen mode

เราเรียกคลาสที่ extends คลาสอื่นมาว่า Child Class ส่วนคลาสที่เป็นต้นฉบับจะเรียกว่า Parent Class

เช่นในเคสนี้ Animal=Parent, Bird=Child

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

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

Method & Properties Overriding

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

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

จากโค้ดข้างบน Vehicle มี

  • Properties: name
  • Method: move(), addFuel()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

สำหรับรายละเอียดเพิ่มเติม สามารถอ่านได้ในบทความที่เราเคยเขียนไว้ เกี่ยวกับ OOP ที่ All About OOP – ตอนที่ 2 เจาะลึก Inheritance เมื่อคลาสมีผู้สืบทอดได้ ได้เลย!

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

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

class Vehicle {
    void addFuel(int fuel){}
    ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

var calculator = Calculator();
//Compile Error: Abstract classes can't ne instantiated!
Enter fullscreen mode Exit fullscreen mode

เรามันเป็นคลาสที่ไม่สมบูรณ์ เลยมีกฎว่า ห้ามสร้าง object จาก Abstract Class นะ

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

class Airplane extends Vehicle {
}
//Compile Error: Missing concrete implementations of 'Vehicle.addFuel'
Enter fullscreen mode Exit fullscreen mode

ซึ่งหากเรา extends มาแล้วแต่ไม่ยอมเติมเมธอดที่หายไป มันจะแจ้งเออเรอร์ประมาณนี้ ทำให้เราต้องเขียนเมธอดเพิ่มลงไปนั่นเอง

class Airplane extends Vehicle {
    @override
    void addFuel(int fuel){
        ...
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

class ธรรมดาที่เราเขียนๆ กันเราจะเรียกมันว่า Concrete Class ซึ่งไม่สามารถมี Abstract Method ได้ จะต้องเขียนวิธีการรันทั้งหมดลงไป

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

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

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

interface MyInterface {
    // method: only abstraction
    void f();
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

class C implements A {}
//Compile Error: Missing concrete implementations of 'A.f'
Enter fullscreen mode Exit fullscreen mode

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

class C implements A {
    int f() => 2
}
Enter fullscreen mode Exit fullscreen mode

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

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

เหมือนเดิมนะ คือถ้าใครยังไม่รู้จัก Polymorphism อ่านต่อได้ที่ All About OOP – ตอนที่ 3 Polymorphism หลากรูป หลายลักษณ์

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

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

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

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

class Bird extends Animal {
    ...
}

class Airplane extends Vehicle {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

class FlyObject {
    void fly(){ print('fly~'); }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

// Compile Error!
class Airplane extends Vehicle, FlyObject {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

C().f() ??
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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();
Enter fullscreen mode Exit fullscreen mode

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

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();
Enter fullscreen mode Exit fullscreen mode

แต่ก็ไม่ใช่ว่าไม่มีข้อจำกัดนะ นั่นคือคลาสที่จะนำมาสร้างเป็น Mixin นั้นจะต้อง ไม่ extends คลาสอื่นมา

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
Enter fullscreen mode Exit fullscreen mode

เรื่องสุดท้ายที่จะพูดถึงเกี่ยวกับ Mixin คือจะเกิดอะไรขึ้น เมื่อเรา with จาก 2 คลาสที่มีเมธอดเดียวกัน

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'
Enter fullscreen mode Exit fullscreen mode

การจะทำแบบนี้ได้มีข้อจำกัด (อีกแล้ว) นั่นคือเมธอดทั้ง 2 ตัวจะต้องมี return-type และ parameters ที่เหมือนกันเป๊ะทุกอย่าง

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

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


สรุป

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

Top comments (0)