All About OOP – ตอนที่ 2 เจาะลึก Inheritance เมื่อคลาสมีผู้สืบทอดได้

อ่านตอนก่อนหน้านี้ได้ที่

developer

บทความชุด: Object Oriented Programming (All About OOP)

รวมบทความเจาะลึกเกี่ยวกับการเขียนโปรแกรมเชิงวัตถุ ตั้งแต่แนวคิด วิธีการใช้งาน ตัวอย่างและหลักการใช้ต่างๆ เช่น S.O.L.I.D หรือการใช้ Design Pattern

ในโลก OOP เกิดขึ้นจากแนวความคิดหลักคือ "reuse เอากลับมาใช้ซ้ำ" ซึ่งเป็นสาเหตุที่ทำให้เกิดคอนเซ็ปที่สองตามมากคือ "ความเป็น abstract"

ย้อนอดีตกันนิดนึง

แนว คิดของ Object Oriented นั้นมีมานานมากแล้ว ประมาณปี 1960 (ใครเกิดทันบ้าง แน่นอนว่าเราเกิดไม่ทัน ฮา) โดยนักวิทยาศาสตร์คอมพิวเตอร์ 2 คนคือ Ole-Johan Dahl และ Kristen Nygaard ในยุคนั้นการเขียนโปรแกรมจะเขียนกับแบบ imperative แบบบน-ลง-ล่าง เขียนแบบสอนคอมพิวเตอร์เป็นขั้นๆ แบบ how-to ... ทั้งสองคนต้องการสร้างภาษาคอมพิวเตอร์ใหม่ชื่อว่า SIMULA I ซึ่งเป็นภาษาสำหรับการทำ simulation

หลังจากทำเสร็จทั้งสองก็มีแนวคิด ในการออกแบบภาษาคอมพิวเตอร์แบบใหม่ที่ใช้งานได้รวมๆ ทุกรูปแบบ ซึ่งในสมัยนั้นภาษาคอมพิวเตอร์ภาษาหนึ่งมักจะถูกออกแบบมาสำหรับงานที่เฉพาะ เจาะจงแบบหนึ่งเท่านั้น เช่นงานด้านวิทยาศาสตร์และคณิตศาสตร์ก็จะใช้ภาษา FORTRAN, งานธุรกิจการเงินก็จะใช้COBAL, การศึกษา AI ก็จะใช้ LISP ไม่เหมือนทุกวันที่ที่ภาษาหนึ่งภาษาแทบจะเอาอยู่กับงานเกือบทุกรูปแบบ

ทั้ง คู่เลยยกเอาภาษา SIMULA I มาปัดฝุ่นซะใหม่เรียกว่า SIMULA 67 โดยมองข้อมูลและของต่างๆ ในโปรแกรมเป็น "วัตถุ" มีหลักการ โน่น-นี่-นั่น มากมายแต่สิ่งที่สำคัญที่สุดคือภาษานี้เป็นภาษาที่สอนให้ทุกคนรู้จักกับคำ ว่า class ... แต่เป็นที่น่าเสียดายว่าภาษานี้มันไม่ค่อยได้รับความนิยมเท่าไหร่ แต่ตัวภาษาที่ทำให้คอนเซ็ปภาษาแบบ OOP ดังขึ้นมาคือภาษา Smalltalk ของบริษัท Xerox ที่รับแนวคิด OOP ของ SIMULA มาเต็มที่

แล้วสาเหตุอะไรที่ภาษาต้นแบบไม่ดัง แต่ Smalltalk ก็ยังเลือกมาใช้ล่ะ

นั่นก็เพราะภาษา Smalltalk โชว์จุดขายในการทำ interface ส่วนหน้าจอที่ผู้ใช้เห็นด้วยแนวคิดแบบ GUI หรือ Graphic User Interface .. มาร์คตรงคำว่า Graphic เอาไว้เลย ในสมัยนั้นคอมพิวเตอร์แสดงผลได้แค่แบบ Command Line หรือหน้าขอสีดำๆ ที่มีแต่ตัวอักษร ต้องสั่งงานคอมพิวเตอร์โดยการพิมพ์คำสั่งเอาเท่านั้น และเจ้านี่เองก็เป็นต้นแบบให้ Apple หยิบเอาแนวคิดนี้มาทำ Apple Lisa ซึ่งเป็น GUI OS ตัวชูโรง ...ยังไม่หมดแค่นั้น แน่นอนว่ามีหรือที่ Microsoft จะอยู่เฉย Apple ลอกได้ฉันก็ลอกได้ เลยออกมาเป็น Windows ที่เราใช้กันแบบทุกวันนี้

และสาเหตุที่ Smalltalk หยิบแนวทาง Object Oriented ไปใช้ก็คือ interface แบบกราฟิกน่ะ มันประกอบด้วยคอมโพเนนท์ ส่วนประกอบหลายอย่าง เหมือนกันวัตถุในโลกจริง

แนว ทางของ OOP เลยเข้าทางภาษาที่ต้องการ UI แบบกราฟิกพอดี เพราะถ้าลองสังเกตดีๆ ส่วนประกอบต่างๆ ของ GUI นั้นคือหน้าต่างเฟรม (ที่ปัจจุบันเราเรียกกันว่า windows) ตัวอักษรพวกลาเบลต่างๆ ปุ่ม ปุ่ม ปุ่ม ปุ่ม และ ปุ่มสารพัดชนิด ซึ่งภาษา Smalltalk เรียกเจ้าคอมโพเนนท์พวกนี้รวมๆ ว่า "Object"  ... ไม่ว่าจะคอมโพเนนท์ชิ้นไหน พวกแกทั้งหมดคือส่วนประกอบของ GUI ทั้งสิ้น แต่ภาษาโปรแกรมแบบเดิมในตอนนั้น (ภาษาแนว imperative) ไม่ตอบโจทย์การเขียน เพราะสมมุติว่าเราต้องการสร้างอ๊อบเจคของ "button" ขึ้นมา แต่ปุ่มมันก็ไม่ได้มีรูปร่างแบบเดียวกันทั้งระบบ ทั้งตั้งแต่ ปุ่มใหญ่ ปุ่มเล็ก สีเขียว สีแดง สีเทา บางอันเป็นไอค่อน บางอันเป็นตัวอักษร หรือทั้งไอค่อนทั้งตัวอักษรรวมกันอยู่ในปุ่มเดียวกันก็มี ถ้าเราใช้ภาษาโปรแกรมแบบเดิมสร้างเจ้าพวกปุ่มมากมายพวกนี้ขึ้นมา แน่นอนว่าจะต้องมีส่วนของโค้ดที่ "ซ้ำกัน" อยู่มากแน่ๆ (ก็สร้างให้เสร็จสักปุ่มแล้วถ้าอยากได้อีกก็จะเป็นอะไรได้ล่ะ นอกจาก copy-paste-modify ฮา) และแม้ว่าภาษาพวกนี้จะมี function เอาไว้ใช้จัดการกับโค้ดที่ซ้ำซ้อน แต่ในเคสแบบนี้ function ก็ไม่ได้ช้วยอะไรเรามากหรอก

แต่สำหรับ OOP นั้นมันรองรับวิธีเขียนโค้ดประมาณนี้ได้ดี เพราะเป็นการจำลองวัตถุในโลกจริงมาไว้ในรูปแบบของโค้ดโปรแกรม โดยเฉพาะแนวคิด Inheritance หรือที่ภาษาไทยแปลว่า "การสืบทอด" คุณสมบัติจากคลาสๆ หนึ่งไปสู่อีกคลาสได้อย่างง่ายดาย (ถ้าเขียนเป็น)

อย่าง ไรก็ตาม ตัวภาษา Smalltalk ก็ยังไม่ค่อยได้รับความนิยมเพราะว่ามันต้องการพลังของ hardware ในการกระมวลผลสูงสุดๆ เพราะนอกจากคอมพิวเตอร์จะต้องประมวลผลส่วนหลักของโปรแกรมแล้ว output ที่ปกติก็ทำกันแค่ปริ๊นผลลัพท์ออกมาก็ยังต้องมาประมวลผลให้ออกมาในรูป Graphic อีกด้วย ... แต่สำหรับยุคปัจจุบันกำลังประมวลผลของเครื่องที่เร็วกว่าสมัยก่อนหลายร้อย เท่าทำให้มันไม่ใช่ปัญหาอีกแล้ว ภาษาหลายๆ ภาษาที่สืบทอดแนวคิดของ Smalltalk มาเช่น C++ C# Java จึงโด่งดังในยุคนี้ได้

แนวทางพื้นฐาน 4 อย่างในการ reuse ของ OOP

ไหน ก็จะสร้างโค้ดเลียบแบบวัตถุในโลกจริงทั้งที ถ้าสร้างมั่วๆ ไม่มีมาตราฐานตัวคนเขียนอาจจะงงเอง หรือไม่ถึงไม่งงก็เอาไปใช้กับโค้ดคนคนอื่นไม่ได้ก็กลายเป็นผิดวัตถุประสงค์ หลักคือการ reuse โค้ดเดิม ก็เลยจับ OOP มากำหนดมาตราฐานซะ ซึ่งก็เหมือนเป็นสงครามย่อมๆ แต่ละสำนักไม่มีใครยอมใคร ต่างชูแนวคิดของตัวเอง จนในที่สุดเราอยู่ในยุคที่สงครามสงบแล้ว แนวคิดชัดเจนดีแล้ว โชคดีไปนะ

คำเตือน: หัวข้อ Abstraction, Inheritance และ Polymorphism เป็นหัวข้อที่อธิบาย/เข้าใจยาก แต่จะพยายามเขียนให้เข้าใจง่ายที่สุด ... แต่ถ้าคุณอ่านแล้วไม่เข้าใจมันจริงๆ เราเข้าใจคุณ (เพราะครั้งหนึ่งเราก็เคยไม่เข้าใจมัน) ให้อ่านๆ ไปก่อนแล้วในบทความหลังๆ จะเป็นตัวอย่างการใช้ของเจ้าพวกนี้ แต่สำหรับบทความวันนี้เอาทฤษฎีไปก่อนนะ (ฮา)

1. Abstraction

หัวใจหลักของ OOP ที่เข้าใจย๊าก~ยาก จนไม่รู้ว่าตกลงมันเป็นจุดแข็งหรือจุดอ่อนของแนวคิดนี้กันแน่ (ฮา)

แปลเป็นภาษาไทยว่า "นามธรรม" หมายถึง ไร้รูป ไร้ลักษณ์ บรรลุ นิพาน เดี๋ยวๆ! คือเป็นสิ่งที่ทุกคนรู้ แต่อธิบายไม่ถูก แถมไม่แน่ใจด้วยสิ่งที่เรารู้แล้วคนอื่นรู้น่ะ มันจะตรงกันรึเปล่า! ... จะเห็นว่าคอนเซ็ปนี้แค่อธิบายมันก็ "แอ็บสเตร็ก" มากๆ แล้ว ไม่น่าแปลกใจที่นักศึกษาที่เรียน OOP แรกๆ จะอ่านมันไม่เข้าใจแล้วก็ข้ามมันไป ทั้งที่มันคือประเด็นหลักเลย

พอๆ เอาใหม่! กลับมาเข้าเรื่องภาษาโปรแกรม ... ตอนที่เราเขียนโปรแกรมแบบ imperative แบบเดิมน่ะ ด้วยตัวภาษาที่ต้องสั่งงานคอมพิวเตอร์ทุกขั้นตอน แบบขอละเอียดมากๆ อยากได้อะไรเราต้องรู้วิธีการคิดทุกอย่างถึงจะเริ่มเขียนได้ แล้วยิ่งอาจารย์ในชั้นเรียน Fundamental Programming เขียนโปรแกรมเบื้องต้นมักจะชอบบอกนักเรียนด้วยว่า จงรู้วิธีการแก้ปัญหาก่อน เขียนโพลว์ชาร์ตและคิดอัลกอริทึมก่อนจะเริ่มเขียนโค้ดนะ ทำให้นักเรียนมักจะมีความคิดฝั่งเข้าหัวเลยว่า จะเขียนโปรแกรมอะไรก็ตามคิดวิธีให้สำเร็จ 100% ก่อน วิธีการแบบนี้เราเรียกว่าแนว "Real" หรือ "Concrete"

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

Object Oriented เลยมีแนวคิดว่า ก็ถ้ามันยังคิดไม่ออก หรือไม่รู้วิธีตอนนี้ ก็ไม่ต้องเขียนก็ได้ ติ๊ต่างว่ามันทำได้ก็แล้วกัน (แล้วค่อยกลับมาเขียนในอนาคต)

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

เบื้อง หลังอ๊อบเจคพวกนี้คือแนวคิดแบบ Abstract นั่นคือคนคิดปุ่ม คิดกล่องข้อความเขาไม่รู้หรอกว่าเวลาโปรแกรมเมอร์แบบเราๆ เอาส่วนประกอบพวกนี้มารวมๆ กันแล้วมันจะต้องทำงานอย่างไร ดังนั้นเขาเลยละส่วนที่ไม่รู้เอาไว้ โดยหวังว่าคนที่เอาไปใช้จะช่วยเขียนโค้ดส่วนนี้เพิ่มเข้าไปให้

นั่นล่ะ แนวคิดของ Abstract

2. Encapsulation

ศัพท์ คำนี้มาจากคำว่า "แคปซูล" ก็พวกแคปซูลยาอะไรประมาณนั้นแหละ คำแปลคือ "หุ้ม" เติม en- เข้าไปข้างหน้า เติม -tion ไปข้างหลังก็กลายเป็น "การห่อหุ้ม"

เวลา พวกเรากินยา มีใครเคยสงสัยมั้ยว่าทำไมยาเม็ดเล็กๆ แค่นี้มันทำให้เราหายป่วยได้? ถ้าเคยสงสัย งั้นถามต่อ นับเฉพาะยาเม็ดทรงแคปซูล (ที่เป็นทรงกระบอกมีสองสี ผิวเหมือนเป็นพลาสติดแต่ดันกินได้) ใครเคยแกะมันออกมาดูบ้างว่าข้างในมีอะไรใส่เอาไว้อยู่ (ฮา) ส่วนใหญ่ก็ไม่แกะเม็ดยาออกมาดูหรอกเนอะ (แต่เราเคยแกะนะ!) คนส่วนใหญ่แค่รู้ว่ากินยานี้แก้โรคนี้ก็พอแล้ว

คอน เซ็ปของการห่อหุ้มเลยเป็นการติ๊ต่าง (คำนี้อีกแล้ว) ว่าผู้ใช้หรือ user น่ะไม่จำเป็นต้องรู้หรอกว่าโครงสร้างข้างในมันทำงานยังไง คุณมีหน้าที่รู้ว่าถ้าเรียกใช้เจ้านี่ๆๆ มันจะให้ผลออกมาแบบนี้นะ แค่นั้นเลย พอแล้ว

แต่เอ๊ะ คอนเซ็ปนี้คุ้นๆ นะ ... นี่มันคือคอนเซ็ปของ Function นี่นา? ใช่แล้วล่ะ เพราะ Function ก็เกิดขึ้นมาด้วยแนวคิดนี้เหมือนกัน ตัวอย่างเช่นคุณอยากหาค่า sine ของมุม 30 แน่นอนว่าพวกเราไม่อยากรู้สูตรคณิตศาสตร์หรอก เราอยากรู้คำตอบมากกว่า ดังนั้นหน้าที่ของเราก็แค่สั่งว่า sin(30) หรืออะไรทำนองนั้น จบ ดีชีวิตดี มีความสุข

3-4. Inheritance & Polymorphism

หลัก การอย่างที่ 3 และ 4 ขอรวบเราไว้เป็นข้อเดี๋ยวเพราะเป็นสิ่งที่เราจะพูดถึงเป็นเมนหลักในบทความ นี้เลยล่ะ (บทความนี้จะเน้น Inheritance ส่วน Polymorphism จะพูดในบทความต่อๆ ไปแต่ก่อนที่คุณจะอ่านรู้เรื่อง คุณต้องเข้าใจ Inheritance ซะก่อน)

พระเอกสำคัญของโลก OOP ที่โดนชะตากรรมเช่นเดียวกับ Abstraction คือเข้าใจ ย๊าก~ยาก จนนักเรียนมันจะเรียนๆ ไปให้ผ่านแล้วถ้าชีวิตยังต้องยุ่งกับการเขียนโปรแกรมอยู่ก็เอาแค่ imperative พอละกัน

คำว่า Inheritance แปลว่า "การสืบสอด", "การส่งต่อ" หรือ "การได้รับมรดก" แปลว่าถ้าเรามีโค้ดอยู่ชุดหนึ่ง แล้วดันอยากได้โค้ดอีกชุดที่ทำงานคล้ายๆ กัน สังเกตคำว่า "คล้ายๆ " ให้ดีๆ เลย ... คล้ายๆ แปลว่าไม่เหมือน โค้ดต้นฉบับอาจจะมี 100 บรรทัด ส่วนโค้ดชุดที่สองอยากจะทำงานเหมือนโค้ดชุดแรกถึง 99 บรรทัด แต่ดันมีอยู่บรรทัดนึงที่ทำงานต่างออกไป

ตัวอย่างเช่น

function sumNumber(int start, int end){
	int sum = 0
	for(i = start; i <= end; i++ ){
		sum = sum + i;
	}
	return sum
}

ขอเดาเอาเองว่าคนที่เข้ามาอ่านบล๊อกซีรีย์ OOP นี้อ่านโค้ดข้างบนออกว่ามันเอาไว้ทำอะไรได้ภายใน 10 วินาที (ฮา) ... มันก็แค่โค้ดหาผลรวมของเลขชุดหนึ่งนั่นแหละ

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

วิธีการยอดนิยมอันดับหนึ่งคือการเพิ่มสิ่งที่เรียกว่า flag เข้าไปเข้าไปใน parameter (ฉันรู้ คุณก็ทำ...เพราะฉันก็ทำ ฮา)

function sumNumber(int start, int end, boolean only_even){
	int sum = 0
	for(i = start; i <= end; i++ ){
		if(only_even && i mod 2 == 0)
		sum = sum + i;
	}
	return sum
}

เอาล่ะ ใครทำแบบนี้บ้าง ยกมือ!

ก็ไม่ผิดหรอก จนกระทั่งหลังจากทำโจทย์ข้อนี้เสร็จแล้วโจทย์ข้อต่อไปดันสั่งว่า "จงปรับปรุงโค้ดชุดนี้อีกครั้ง โดยครั้งนี้เอาแค่ผลบวกเฉพาะตัวเลขที่เป็นจำนวนเฉพาะ" และ "โค้ดชุดที่ 1 และชุดที่ 2 จะต้องยังทำงานได้เหมือนเดิมด้วย"

นั่นแหละ ปัญหาก็จะเริ่มจากนี้เป็นตันไป จนในที่สุดเราก็จะเลิกการเพิ่ม flag แล้วเขียน function ตัวใหม่ขึ้นมาใช้แทนดีกว่า ... ดังนั้นในโลกของ OOP เมื่อเราต้องการอะไรใหม่ คอนเซ็ปเลยไม่ใช่เป็นการเพิ่ม flag แล้วไป if-else ข้างใน แต่...

เราจะมองหาก่อนว่ามันมีอะไรให้ใช้อยู่แล้วมั้ย

ถ้าไม่เคยมีมาก่อนก็สร้างขึ้นมาใหม่

แต่ถ้ามีอยู่แล้วก็ดูว่าเพียงพอมั้ย ถ้าไม่พอก็เขียนเพิ่มเข้าไปเฉพาะส่วนที่ขาดไป

มาดูตัวอย่างกันดีกว่า เช่นอยากได้คลาส "รถยนต์" ก็ลองมองหาดูก่อนว่ามีอะไรที่ให้อารมณ์ยานพาหนะที่เคยเขียนผ่านไปบ้างแล้วรึเปล่า ถ้าไม่มีก็สร้างใหม่เลย

อาจจะได้โค้ดหน้าตาประมาณนี้

class Car {
	
	int totalDistance = 0
	
	function drive(int distance){
		this.totalDistance = this.totalDistance + distance
	}
	
	function getTotalDistance(){
		return this.totalDistance
	}
	
}

ทีนี้ ในขั้นต่อไป หากเราอยากได้คลาส "รถบรรทุก" อีกคลาส โดยที่รถบรรทุกสามารถบรรทุกของได้ด้วย แต่รถบรรทุกก็เป็น "รถยนต์" ประเภทหนึ่ง เราเลยจะไม่สร้างคลาสรถบรรทุกโดยก๊อปปี้ไฟล์ "รถยนต์" ขึ้นมาอีกไฟล์แล้วเขียนโค้ดเพิ่มลงไป

ไม่ๆ !! ไม่เอาแบบนี้นะ แบบนี้มันคือการ copy -> paste โต้งๆ เลย!

ลองมาเปลี่ยนวิธีใหม่กันโดยบอกว่า รถบรรทุกน่ะมัน inherit มาจากรถยนต์นะ

คีย์เวิร์ด extends

ในแต่ละภาษามี syntax ในการบอกว่าคลาสนี้น่ะมัน inherit มาจากคลาสนี้นะด้วยการเขียนที่ต่างกัน แต่ส่วนใหญ่เราจะเห็นอยู่แค่ 2 รูปแบบคือการใช้คีย์เวิร์ด extends หรือการใช้สัญลักษณ์  แทน

ดังนั้นคลาสรถบรรทุกของเราก็จะมีหน้าตาแบบนี้

class Truck extends Car {
	string luggage
	
	function loadLuggage(string luggage){
		this.luggage = luggage
	}
}

หรือ 

class Truck : Car {
	string luggage
	
	function loadLuggage(string luggage){
		this.luggage = luggage
	}
}

สังเกตว่าการสั่ง extends นั้นแล้วตามด้วยชื่อคลาสต้นแบบ จะเห็นว่าคลาส "รถบรรทุก" จะเห็นโค้ดที่สั้นมาก แต่ก็สามารถเรียกใช้งานความสามารถของ "รถยนต์" ได้ด้วย

car = new Car()
car.drive(10)
car.getTotalDistance()

truck = new Car()
truck.drive(10)
truck.getTotalDistance()

//truck เรียกทุกอย่างได้เหมือน car แต่สามารถเรียก method เพิ่มเติมเฉพาะของ truck ได้
truck.loadLuggage("สัมภาระชิ้นที่1")

สรุปคือเวลาเราทำการ extends คลาสมา ให้จำไว้ว่าแม้ตัวโค้ดจะไม่ถูก copy -> paste ตามมา แต่ในมุมมองโปรแกรมมันจะเห็นอะไรแบบนี้

class Truck{
    
    int totalDistance = 0
    string luggage
    
    function drive(int distance){
        this.totalDistance = this.totalDistance + distance
    }
    
    function getTotalDistance(){
        return this.totalDistance
    }
    
    function loadLuggage(string luggage){
        this.luggage = luggage
    }
    
}

การที่เราไม่ต้องเขียนโค้ด แต่สามารถใช้งานโค้ดในส่วนของคลาสต้นฉบับแบบนี้แหละที่เราเรียกว่าความสามารถ Inheritance

Parent & Child

หรือ คลาสแม่ (คลาสต้นฉบับ) และ คลาสลูก ตามตัวอย่างที่ผ่านมา ถ้าคลาสไหนทำตัวเป็นคลาสต้นฉบับให้คลาสอีกคลาสหนึ่งทำการ extends ตัวมันไป เราจะเรียกคลาสต้นว่า Parent Class และเรียกคลาสที่ extends มันไปว่า Child Class

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

class A {...}

class B extends A {...}

class C extends B {...}

สำหรับคลาส B แล้ว เนื่องจากมัน extends มาจากคลาส A ตัวมันเองกับคลาส A จึงมีสถานะกันแบบ [Parent=A / Child=B] แต่ถ้าคลาส B มองลงไปยังคลาส C ก็จะมีสถานะแบบ [Parent=B / Child=C]

Access Modifier

หนึ่งในคอนเซ็ปของ OOP คือ Encapsulation หรือการห่อหุ้ม เรามักไม่ต้องการให้คนนอกมายุ่งกับตัวแปรภายในคลาสเราได้ตามใจชอบ โดยเฉพาะถ้า property ตัวนั้นเป็นค่าพวก password (ใช่มั้ยล่ะ?)

ใน OOP เลยมีการสร้างคีย์เวิร์ดขึ้นมาเพื่อให้เราเอาไปกำหนดว่า properties / method ตัวไหนที่อยากให้คนภายนอกเรียกใช้งานได้หรือไม่ได้ เรียกว่า access modifier หรือ การกำหนดสิทธิการเข้าถึง เรียกง่ายๆ ว่า 4P

  • public - เปิดโล่ง ใจดี ใครเข้ามาใช้ก็ได้
  • protected - สำหรับคลาสอื่นๆ จะเห็นแบบ private / แต่สำหรับคลาส Child จะเห็นเป็น public
  • private - ส่วนตัวสุดๆ นอกจากคลาสตัวเองแล้วไม่อนุญาตให้ใครเข้ามาใช้ทั้งนั้น
  • package - เดี๋ยวค่อยว่ากัน

วิธีการวาง access modifier นั้นให้วางเอาไว้ข้างหน้าของ properties / method เช่น

class Demo {
	private int x
	protected int y
	public int z
	int w
}

สำหรับคีย์เวิร์ด package นั้นไม่มี แต่ถ้าเราไม่เติม access modifier ลงไปเลยมันจะถือว่าเป็นระดับ package เอง

เอ้า มาดูตัวอย่างกันดีกว่า สมมุติว่าเรามีคลาสอยู่ 3 คลาส คือ Car, Truck (extends มาจาก Car), และ Drone โดยแต่ละคลาสจะมี properties / method ที่ประกาศ access modifier เอาไว้

ดังนั้นในมุมมองของ Car มันสามารถเข้าถึง

  • public, protected, private ของคลาสตัวเองได้ (อันนี้แน่อยู่แล้ว)
  • public ของคลาสอื่นๆ ทุกตัว (สำหรับ parent แล้ว child ถือเป็นคลาสอื่นๆ)

แต่ในมุมมองของ Truck จะเป็นแบบนี้

  • public, protected, private ของคลาสตัวเองได้ (ชัวร์~)
  • public ของคลาสอื่นๆ ทุกตัว
  • protected ของคลาส Car ที่เป็น parent ของมัน

จะเห็นว่าตัวมาตราฐานทั่วๆ ไปคือระดับ public / private ... เพราะมันใช้ง่ายสุด ไม่เปิดให้ทุกคนใช้ ก็ปิดไม่ให้ทุกคนใช้ แค่นั้นแหละ ง่ายสุดๆ อ่ะ (ฮา)

แต่ถ้าเราทำการ extends เราอาจจะต้องคิดเผื่อคลาสลูกเสียหน่อย เช่นคลาส Car ที่เราสร้างไปตอนต้น มี property: totalDistance อยู่ สำหรับคลาสอื่นๆ เราอาจจะไม่อนุญาตให้เข้าถึงตัวแปรนี้ได้ก็จริง แต่สำหรับคลาส Truck ที่ extends มันไป อาจจะเป็นข้อยกเว้น ... สำหรับคลาสอื่นไม่อยากให้เข้าใช้ได้ แต่ให้ Truck ใช้งานได้ เคสแบบนี้เราจะประกาศเป็น protected ล่ะ

package

บอกก่อนว่าระดับ package เป็นระดับที่บางภาษาก็มี บางภาษาก็ไม่มี ... สำหรับภาษาที่ไม่มี การที่เราไม่กำหนด access modifier จะถูกกำหนดระดับเป็น public แทน

ข้อยกตัวอย่างด้วยภาษา Java นะ

package vehicle;

class Car { 
	...
	int w;
}

class Truck extends Car {
	...
	int w;
}

class Drone { 
	...
	int w;
}

ในโค้ดชุดนี้มีการเพิ่ม package เข้าไปว่าคลาสพวกนี้น่ะ อยู่ใน "กลุ่มของยานพาหนะ" นะ (บางภาษาเช่น PHP จะใช้คำว่า namespace แทน package)

อย่างที่อธิบายไปในตัวอย่างข้างบน ในที่นี้ตัวแปร w แทนตัวแปรระดับ package ซึ่งในมุมมองของคลาส Car, Truck และ Drone มองเห็นตัวแปรตัวนี้ของคลาสอื่นเป็นระดับ public เลยล่ะ เข้าใช้ได้ตามสบายไม่มีกั๊ก

อันนี้ไม่มีปัญหา แต่ถ้าหากคุณเป็น "วัว" คราวนี้มันก็ไม่แน่นะ!!

ให้คลาสวัวหน้าตาแบบนี้

package animal;

class Cow { 
	...
	int milk;
}

เราประกาศว่าคลาส Cow นี่น่ะมันอยู่ใน package "animal" คือเป็นสัตว์ไม่ใช่ยานพาหนะ คราวนี้ Cow จะไม่สามารถเข้าไปเรียกใช้ตัวแปร w ในคลาส Car, Truck และ Drone ได้แล้วเพราะอยู่คนละกลุ่มกัน (และเช่นกันคือคลาส Car, Truck และ Drone ก็ไม่สามารถใช้ตัวแปร milk ของ Cow ได้ด้วยเหมือนกันนะ)

อันนี้แหละคือระดับ package ยอมให้คลาสใน package เดียวกันเท่านั้นเข้ามาใช้งานได้ ถ้าคนละคลาสก็อดไป แต่ข้อความระวัง (ย้ำอีกทีนะ) คือบางภาษาก็ไม่มี!

แต่ก็มีภาษายอดฮิตตัวหนึ่งที่เป็น OOP แต่ดันไม่มี access modifier ให้ใช้ นั่นคือ Python ... หลักปรัชญาของภาษานี้คือเน้นง่ายเข้าไว้ อะไรไม่จำเป็นตัดออกให้หมด คนคิดภาษา Guido van Rossum เคยกล่าวเอาไว้ว่า "We're all consenting adults here" หรือถ้าแปลก็ประมาณ "พวกเราโตๆ กันแล้ว (ควรจะรู้ว่าอะไรควรทำอะไรไม่ควรทำนะ)"

- - -

กระบวนท่า: Inherit หลายชั้นอันแสนงงงวย

อีกเรื่องที่หลายคนงงกับการ inherit (ยังมีอีกเรอะ!?) คือถ้าเราทำการ inheritance หลายคลาสต่อๆ กันแล้ว เวลาเขียน method ทับกันไปทับกันมา แล้วมันจะเรียกตัวไหนทำงานกันแน่

class A {
	function first(){
		print "A"
	}
	function second(){
		this.first()
	}
	function third(){
		this.second()
	}
}

class B extends A {
	function first(){
		print "B"
	}
	function fourth(){
		this.third()
	}
}

class C extends B {
	function second(){
		this.first()
	}
}

มีคลาส A, B, C ที่มีการเรียก (call)  ต่อกันเป็นทอดๆ แต่ในคลาสลูกคือ B และ C ก็มีการเขียนบาง method ทับของเดิมลงไป หรือที่เราเรียกว่า override นั่นเอง

ถ้าเราสร้าง object จากคลาส A มาหนึ่งตัวแล้วสั่งว่า a.third() ละก็ ผลสุดท้ายจะได้ print "A" ... อันนี้เรียกต่อกันเป็นทอดๆ ในคลาสเดียวกัน เข้าใจได้ง่าย

แต่สำหรับ b และ c ล่ะ? ให้จำเอาไว้ว่า

ไม่ว่ามันจะทำงาน method ในคลาสไหน -> this จะเริ่มจากคลาสตัวเองเสมอ

ถ้าเราสั่งว่า b.fourth() ให้ทำงาน ลำดับการเรียก method ทำงานจะเป็นดังนี้

  1. เริ่มที่ fourth() ของ B -> call third() -> แต่ third() ดันไม่มีในคลาสตัวเอง
  2. เนื่องจาก B extends มาจาก A เป้าหมายต่อไปมันเลยไปหา third() ใน A แทน -> call second()
  3. กลับมาเริ่มหาจากคลาส B อีกครั้ง -> แต่ second() ก็ไม่มีในคลาสตัวเอง (อีกแล้ว)
  4. เนื่องจาก B extends มาจาก A เป้าหมายต่อไปมันเลยไปหา second() ใน A แทน -> call first()
  5. กลับมาเริ่มหาจากคลาส B อีกครั้ง -> คราวนี้ first() มีใน B เลยปริ๊น "B"

จะเห็นว่าแม้ว่าเราจะเขียนโค้ดใน A เช่น this.second() คีย์เวิร์ด this ไม่ได้อ้างอิงถึงคลาส A เสมอไป แต่จะอ้างอิงถึงคลาสที่เป็นที่เป็นคนโดน call เป็นคนแรก ในที่นี้คือคลาส B เป็นต้น

ลองมาดูอีกตัวอย่างเผื่อยังไม่เก็ท คราวนี้เป็นคลาส C บ้าง

ถ้าเราสั่งว่า c.fourth() ให้ทำงาน อะไรจะเกิดขึ้น ลองเดาตามไปด้วยนะ

  1. เริ่มที่ fourth() ของ C -> แต่ C ดันไม่มี fourth() ในคลาสตัวเอง
  2. เนื่องจาก C extends มาจาก B เป้าหมายต่อไปมันเลยไปหา fourth() ใน B แทน (ซึ่งมี) -> call third()
  3. กลับมาเริ่มหาจากคลาส C อีกครั้ง -> แต่ third() ก็ไม่มีในคลาสตัวเอง (อีกแล้ว)
  4. เนื่องจาก C extends มาจาก B เป้าหมายต่อไปมันเลยไปหา third() ใน B แทน (ซึ่งก็ยังไม่มี)
  5. เนื่องจาก B extends มาจาก A เป้าหมายต่อไปมันเลยไปหา third() ใน A แทน (คราวนี้ต้องมีแล้วล่ะ ไม่งั้นพังแน่ เพราะ A ไม่ได้ extends จากใครมาอีกแล้ว) -> call second()
  6. กลับมาเริ่มหาจากคลาส C อีกครั้ง -> คราวนี้ second() มีใน C -> call first()
  7. แต่ first() ก็ไม่มีใน C -> เป้าหมายต่อไปเลยเป็น B
  8. เจอ first() ใน B เลยปริ๊น "B"

สำหรับเนื้อหาในส่วนของ Inheritance ขอจบเอาไว้แค่นี้ก่อน ถึงตอนนี้อาจจะยังไม่เห็นประโยชน์ของมักมากนักเพราะเรายังไม่มี Polymorphism ซึ่งเดี๋ยวจะพูดถึงในตอนหน้า

ก่อนจบของทิ้งคำพูกของ Grady Booch หนึ่งในคณะผู้คิดค้นโมเดล UML และเป็นปรมาจารย์ทางด้าน Object-Oriented ที่กล่าวเอาไว้ว่า

Programming without inheritance is distinctly not object-oriented.

หรือการเขียนโปรแกรมโดยไม่ได้กะให้มันทำการ inheritance ได้ด้วย ก็ยังไม่ถือว่าคุณเขียนโค้ดแบบ Object-Oriented หรอกนะ

ป.ล. แต่ขอเสริมอีกว่า OOP ไม่ใช่พระเจ้า ไม่ได้เขียนโค้ดเป็น OOP ก็ไม่ใช่เรื่องผิดหรอก งานบางอย่างก็ไม่จำเป็นต้องเข้าให้มันเป็น OOP ก็ได้นะ

1940 Total Views 2 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

You may also like...

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องที่ต้องการถูกทำเครื่องหมาย *