Training

Inheritance คืออะไร

25 Jun 2013

บทความนี้เป็นบทความภาคต่อเกี่ยวกับ "ฟีเจอร์ของภาษาออบเจกต์" โดยในบทความที่แล้ว ("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)

ตัวอย่างที่ 1: Address.java
package ems.model;

public class Address {

  public String street;
  public String city;

  public String getAddressInfo() {
    return street + ", " + city;
  } // end of getAddressInfo()

} // end of class
ตัวอย่างที่ 2: Employee.java
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
ตัวอย่างที่ 3: TestEmployee.java
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
รูปที่ 1: Class ไดอะแกรมของ Address.java และ Employee.java

Inheritance

ในการเขียนโปรแกรม บ่อยครั้งที่เรามักเริ่มต้นออกแบบระบบจากคลาสที่มีบทบาทหน้าที่เป็น Model (หรือ Entity) ก่อน อาทิเช่นคลาส Employee เป็นต้น และก็อีกบ่อยครั้งที่ต่อมาเราต้องการคลาสที่เป็น Specialized Version ของมัน อาทิเช่นคลาส Manager รูปที่ 2 แสดงรายละเอียดของคลาสทั้งสองในรูปของไดอะแกรม จากรูปจะเห็นได้ว่าทั้ง Employee และ Manager มีคุณสมบัติที่เหมือนกันมาก คือมี id, name, salary, และ address เหมือนกัน แต่ Manager มีความพิเศษตรงที่มีที่จอดรถประจำหรือ parkingNo ด้วย ซึ่งจะว่าไปแล้วในความเป็นจริง Manager ก็คือ Employee คนหนึ่งนั่นแหละ แต่มีคุณสมบัติหรือลักษณะเฉพาะที่เจาะจงลงไปมากกว่าความเป็น Employee ทั่วๆไป

รูปที่ 2: Class ไดอะแกรมของ `Employee` และ `Manager`

แล้วทำไมเราจะต้องสร้างคลาส Manager ขึ้นมาใหม่ทั้งหมดทั้งๆที่คุณสมบัติหลายๆตัวมันก็ซ้ำเดิมกับที่มีใน Employee (ซึ่งในความเป็นจริงคุณสมบัติทั่วๆไปของ Employee มีมากกว่านี้อีกนะ) แล้วถ้าเรามีตำแหน่งงานต่างๆมากมายในระบบ เรามิต้องพิมพ์กันให้เหนื่อยหรือ ภาษาแบบออบเจกต์ (OOP) มีฟีเจอร์ที่ช่วยให้เรานำเอาสิ่งที่มีอยู่ก่อนแล้วมาใช้ให้เป็นประโยขน์ หรือพูดง่ายๆก็คือให้เราสามารถรับเอาแอททริบิวต์หรือเมธอดที่มีอยู่แล้วในคลาสอื่นมาใช้ในคลาสใหม่ได้ ดังนั้นเราจึงสามารถสร้างคลาส Manager ที่ต่อยอดจากคลาส Employee ได้ ดังแสดงในรูปที่ 3 ทำให้เราไม่ต้องมานั่งสร้าง id, name, salary, address และ getDetails() อีกให้มันซ้ำซ้อน ซึ่งเราเรียกฟีเจอร์ทางภาษานี้ว่า Inheritance ครับ จากรูปที่ 3:

  • เราเรียก Employee ว่าเป็น Super-class (บางภาษาเรียก Parent)
  • และเราเรียก Manager ว่าเป็น Sub-class (บางภาษาเรียก Child)
รูปที่ 3: Inheritance

การทำ 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 นะครับ)

รูปที่ 4: ไดอะแกรมแสดงคุณสมบัติของแอททริบิวต์ `protected`

ทีนี้เราก็สามารถประยุกต์ใช้ Information Hiding และ Encapsulation พร้อมกับ Inheritance ได้แล้ว ว่าแล้วเราก็มา Refactor โค้ดของเรากัน รูปที่ 5 แสดงโค้ดที่ Refactor แล้วในลักษณะของไดอะแกรม โปรดสังเกตุว่าเครื่องหมาย # คือสัญลักษณ์แทน protected

รูปที่ 5: ไดอะแกรมในเวอร์ชันของ 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

Books By Me

JavaScript Programming Guide book cover

JavaScript Programming Guide
Thai language
Kontentblue Publishing

About This Site

I use this site to share my knowledge in form of articles. I also use it as an experimental space, trying and testing things. If you have any problems viewing this site, please tell me.

→ More about me

→ Contact me

→ Me on facebook

Creative Commons Attribution License

creative commons logo

This license lets you distribute, remix, tweak my articles, even commercially, as long as you credit me for the original creation.

ด้วยสัญญาอนุญาตินี้ คุณสามารถเผยแพร่ ดัดแปลง แก้ไขและนำบทความของผมไปใช้ แม้ในเชิงธุรกิจ ตราบใดที่คุณได้อ้างอิงกลับมาและให้เครดิตกับผม