จากบทที่แล้วเราพูดถึงการสร้างคลาสในภาษา Dart ไปแล้ว แต่ยังไม่ได้พูดถึงหลักการสำคัญหนึ่งของ OOP นั่นคือการทำ Inheritance เลย ซึ่งจะมาต่อกันในบทนี้
ในบทความนี้จะเน้นพูดถึงความแตกต่างหลังๆ ของการทำ inheritance ในภาษา Dart เป็นหลัก ไม่ได้เน้นสอนเรื่องการทำ class inheritance นะครับ
Inheritance การสืบทอดคุณสมบัติจากคลาสสู่คลาส
เวลาเราสร้างคลาส ตามหลักการ OOP บอกเอาไว้ว่าให้ออกแบบในรูปของ Abstraction ซึ่งจะทำให้เราสามารถส่งต่อความสามารถของคลาสหนึ่ง ไปให้อีกคลาสหนึ่งได้ ถ้าพวกมันเป็นของชนิดเดียวกัน (เรามักใช้คำว่า is-a)
เช่น
class Animal {
String name = 'สัตว์';
void eat(){
print('สัตว์กิน');
}
}
var animal = Animal();
animal.eat();
เราสร้างคลาส 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(); // นกบิน
เราเรียกคลาสที่
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');
}
}
จากโค้ดข้างบน 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){}
...
}
เหตุผลนั่นก็คือตอนกำหนดความสามารถให้ "ยานพาหนะ" นั้น เรายังไม่รู้ว่ายานพาหนะนั้นจะมีการเติมเชื้อเพลิงยังไง? เลยเว้นว่างๆ เอาไว้ก่อน เดี๋ยวก็มีคลาสลูก override ความสามารถนี้ทับไปเองแหละ
ในการเขียนโปรแกรมแบบ OOP เรามักจะเจอเหตุการณ์ประมาณนี้อยู่เสมอๆ (เพราะต้องออกแบบคลาสให้เป็น Abstraction ไง บางทีเลยรู้ว่าต้องทำอะไรก่อนที่จะรู้ว่าททำยังไง)
เรื่องนี้เลยเป็นที่มาของ Abstract Class นั่นเอง!
Abstract Class จะเน้นเล่นกับเมธอด เพราะเป็นส่วนที่มีทั้ง abstraction และ body
abstract class Vehicle {
void addFuel(int fuel); //ในเมื่อไม่รู้ ก็ไม่ต้องเขียนลงไป
...
}
แอบสเตร็กคลาสคือคลาสที่อนุญาตให้เราประกาศแค่ชื่อของเมธอดได้โดยไม่ต้องเขียน body แต่อย่างใด
คำถาม? ถ้าเราแค่ประกาศชื่อเมธอด ไม่ได้เขียนว่ามันจะรันยังไง แล้วเวลาสั่งรันมันจะทำงานได้ยังไงล่ะ?
คำตอบ! ไม่ได้ยังไงล่ะ!!
var calculator = Calculator();
//Compile Error: Abstract classes can't ne instantiated!
เรามันเป็นคลาสที่ไม่สมบูรณ์ เลยมีกฎว่า ห้ามสร้าง object จาก Abstract Class นะ
วิธีการใช้งานแอบสเตร็กคลาสเราจะต้องทำการ extends มันไปเป็นคลาสลูก แบบนี้
class Airplane extends Vehicle {
}
//Compile Error: Missing concrete implementations of 'Vehicle.addFuel'
ซึ่งหากเรา extends มาแล้วแต่ไม่ยอมเติมเมธอดที่หายไป มันจะแจ้งเออเรอร์ประมาณนี้ ทำให้เราต้องเขียนเมธอดเพิ่มลงไปนั่นเอง
class Airplane extends Vehicle {
@override
void addFuel(int fuel){
...
}
...
}
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();
}
แต่ภาษา 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'
สังเกตดูว่า แม้คลาส A จะเป็น Concrete Class ที่สมบูรณ์แล้ว แต่ถ้าเราสั่ง implements
มันจะแจ้งประหนึ่งว่า A::f()
ของเรานั้นไม่ได้เขียน body อะไรเอาไว้เลย
class C implements A {
int f() => 2
}
อาจจะคิดว่า แล้วแบบนี้ implements
มันจะมีประโยชน์อะไรน่ะ?
ในแง่การสร้างคลาสก็ไม่ค่อยจะมีประโยชน์อะไรหรอก แต่เราสามารถใช้งานมันในมุมของ Polymorphism แทน
เหมือนเดิมนะ คือถ้าใครยังไม่รู้จัก Polymorphism อ่านต่อได้ที่ All About OOP – ตอนที่ 3 Polymorphism หลากรูป หลายลักษณ์
Mixin จับพวกมันมาผสมกัน!
ในภาษาสมัยใหม่ส่วนมาก เรามักจะไม่สามารถทำสิ่งที่เรียกว่า Multiple Inheritance ได้ (ตัวอย่างภาษาที่ทำได้คือ C++)
เหตุผลคือการที่เราทำการ extends จากหลายๆ คลาสจะเป็นอะไรที่สร้างความมึนงงเวลาเขียนโปรแกรมมาก และมันนำมาสู่การเกิดบั๊กยังไงล่ะ!
มาดูตัวอย่างกัน
class Bird extends Animal {
...
}
class Airplane extends Vehicle {
...
}
เรามีคลาส 2 คลาสคือ Bird และ Airplane ซึ่งทั้งสองนั้นมันสามารถบินได้ทั้งคู่เลย
เราก็เลยสร้างคลาสใหม่ ชื่อว่า FlyObject เอาไว้เป็นตัวกลาง หวังว่าจะส่งต่อความสามารถนี้ให้กับทั้ง Bird และ Airplane
class FlyObject {
void fly(){ print('fly~'); }
}
แต่มันก็เกิดปัญหาขึ้นจนได้!
นั่นคือเราไม่สามารถ extends คลาสหลายๆ คลาสซ้อนกันได้
// Compile Error!
class Bird extends Animal, FlyObject {
...
}
// Compile Error!
class Airplane extends Vehicle, FlyObject {
...
}
extends หลายคลาสแล้วเป็นอะไรเหรอไง?
การอนุญาตให้ extends หลายคลาส อาจจะทำให้เกิดเหตุการแบบนี้ คือคลาส Parent ทั้ง 2 คลาสดันมีเมธอดชื่อเดียวกัน!
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 ("มิกซ์-อิน") เอาไว้แก้ปัญหานี้ได้
ในตัวอย่างเรามีคลาส 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();
หรือจะทำการ 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();
แต่ก็ไม่ใช่ว่าไม่มีข้อจำกัดนะ นั่นคือคลาสที่จะนำมาสร้างเป็น 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
เรื่องสุดท้ายที่จะพูดถึงเกี่ยวกับ 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'
การจะทำแบบนี้ได้มีข้อจำกัด (อีกแล้ว) นั่นคือเมธอดทั้ง 2 ตัวจะต้องมี return-type และ parameters ที่เหมือนกันเป๊ะทุกอย่าง
ตัวคอมไพเลอร์ของ Dart จะเลือกเมธอดหลังเสมอ (อ่านจากซ้ายไปขวา ดังนั้น เมธอดด้านขวาจะ override ทับตัวทางซ้าย)
เช่นถ้าเราสั่ง with A, B
เมธอด B::f()
จะทับ A::f()
สรุป
สำหรับภาษา Dart ก็มีฟีเจอร์ในการทำ inheritance เทียบเท่ากับภาษาสมัยใหม่ทั่วๆ ไปแต่อาจจะมีรูปแบบการเขียนต่างกันเล็กน้อย เช่นการไม่มี interface
แต่ก็แทนที่ด้วยการที่เราสามารถ implements
จากคลาสตรงๆ ได้เลย
Top comments (0)