Inheritance คืออะไร
บทความนี้เป็นบทความภาคต่อเกี่ยวกับ "ฟีเจอร์ของภาษาออบเจกต์" โดยในบทความที่แล้ว ("Encapsulation คืออะไร") ผมได้พูดถึงเรื่องของ Encapsulation ไป ในบทความนี้ผมจะมาพูดถึงฟีเจอร์ทางภาษาที่ชื่อว่า Inheritance พร้อมทั้งยกตัวอย่างด้วย Java เหมือนเช่นเคย เรื่องนี้เป็นเรื่องที่เข้าใจได้ง่ายกว่า Encapsulation และ Polymorphism นะครับ ตรงๆตัวไม่ได้ซับซ้อนอะไร จากการที่สอบถามหลายๆคนก็มักจะได้คำตอบว่าเข้าใจ ถามว่าแปลว่าอะไรก็ตอบได้เช่นกัน พอมันแปลได้แล้วความหมายมันใกล้เคียงกับสิ่งที่เราเรียน เราก็มักจะเข้าใจและจำได้ ถ้าเช่นนั้นคุณก็ลองตอบคำถามในใจก่อนอ่านนะว่า มันแปลว่าอะไรแล้วน่าจะเกี่ยวกับอะไรในการเขียนโปรแกรม ถ้าได้คำตอบแล้วก็ไปลุยกันเลย ดูว่ามันจะเหมือนกับที่คิดไว้หรือไม่
เตรียมความพร้อมกันก่อน
ก่อนอื่นเลยผมขอเริ่มต้นจากโค้ดตัวอย่างที่จำเป็นก่อนเข้าเรื่องของ Inheritance นะครับ โดยมีทั้งสิ้น 3 คลาสดัวยกันคือ i) Address.java ii) Employee.java และ iii) TestEmployee.java ดังแสดงในตัวอย่างที่ 1, 2, และ 3 ตามลำดับ และรูปที่ 1 แสดง UML Class ไดอะแกรมของโค้ด Address.java และ Employee.java คลาสเหล่านี้เป็นคลาสส่วนหนึ่งของระบบ Employee Management System (EMS)
package ems.model;
public class Address {
public String street;
public String city;
public String getAddressInfo() {
return street + ", " + city;
} // end of getAddressInfo()
} // end of class
package ems.model;
public class Employee {
public int id;
public String name;
public double salary;
public Address address = new Address();
public String getDetails() {
return (id + ", " + name + ", " + salary + ", "
+ address.getAddressInfo());
} // end of getDetails()
} // end of class
package ems.test;
import ems.model.Employee;
public class TestEmployee {
public static void main(String[] args) {
Employee emp1 = new Employee();
emp1.id = 1;
emp1.name = "James";
emp1.salary = 15000;
emp1.address.street = "Rama 3";
emp1.address.city = "Bangkok";
System.out.println(emp1.getDetails());
Employee emp2 = new Employee();
emp2.id = 2;
emp2.name = "Ann";
emp2.salary = 25000;
emp2.address.street = "Silom";
emp2.address.city = "Bangkok";
System.out.println(emp2.getDetails());
} // end of main()
} // end class
Inheritance
ในการเขียนโปรแกรม บ่อยครั้งที่เรามักเริ่มต้นออกแบบระบบจากคลาสที่มีบทบาทหน้าที่เป็น Model (หรือ Entity) ก่อน อาทิเช่นคลาส Employee
เป็นต้น และก็อีกบ่อยครั้งที่ต่อมาเราต้องการคลาสที่เป็น Specialized Version ของมัน อาทิเช่นคลาส Manager
รูปที่ 2 แสดงรายละเอียดของคลาสทั้งสองในรูปของไดอะแกรม จากรูปจะเห็นได้ว่าทั้ง Employee
และ Manager
มีคุณสมบัติที่เหมือนกันมาก คือมี id
, name
, salary
, และ address
เหมือนกัน แต่ Manager
มีความพิเศษตรงที่มีที่จอดรถประจำหรือ parkingNo
ด้วย ซึ่งจะว่าไปแล้วในความเป็นจริง Manager
ก็คือ Employee
คนหนึ่งนั่นแหละ แต่มีคุณสมบัติหรือลักษณะเฉพาะที่เจาะจงลงไปมากกว่าความเป็น Employee
ทั่วๆไป
แล้วทำไมเราจะต้องสร้างคลาส Manager ขึ้นมาใหม่ทั้งหมดทั้งๆที่คุณสมบัติหลายๆตัวมันก็ซ้ำเดิมกับที่มีใน Employee (ซึ่งในความเป็นจริงคุณสมบัติทั่วๆไปของ Employee มีมากกว่านี้อีกนะ) แล้วถ้าเรามีตำแหน่งงานต่างๆมากมายในระบบ เรามิต้องพิมพ์กันให้เหนื่อยหรือ ภาษาแบบออบเจกต์ (OOP) มีฟีเจอร์ที่ช่วยให้เรานำเอาสิ่งที่มีอยู่ก่อนแล้วมาใช้ให้เป็นประโยขน์ หรือพูดง่ายๆก็คือให้เราสามารถรับเอาแอททริบิวต์หรือเมธอดที่มีอยู่แล้วในคลาสอื่นมาใช้ในคลาสใหม่ได้ ดังนั้นเราจึงสามารถสร้างคลาส Manager
ที่ต่อยอดจากคลาส Employee
ได้ ดังแสดงในรูปที่ 3 ทำให้เราไม่ต้องมานั่งสร้าง id
, name
, salary
, address
และ getDetails()
อีกให้มันซ้ำซ้อน ซึ่งเราเรียกฟีเจอร์ทางภาษานี้ว่า Inheritance ครับ จากรูปที่ 3:
- เราเรียก
Employee
ว่าเป็น Super-class (บางภาษาเรียก Parent) - และเราเรียก
Manager
ว่าเป็น Sub-class (บางภาษาเรียก Child)
การทำ Inheritance นี้ยังก่อให้เกิดความสัมพันธ์แบบ Is-A Relationship ด้วย และเราสามารถพูดได้ว่า "A manager is-an employee" ครับ เพราะว่า Manager
มีคุณสมบัติครบทุกอย่างที่ Employee
มี แต่ Employee
ไม่จำเป็นต้องเป็น Manager
เสมอไปนะครับ อาจเป็นตำแหน่งงานอื่นๆก็ได้ (ส่วนความสัมพันธ์ระหว่าง Employee
กับ Address
เราเรียกว่าแบบ Has-A Relationship ซึ่งในกรณีนี้เราพูดได้ว่า "An employee has-an address" ครับ อันนี้แถมให้)
อ่านมาถึงตรงนี้คุณอาจจะยังนึกภาพไม่ออกว่าในทางปฏิบัติมันเป็นอย่างไร งั้นเรามาดูโค้ดกันเลย
public class Employee{
public int id;
. . .
}
public class Manager extends Employee{
public String parkingNo;
}
จากโค้ดข้างต้นจะเห็นได้ว่าคลาส Manager
นั้น extends
หรือต่อยอดออกมาจากคลาส Employee
ทำให้คลาส Employee
มีอะไร คลาส Manager
ก็มีตามนั้น นอกจากนี้ Manager
ยังมี parkingNo
ที่เป็นเฉพาะของตัวมันเองด้วย ใน Java เราทำ Inheritance ผ่านคีย์เวิร์ด extends
นะครับ (ภาษาอื่นๆอาจจะเลือกใช้คีย์เวิร์ดที่แตกต่างกันไปสำหรับการทำ Inheritance) เพียงเท่านี้เราก็ไม่ต้องมานั่งพิมพ์กันให้เมื่อยแล้วครับ :) ในตอนใช้งานเมื่อเราสร้าง Instance ของ Manager
ขึ้นมา เราก็จะได้แอททริบิวต์และเมธอดในคลาส Employee
มาใช้ด้วย ตัวอย่างเช่น
// TestEmployee.java
Manager emp3 = new Manager();
emp3.id = 3;
emp3.name = "Peter";
emp3.salary = 40000;
emp3.address.street = "Bangrak";
emp3.address.city = "Bangkok";
emp3.parkingNo = "4C-19";
System.out.println( emp3.getDetails() );
// 3, Peter, 40000.0, Bangrak, Bangkok
ถึงตรงนี้คุณอาจมีคำถามในใจว่า ทำไม่แอททริบิวต์ทั้งหมดใน Employee
ถึงได้เป็น public
ทั้งที่เราพึ่งเรียน Encapsulation มาในบทความที่แล้ว ทั้งนี้ทั้งนั้นเพราะแอททริบิวต์ private
ไม่สามารถอ้างถึงได้ใน Sub-class และก็ไม่ได้รับการถ่ายทอดมาใช้ใน Sub-class ด้วย (Instance ของ Manager
ก็จะมีเพียงแค่ parkingNo
เท่านั้น) อ้าว แล้วเราจะทำไงดี ถ้าอยากทำทั้ง Encapsulation และ Inheritance ด้วย? และนอกจากนี้เจ้าเมธอด getDetails()
ในตัวอย่างข้างต้นมันยังไม่แสดงผลข้อมูล parkingNo
อีกด้วย!!! ใช่ครับเพราะ getDetails()
ที่เรารับมาจาก Employee
มันไม่มีส่วนเกี่ยวข้องกับ parkingNo
เลย ไม่เป็นไรครับเดี๋ยวเราจะแก้ไขมันในหัวข้อถัดๆไป
ปล. เสริมสักนิดนึงนะครับว่า ในภาษาไทยเรามักใช้คำว่า "ถ่ายทอดคุณสมบัติ" ซึ่งก็ไม่ได้ผิดอะไรนะครับ หนังสือต่างประเทศก็ใช้คำว่า "Inherit" ในการอธิบายในหลายๆกรณีเช่นกัน แต่สิ่งที่ผมกำลังจะสื่อถึงก็คือ สัญลักษณ์ใน UML มันจะต้องชี้จาก Sub-class ไปสู่ Super-class นะครับ ถ้าตามรูปที่ 3 ก็คือเราต้องวาดให้ชี้ขึ้นไม่ใช่ชี้ลงเหมือนกับคำว่าถ่ายทอดคุณสมบัติลงมา (เรามักวาดผิดกันอยู่บ่อยๆ) สรุปท่องไว้นะครับว่า Extends from หรือขยายออกมาจาก เมื่อรู้แล้วจะวาดทแยงยังไงความหมายก็ยังถูกต้องครับ
Overriding Methods
นอกจากที่เราจะสามารถนำเอาเมธอดที่มีอยู่แล้วมาใช้ได้ใหม่ เรายังสามารถแก้ไขลอจิกในเมธอดให้สอดคล้องกับรูปแบบของคลาสใหม่ได้อีกด้วย และเราเรียกการทำเช่นนี้ว่า "Method Overriding" อย่างเช่นในตัวอย่างที่ผ่านมาเมธอด getDetails()
ไม่สามารถแสดงผล parkingNo
ได้ ดังนั้นเราจะมา Override เมธอดนี้ในคลาส Manager
กัน
public class Manager extends Employee{
public String parkingNo;
. . .
public String getDetails(){
return (id + ", " + name + ", " + salary + ", "
+ address.getAddressInfo() +
", [Parking No: " + parkingNo + "]");
} // end of getDetials()
} // end of class
โปรดสังเกตุนะครับว่าเรามีการอ้างถึง id
, name
, salary
, และ address
ในคลาส Manager
(นี่คือสาเหตุที่ทำให้เราไม่สามารถใช้ private
กับแอททริบิวต์เหล่านี้ได้) และเมื่อเรารัน TestEmployee ใหม่ เราก็จะเห็นผลลัพธ์ที่มีค่าของ parkingNo
แล้ว ทั้งนี้เพราะ Java Runtime จะตรวจสอบดูว่ามีเมธอด getDetails()
ในคลาส Manager
หรือไม่ ถ้าไม่มีก็จะไปเรียก getDetails()
ใน Super-class ให้เองครับ ทีนี้มาลองดูกฏการทำ Overriding กันบ้างนะ
- เมธอดต้องชื่อเหมือนเดิม
- เมธอดต้องมี Parameters เหมือนเดิม
- เมธอดต้องมี Return Type เหมือนเดิม (ตั้งแต่ Java 5 ขึ้นไป สามารถใช้ Return Type ที่เป็น Sub-class ของ Super-class Return Type ได้)
- Access Modifier จะต้องเหมือนเดิม หรือระดับการเข้าถึงต้องไม่ต่ำไปกว่านั้น
เรื่องของกฏนี้ผมขอไม่ลงลึกในรายละเอียดในบทความนี้นะครับ เอาเป็นว่าเมื่อไรก็ตามที่เราจะทำ Overriding เรามักจะใช้เมธอดที่มีโครงสร้าง (Method Signature) เหมือนเดิม (ชื่อน่ะบังคับว่าต้องเหมือนเดิมอยู่แล้ว) เพราะเราเพียงต้องการแค่การเปลี่ยนแปลงลอจิกเท่านั้น เรายังอยากได้เมธอดที่มีชื่อเหมือนเดิมอยู่ เอ แล้วทำไมเราไม่สร้างเป็นเมธอดใหม่ชื่อใหม่ไปเลยหล่ะ ในเมื่อเราก็เขียนมันขึ้นมาใหม่อยู่แล้ว มีเหตุผลอะไรที่เราจะต้องใช้ชื่อเดิมหรือทำ Overriding ด้วย? ทั้งนี้ทั้งนั้นก็เพื่อใช้กับฟีเจอร์ Polymorphism ต่อไปครับ
The protected
Modifier
กลับเข้ามาที่ประเด็นของ Modifier บ้าง เราอยากได้แอททริบิวต์ใน Super-class มาใช้ใน Sub-class และเราก็อยากปกป้องข้อมูลของเราจากการเข้าถึงได้โดยตรงของออบเจกต์อื่นๆด้วย ด้วยเหตุนี้ Modifier ตัวที่ชื่อว่า protected
จึงถือกำเนิดขึ้น นั่นก็คือแอททริบิวต์ชนิด protected
จะไม่สามารถเข้าถึงได้โดยตรงจากออบเจกต์อื่นๆ แต่เราสามารถใช้มันได้ภายใน Sub-class รูปที่ 4 แสดงคุณสมบัติของ แอททริบิวต์ protected
(*แอททริบิวต์ในรูปเป็นแอททริบิวต์ชนิด protected
นะครับ)
ทีนี้เราก็สามารถประยุกต์ใช้ Information Hiding และ Encapsulation พร้อมกับ Inheritance ได้แล้ว ว่าแล้วเราก็มา Refactor โค้ดของเรากัน รูปที่ 5 แสดงโค้ดที่ Refactor แล้วในลักษณะของไดอะแกรม โปรดสังเกตุว่าเครื่องหมาย #
คือสัญลักษณ์แทน protected
ทีนี้เรามาดูโค้ดใน TestEmployee.java กันบ้างเพื่อให้เข้าใจมากยิ่งขึ้น (ส่วนโค้ดเต็มๆดูได้ที่หัวข้อ Putting It All Together ครับ)
public static void main(String[] args) {
Employee emp1 = new Employee();
emp1.setId(1);
emp1.setName("James");
emp1.setSalary(15000);
emp1.getAddress().setStreet("Rama 3");
emp1.getAddress().setCity("Bangkok");
System.out.println( emp1.getDetails() );
. . .
Manager emp3 = new Manager();
emp3.setId(3);
emp3.setName("Peter");
emp3.setSalary(40000);
emp3.getAddress().setStreet("Bangrak");
emp3.getAddress().setCity("Bangkok");
emp3.setParkingNo("4C-19");
System.out.println( emp3.getDetails() );
} // end of main()
จากโค้ดข้างต้นเราจะเห็นได้ว่า TestEmployee
ซึ่งอยู่ภายนอกคลาส Employee
ไม่สามารถเข้าถึงแอททริบิวต์ชนิด protected
ได้โดยตรง แต่จะต้องเข้าถึงผ่านเมธอด get/set
เท่านั้น และแม้ว่าจะเข้าถึงผ่านคลาส Manager
ที่ได้รับถ่ายถอดแอททริบิวต์มา ก็ยังคงต้องกระทำผ่านเมธอด get/set
เท่านั้น แต่ภายในคลาส Manager
เราสามารถอ้างถึงแอททริบิวต์ชนิด protected
ได้
คุณอาจมีข้อสงสัยว่าถึงเราไม่ใช้ protected
เราก็ทำ Information Hiding และ Encapsulation (กับแอททริบิวต์ชนิด private
) พร้อมกับทำ Inheritance ได้ เพราะเราก็สามารถใช้เมธอด get/set
ซึ่งเป็น public
เพื่อเข้าถึงแอททริบิวต์ภายใน Sub-class ได้? คือแทนที่จะอ้างถึงแอททริบิวต์มันตรงๆภายใน Sub-class อาทิเช่น
public String getDetails(int x){
return (getId() + ", " + getName() + ", " + getSalary() + ", "
+ getAddress().getAddressInfo() +
", [Parking No: " + parkingNo + "]");
}
ถูกต้องครับ แต่ protected
มันยอมให้เราอ้างถึงตัวแปรได้เลย ซึ่งอาจสะดวกกว่าหรือจำเป็นในบางกรณีครับ ส่วนแบบไหนดีกว่า อันนี้ต้องไปถกกันเรื่องของการออกแบบแล้ว
ปล. ในรูปที่ 5 ข้างต้นนี้ผมเปลี่ยนลักษณะการวาดไดอะแกรมโดยตัดแอททริบิวต์ address
ออกจากคลาส Employee
และให้มีเส้นเชื่อมไปยังคลาส Address
แทน ซึ่งเป็นการวาดแบบ Association เพื่อแสดงให้เห็นถึง Has-A Relationship มากขึ้น ซึ่งของเดิมจะวาดในแบบ In-line นะครับ
The super
Keyword
ในเรื่องของ Inheritance นี้ เรายังมีอีกคีย์เวิร์ดหนึ่งที่ไม่พูดถึงไม่ได้ นั่นก็คือ super
ซึ่งเป็นคีย์เวิร์ดที่ใช้อ้างอิงถึง Super-class ลองมาดูตัวอย่าง getDetails()
ที่ประยุกต์ใช้คีย์เวิร์ดตัวนี้กัน
// from Manager.java
public String getDetails(){
return (super.getDetails() +
", [Parking No: " + this.parkingNo + "]");
}
เห็นมั้ยครับว่าทำให้โค้ดกระชับขึ้น สรุปนะครับ
- super.varName หมายถึงแอททริบิวต์ใน Super-class
- super.method() หมายถึงเมธอดใน Super-class
- super() หมายถึง Super-class Constructor