OOP อย่างง่าย
ถ่ายทอดความรู้การออกแบบโปรแกรมเชิงวัตถุ & การเขียนโปรแกรมเชิงวัตถุ (OOP)
ตอนที่ 16: Behavior (method) ควรจัดวางอยู่ในคลาสใดดี ?
หากเรามีคลาส Stack ซึ่งทำหน้าที่เขียน/อ่านข้อมูลด้วยคำสั่ง push() กับ pop() ต่อมา client class ต้องการแสดงผลข้อมูลภายใน Stack ด้วยรูปแบบ text ดังนั้นจึงจะสร้าง method print() ขึ้น คำถามคือ method print() นี้ ควรจัดวางในคลาสใด
จะพูดถึง 3 วิธี ได้แก่
1) จัดวางใน Stack class
2) จัดวางในคลาสเฉพาะ
3) จัดวางใน Client class
Design 1: Basic Design
ออกแบบตามหลักพื้นฐานของ OOP คือจะเพิ่ม behavior ไว้ในบริเวณที่ใกล้กับข้อมูลที่มันต้องการใช้งาน ดังนั้นจึงเพิ่ม print() ลงในคลาส Stack เนื่องจากจะต้องอ่านข้อมูลภายใน Stack
design นี้ดีหาก method ที่จะเพิ่มมีจำนวนไม่มาก และ client ของคลาส Stack โดยส่วนใหญ่มีความจำเป็นต้องใช้ print()
แต่หาก client แต่ละตัวต้องการ print ในรูปแบบต่างๆ กัน หรืออาจมี client ใหม่ๆ ที่จะ print ในรูปแบบใหม่ๆ ที่ไม่เคยคิดมาก่อน การให้คลาส Stack มี printText(), printGUI(), etc. จะทำให้ interface ของคลาสรกเกินความจำเป็น นั่นคือ client ส่วนใหญ่อาจไม่ได้ต้องใช้ print นอกจากนั้นทำให้เกิด dependency โดยไม่จำเป็น เช่น printGUI() อาจมีการเรียกใช้งาน Graphics Library เช่น Java Swing ทำให้ทุก client ที่ใช้งานคลาส Stack ก็ต้องขึ้นกับ Java Swing Library ไปด้วย ทั้งๆ ที่บาง client อาจไม่ได้สนใจใช้งาน printGUI() เลย
ผลที่ได้ของการออกแบบแบบนี้คือ อาจเกิดข้อเสีย 1) ทำให้คลาส Stack มี interface ที่ใหญ่ และ 2) มี dependency ที่มากเกินความจำเป็น
interface ที่ใหญ่และซับซ้อนของคลาสย่อมทำให้ client ที่จะมาเรียกใช้งานคลาสนั้นต้องเสียเวลาศึกษาวิธีเรียกใช้งานมากขึ้นไปด้วย
(อ่านต่อในรูปถัดไป...)
ตอนที่ 15: ควรรวม class หรือแยก class ดี ?
จุดประสงค์ของการแยกคลาสคือเพื่อให้โค้ดอ่านง่าย จุดประสงค์ที่ให้โค้ดอ่านง่ายก็เพื่อให้แก้ไขได้ง่าย
การแก้ไขโค้ดรวมถึงการเพิ่มเติมโค้ด เกิดขึ้นเสมอ เช่น ขณะที่เขียนเสร็จไปครึ่งหนึ่ง พอจะมาเขียนต่ออาจจะจำไม่ได้ว่าทำอะไรไปก็ต้องไล่อ่านโปรแกรมใหม่
คลาสจะอ่านง่าย เมื่อคลาสนั้นไม่ซับซ้อนมาก (low complexity)
จากประสบการณ์ ความซับซ้อนของคลาสมักเกี่ยวพันกับสองอย่างคือ
1. ขนาดของคลาส ยิ่งคลาสใหญ่ (จำนวนบรรทัดมาก หรือจำนวนคำสั่งมาก) ก็มีแนวโน้มจะซับซ้อนและอ่านเข้าใจยาก
2. Dependency คือความเกี่ยวพันกับองค์ประกอบอื่นๆ ไม่ว่าจะเป็น methods & fields ต่างๆ ภายในคลาส รวมถึงการเรียกใช้คลาสภายนอกอื่นๆ ที่เกี่ยวข้อง ยิ่ง dependency สูงก็มีแนวโน้มจะอ่านเข้าใจยาก
ขนาดที่เหมาะสมของคลาส
ในตำราส่วนใหญ่จะไม่ได้บอกตายตัวว่ากี่บรรทัด แต่จากประสบการณ์ส่วนตัวแล้ว จะขอให้ guideline คร่าวๆ ดังนี้ (สำหรับโค้ดทั่วๆ ไปที่ไม่ได้เว้นบรรทัดว่างเยอะมาก หรือไม่ได้ comment ยาวมาก)
ความยาวของ 1 คลาส (หรือ 1 ไฟล์)
0 - 200 บรรทัด : อ่านง่าย
201 - 500 บรรทัด : อ่านยากปานกลาง
501 - 2,000 บรรทัด : อ่านยาก
> 2,000 บรรทัด : อ่านยากมาก
(ตัวเลขแค่คร่าวๆ จากประสบการณ์ พอให้เห็นภาพ)
ตัวเลขตรงนี้ แต่ละคนจะต่างกันไป แค่เพียงช่วยให้เห็นภาพคร่าวๆ ใช้เป็น guideline เวลาที่โปรแกรมอ่านยากและต้องการแยกคลาส ให้เพ่งเล็งไปที่คลาสใหญ่ ๆ หรือคลาสที่ซับซ้อนมาก ๆ ก่อน
ทำไมคลาสใหญ่จึงทำให้อ่านเข้าใจยาก
คลาสใหญ่มีแนวโน้มที่จะมีองค์ประกอบภายในมาก (methods & fields) เมื่อองค์ประกอบมาก ก็จะมองความสัมพันธ์ยาก ว่าอะไรเรียกใช้อะไร จึงทำให้มองภาพรวมได้ยาก
ถ้างั้น ใช้วิธีทำให้ method เป็น method ยาวๆ ดีไหม จะได้มี method น้อยลง
หากทำให้ method ยาว ผลที่ได้คือ องค์ประกอบภายใน method จะมีจำนวนมากแทน ซึ่งส่งผลให้อ่านเข้าใจยาก และเกิด bug ได้ง่าย เมื่อชั่งน้ำหนักดูแล้ว การทำ method ให้เล็กๆ หลายๆ method จะส่งผลดีกว่าในแง่การอ่านโปรแกรม
แต่สุดท้ายจะพบว่า method เล็กๆ หลายๆ ตัวมีขึ้นเพื่อรองรับการทำงานของ method หลักตัวนึง ตรงนี้เราจะสามารถแยกคลาสออกเป็น Method Object (อ่านตอนที่ 13) ได้ นั่นเอง
การแยกคลาสที่ดีควรมีส่วน private
ลองนึกถึงในภาษาซี ที่ทุก function เป็น public หมด สมมุติว่ามี 100 functions ในไฟล์เดียว
ต่อให้เราแยกเป็น 5 ไฟล์ ไฟล์ละ 20 functions แต่ว่าทุก functions ก็มีสิทธิเรียกใช้งานกันได้หมดอยู่ดี
ดังนั้นเราจะไม่สามารถแน่ใจได้เลยว่า function f() ถูก function จากไฟล์ใดเรียกใช้งานบ้าง
การที่ OOP มีส่วน private มาช่วยแก้ปัญหานี้ โดยรับประกันว่าส่วน private จะถูกเรียกโดยกลุ่ม functions จำนวนจำกัด แค่ภายในคลาสเท่านั้น
สรุปว่า
จะแยกคลาสเมื่อโค้ดยาวขึ้น จนมีแนวโน้มอ่านยากขึ้น อีกเหตุผลหนึ่งคือ โค้ดยาวๆ มีแนวโน้มจะแยกคลาสได้ง่ายกว่าโค้ดสั้นๆ
การแยกคลาสที่ดี คลาสที่ได้ใหม่ ควรมีส่วน private ซึ่งจะทำให้ผลที่ได้มีความซับซ้อนน้อยลง
Q: เป็นไปได้ไหม ที่จะแยกคลาสไม่ได้เลย ?
A: เป็นไปได้ถ้างานนั้นๆ ซับซ้อนมากจริงๆ ทุกอย่างต้องเรียกใช้งานกันหมด แต่จากประสบการณ์แล้ว ถ้าโค้ดยาวระดับ 500 บรรทัดขึ้นไป จะสามารถแยกคลาสได้ค่อนข้างแน่นอน
Q: กรณีใดไม่จำเป็นต้องแยกคลาส ?
A: กรณีคลาสสั้นๆ และอ่านเข้าใจง่ายอยู่แล้ว เช่นโค้ดยาว 0-200 บรรทัด กรณีเช่นนี้ถ้ามองผ่านๆ แล้วไม่เห็นชัดว่าแยกอะไรออกมาได้ ก็คิดว่าไม่จำเป็นต้องแยกคลาส
Q: แล้วการรวมคลาสจำเป็นต้องทำไหม เมื่อไร ?
A: การรวมคลาสอาจทำกับคลาสที่เล็กเกินไปและสามารถยุบรวม concept ไปเข้ากับคลาสอื่นได้ คลาสที่มีแค่ 1-2 methods และมีแนวโน้มว่าจะไม่ขยายใหญ่ขึ้น อาจพิจารณาย้าย methods เหล่านั้นไปรวมในคลาสอื่น ถ้ามีคลาสที่เหมาะสม
จากประสบการณ์แล้วในการทำงานจริง มักพบว่าคลาสใหญ่เกินไป มากกว่าพบว่าคลาสเล็กเกินไป ดังนั้นจึงมักต้องแยกคลาส บ่อยกว่าที่จะต้องรวมคลาส
อ่านเพิ่มเติม : Bad Smells in Code: Large Class, Lazy Class, and Data Class จากหนังสือ Refactoring, 1st edition โดย Martin Fowler
ตอนที่ 14: แนะนำหนังสือ OOP & OOD ที่น่าอ่าน
บางครั้งผู้เริ่มศึกษาจะเกิดคำถามว่าจะหาอ่านความรู้ทาง OOP ดีๆ ได้จากไหน โพสนี้เลยตั้งใจจะมาแนะนำหนังสือคุณภาพในเชิง OOP & OOD สัก 3 เล่มครับ
ก่อนอื่น OOP ไม่ใช่แค่การรู้ว่าคลาสคืออะไร, object คืออะไร, method คืออะไร ซึ่งหลักพวกนั้นจะเป็นแค่จุดเริ่มต้น แต่ OOP ที่ลึกลงไปควรจะต้องเกิดความเข้าใจว่า
- method ควรวางที่ใด
- คลาสนี้ดูจะใหญ่หรือซับซ้อนเกินไปแล้วหรือไม่
- การจะแตกคลาสหรือ method ออกมา จะแยกออกมาอย่างไร และเมื่อทำแล้วมันดีขึ้นหรือแย่ลงในแง่ความซับซ้อน, dependency, และ reusability
ซึ่งในเพจนี้ตั้งใจว่าจะทยอยเล่าเรื่องเหล่านี้ แต่ช่วงนี้เนื่องจากยังไม่ค่อยมีเวลา update เพจ ดังนั้นจึงจะมาแนะนำหนังสือน่าอ่านให้ก่อนสำหรับผู้ที่สนใจครับ
ตอนที่ 13: การแยกคลาสออกมาเป็น Method Object (Helper Class)
ในการแยกคลาส ไม่จำเป็นว่าสิ่งที่ได้จะต้องเป็น object composition ตามที่เคยกล่าวในสองบทความก่อนเสมอไป
ในบางกรณีคลาสที่สร้างใหม่จะมีขึ้นเพื่อช่วยการทำงานของ method เดี่ยวๆ method เดียว จึงไม่มีความจำเป็นต้องเก็บ reference ของวัตถุนั้นไว้ในฟิลด์ วัตถุที่มีลักษณะเช่นนี้เรียกว่า "Method Object"
Method Object เป็นศัพท์ที่ปรากฎในตำรา Refactoring [1] ซึ่งในตำราไม่ได้ให้นิยามที่ชัดเจน แต่พอตีความได้ว่า method object เป็นคลาสที่มาช่วย (Helper Class) ให้ method บางตัวที่ทำงานซับซ้อนทำงานได้ง่ายขึ้น โดยการย้ายความซับซ้อนไปไว้ในตัว Method Object แทน
ดังตัวอย่างในภาพ method object ได้แก่คลาส TaxCalculator ซึ่งภายในมีการทำงานโดย 3 methods ได้แก่ calcTax(), calcTaxStep1(), calcTaxStep2()
คุณสมบัติของ Method Object
1. Method Object จะถูกสร้าง, ใช้, และทำลายอยู่ภายใน method เดียว, ไม่แชร์กันระหว่างหลายๆ method
2. เหมาะกับงานที่ซับซ้อนแต่ทำจบในการ call ครั้งเดียว ไม่ต้องเก็บ state ค้างไว้
3. การรับค่าเข้า รับผ่านทาง constructor แล้วเก็บพักไว้ใน field ได้ (เช่นรับค่า a ตามภาพ)
4. การส่งค่ากลับ อาจส่งด้วยการ return ค่าออกมาจาก method
ข้อดี:
- ประโยชน์ของการแยกคลาสเป็น Method Object คือคลาสหลักจะมีขนาดเล็กลงและอ่านเข้าใจง่ายขึ้น
ข้อเสีย:
- ไม่เหมาะกับงานที่ต้องเปลี่ยนแปลงแก้ไขค่าฟิลด์หลาย ๆ ฟิลด์ เพราะจะส่งค่ากลับยาก
- ไม่เหมาะกับงานที่ต้องอ้างอิงค่าฟิลด์ในคลาสหลักมาก ๆ เพราะ parameter list จะยาว
อ้างอิง
[1] Martin Fowler, Refactoring, 1st edition, 1999, p.135 "Replace Method with Method Object".
ตอนที่ 12: การแยกคลาสด้วยเทคนิค Object Composition ในกรณีมี field ที่ต้องใช้ร่วมกัน
ตามภาพ ดูเหมือนเกือบจะแบ่งกลุ่มสมาชิกในคลาสออกได้เป็นสองกลุ่มแล้ว เว้นแต่ว่าทุก methods เรียกใช้ตัวแปร e ร่วมกันหมด
มีวิธีทำได้หลักๆ 3 วิธี (ในภาพแสดงแค่วิธีที่ 1)
1. อาจให้ e อยู่ใน Component แล้วเปิดเป็น public ไว้เพื่อให้ MyClass เรียกใช้งานได้ ผ่านทาง sub.e
2. หรือถ้าจะให้ e อยู่ใน MyClass แล้วต้องการให้ Component เรียกใช้ได้ จะต้องสร้าง back link ย้อนกลับมา (ซับซ้อนกว่า)
3. หรือกรณี e เป็น object (reference type) สามารถ share กันได้โดยเก็บ ตัวแปร e ในทั้งสองคลาสได้เลย
กรณีที่มีตัวแปรร่วมกันแค่ตัวแปรเดียวแบบนี้ยังพอทำได้ แต่หากมีการใช้ตัวแปรร่วมกันมากๆ อาจจะไม่ควรแยกคลาส หรืออาจจะต้องหาวิธีจับกลุ่มด้วยวิธีอื่นแทน
ตอนที่ 11: การแยกคลาสด้วยเทคนิค Object Composition
เมื่อคลาสใหญ่ขึ้นมาก และประกอบด้วย field (ตัวแปรภายในคลาส) และ methods จำนวนมาก การอ่านทำความเข้าใจคลาสจะยากขึ้น จึงควรเริ่มมองหาวิธีการแยกคลาสออกเป็นคลาสเล็กๆ ซึ่งในตำรา Refactoring เรียกว่าเทคนิค Extract Method
หากพบว่ามีกลุ่มของ fields และ methods ที่มักเรียกใช้กัน และไม่ได้เรียกใช้กลุ่มของ fields และ methods กลุ่มอื่น มีโอกาสสูงที่เราจะสามารถแยกส่วนนั้นออกมาเป็นคลาสได้
ในภาพซ้ายมือจะเห็นว่าคลาส A มี members แบ่งได้เป็นสองกลุ่มคือ
a, b, m(), n() : มีความเกี่ยวข้องกัน
c, d, p(), q() : มีความเกี่ยวข้องกัน
ใช้วิธีการ Extract Method เพื่อแยกคลาสออกมา เช่นแยก c, d, p(), q() ออกมาเป็นคลาสชื่อว่า Component ส่วนคลาสเดิมจะต้องเพิ่มตัวแปรชี้ไปยังคลาสใหม่นี้ เช่นในภาพคือตัวแปรชื่อว่า sub
Delegation (การส่งต่องาน)
แม้งานทั้งหมดของ p() กับ q() จะถูกย้ายไปยังคลาส Component แล้ว แต่ MyClass ก็ยังต้องการ p() กับ q() เนื่องจากเดิมเป็น public อยู่ (อาจมีใครต้องการเรียกใช้งาน) วิธีการที่จะทำให้โค้ดไม่ซ้ำซ้อนก็คือ ใน MyClass เราจะเขียน p() และ q() สั้นๆ ให้ไปเรียก sub.p() และ sub.q() ตามลำดับ วิธีนี้เรียกว่า delegation
Object Composition
การที่ object หนึ่ง (Component) เป็น field อยู่ในอีก object หนึ่ง (MyClass) และ life time ค่อนข้างตรงกัน คือเกิดและตายพร้อมกัน หรืออย่างน้อยตัว component ต้อง life time ไม่กว้างกว่าตัว MyClass เรียกว่าเป็นความสัมพันธ์แบบ Object Composition
- object ใหญ่ (MyClass) จะเรียกด้วยชื่อทั่วๆ ไปว่า Composite [1]
- object ที่เป็นองค์ประกอบภายใน object ใหญ่ จะเรียกด้วยชื่อทั่วๆ ไปว่า Component
ตัวอย่างในภาพเป็นคลาสที่ลดทอนเพื่อให้เข้าใจง่าย ในความเป็นจริง ถ้าคลาสเล็กๆ มีแค่ 4 fields และ 4 methods อาจไม่ต้องทำ Extract Class แต่ในงานจริงคลาสที่ควรจะต้องทำ Extract Class มักจะซับซ้อนกว่านี้มาก
สามารถอ่านเรื่อง Extract Class เพิ่มเติมได้จาก [1]
อ้างอิง
[1] Martin Fowler, Refactoring, 1st edition, 1999, p.198 "Change Unidirectional Association to Bidirectional".
ตอนที่ 10: ประโยชน์ของ Information Hiding (public - private)
คำศัพท์ควรรู้
class member : สมาชิกที่อยู่ในคลาส ได้แก่ fields (ตัวแปรในคลาส) และ methods (functions ในคลาส)
สมาชิกในคลาสอาจแบ่งออกได้เป็นสองกลุ่มคือกลุ่มที่เป็น public เข้าถึงได้หมด กับกลุ่มที่เป็น private เข้าถึงได้เฉพาะจากสมาชิกในคลาสเดียวกัน หรือเรียกอีกอย่างว่าส่วน interface กับส่วน implementation
การแบ่งส่วนแบบนี้เรียกว่าการทำ information hiding คือการซ่อนส่วน implementation ไว้แล้วเปิดเผยเฉพาะส่วนที่จำเป็นคือส่วน interface
ตัวอย่าง
จากตัวอย่างในภาพ จะเห็นว่าส่วน private มีจำนวนมาก แต่เมื่อจะทำความเข้าใจโค้ด เราจะศึกษาจากส่วน public ก่อนซึ่งในภาพมีแค่ 5 methods ก็จะพอเห็นภาพว่าคลาสนี้ทำงานอะไร เรียกใช้งานอย่างไร
หมายเหตุ: โค้ดในภาพเป็นโค้ดของ compiler program ที่ผู้เขียนเคยทำขึ้น คลาส ControlFlowGenerator ทำหน้าที่สร้างโค้ดภาษา Assembly จากคำสั่งควบคุม (จำพวก if หรือ loop) เหตุที่เลือกตัวอย่างนี้เพราะต้องการให้เห็นว่าแม้โค้ดจะซับซ้อนแต่ถ้าส่วน interface (public) เข้าใจง่าย ก็จะทำความเข้าใจและใช้งานได้ไม่ยากนัก
ประโยชน์
การทำ information hiding จะเกิดประโยชน์อย่างน้อย 3 อย่างคือ
1. อ่านเข้าใจวิธีใช้งานคลาสได้ง่ายขึ้น
2. ลดโอกาสเรียกใช้งานผิด เช่นเผลอไปเรียกใช้บาง method ที่ไม่ได้ต้องการให้คนภายนอกใช้ (ส่วนที่เป็น private) compiler จะแจ้ง error ให้ทราบได้
3. แก้ไขส่วน private ได้โดยไม่กระทบกับ client source code ที่มาเรียกใช้จากภายนอกคลาสเลย
ที่ว่าซ่อน คือซ่อนอะไร?
ซ่อนส่วน implementation หรือรายละเอียดการทำงานภายในคลาสที่ผู้เรียกใช้งานไม่จำเป็นต้องรู้
ซ่อนจากใคร?
แม้จะระบุ private แต่เราซึ่งเป็นเจ้าของ source code ก็สามารถมองเห็นโค้ดทั้งหมดอยู่ดี การซ่อนที่เกิดขึ้นไม่ได้ซ่อนจากเจ้าของผู้เขียนโค้ด แต่เป็นการซ่อนจากโค้ดของคลาสอื่นๆ ทำให้คลาสอื่นๆ ไม่สามารถเรียกใช้ได้โดยตรง
ภาษาที่ไม่เป็น OOP จะใช้หลัก information hiding ได้ไหม?
ในภาษาที่ไม่ได้รองรับ OOP เช่นภาษาซีก็สามารถแบ่งแยกส่วน interface และ implementation ได้ โดยอาจใช้การ comment เพื่อจดบันทึกไว้ว่า function ใดให้โค้ดภายนอกใช้งาน function ใดใช้เป็นการภายใน แต่จุดที่ทำไม่ได้คือ compiler ไม่สามารถป้องกันการเรียกใช้ผิดวิธีแล้วแจ้ง error ให้เราทราบได้
ถ้าไม่ซ่อนจะเป็นเช่นไร?
ถ้าไม่ซ่อนส่วน implementation ก็คือเราไม่แยกว่า function ใดบ้างที่ใช้เป็นการภายใน function ใดบ้างยอมให้โค้ดภายนอกเรียกใช้ได้ ผลก็คือ
1. จะอ่านโค้ดทำความเข้าใจวิธีใช้งานยาก
2. อาจเรียกใช้งานผิดวิธีได้ เช่นเรียก function ที่ใช้เป็นการภายใน หรือไปเปลี่ยนค่าตัวแปรบางตัวโดยตรง
3. แก้ไขโค้ดยาก เพราะไม่รู้ว่าแก้ตรงไหนแล้วจะไปกระทบกับใครบ้าง
ทั้งนี้ในการทำ Information Hiding ยิ่งส่วน interface (public) มีบริเวณเล็กเท่าไร ยิ่งเกิดผลดีเท่านั้น
ตอนที่ 9 สองด้านของ Class
(interface and implementation)
คำศัพท์
client = โค้ดที่มาเรียกใช้งาน class (client อาจจะหมายถึง method หรือ class อื่นใดก็ได้) ความหมายของ client ในที่นี้ไม่เกี่ยวกับเรื่อง client/server ในทาง networking แต่อย่างใด
คลาสคลาสหนึ่งอาจจะมีรายละเอียดภายในได้มากมาย แต่ client ที่จะมาเรียกใช้งานคลาส ไม่จำเป็นต้องรู้รายละเอียดทั้งหมดนั้น client ขอเพียงรู้รายละเอียดที่เพียงพอต่อการเรียกใช้งานคลาสได้ถูกต้อง ก็เพียงพอแล้ว
ด้วยเหตุนี้จึงแบ่งองค์ประกอบภายในคลาสได้เป็นสองกลุ่ม ได้แก่
1. interface คือส่วนที่ client จำเป็นต้องรู้เพื่อเรียกใช้งานคลาส
2. implementation คือรายละเอียดการทำงานภายในคลาส (client ไม่จำเป็นต้องรู้)
ในภาษา Java หรือ C # จะใช้ keyword "public" เพื่อแสดงถึงส่วนที่โค้ดภายนอกเรียกใช้ได้โดยตรง และจะใช้ keyword "private" เพื่อแสดงถึงส่วนที่ไม่ให้โค้ดภายนอกเรียกใช้งานโดยตรง
Interface จึงได้แก่
- ส่วนที่เป็น public แต่จะยกเว้น method body เนื่องจาก method body ถือเป็นรายละเอียดการทำงานภายใน
- นอกจากนั้นยังมีส่วนที่ไม่ปรากฎในโค้ดตรงๆ ได้แก่ contract หรือ "คำสัญญา" ว่าแต่ละ method จะทำงานอะไรเมื่อได้รับค่าอะไรเข้ามา ตรงนี้มักอยู่ในคำบรรยายคลาสใน document ไม่ได้อยู่ใน source code ตรง ๆ
Implementation คือส่วนที่ client ไม่จำเป็นต้องรู้ ได้แก่
- private members ทั้งหมด (ทั้ง methods และ fields)
- method body ทั้งหมด (ทั้ง public & private)
ประเด็นข้อถกเถียง
1. ส่วน method body ถือเป็น implementation detail เนื่องจาก การเปลี่ยนแปลงภายใน method body ไม่ส่งผลกระทบกับ client (หากไม่ break contract)
แต่สำหรับบางมุมมอง อาจมองง่ายๆ ว่า public ทั้งหมด คือ interface และ private ทั้งหมด คือ implementation ก็แล้วแต่การจะให้นิยาม
2. คำว่า implementation อาจตีความอีกอย่างว่ารวมไปถึงส่วน interface ด้วยก็ได้ แล้วแต่นิยามและการตีความ (แล้วแต่การตกลงกันให้สื่อสารกันเข้าใจตรงกัน) แต่ในบทความนี้จะใช้ในความหมายที่ว่า interface & implementation เป็นสองส่วนแยกจากกันชัดเจน ไม่ซ้อนทับกัน
ประโยชน์ของ interface & implementation
ที่ระดับ source code:
คลาสสามารถเปลี่ยนแปลง implementation อย่างไรก็ได้ ตราบใดที่ยังคง interface ไว้ให้คงเดิม ตราบนั้นจะไม่จำเป็นต้องแก้ไข client code
ตัวอย่าง
- การแก้ไข method body
- การเพิ่ม/ลบ/แก้ไข private method
- การเพิ่ม (public หรือ private) fields ในคลาส
- การลบ private fields ในคลาส
สิ่งเหล่านี้ ตราบที่ยังรักษา contract ของคลาสไว้ ก็จะไม่ต้องแก้ไข client source code
ในระดับ binary interface:
หาก project เป็นแบบ static link เมื่อมีการแก้ไขคลาส อย่างไรก็ต้อง compile ใหม่ แม้ client source code จะไม่ต้องแก้ไขเลยก็ตาม
แต่ถ้าเป็น dynamic link อาจไม่ต้อง compile client code ใหม่ หาก binary interface ของคลาสที่ถูกเรียกใช้ยังคงเดิม ประเด็นนี้ไว้จะหาเวลาลงในรายละเอียดอีกครั้ง
ตอนที่ 8: ประโยชน์ของ instance variables (fields) ในการลดการส่ง parameter
ภาษายุคก่อน OOP เช่นภาษาซี ตัวแปรจะแบ่งตาม scope ได้แค่สองแบบคือ
1) global variables : ได้แก่ตัวแปรที่ประกาศภายนอก function จะใช้งานได้ทั้งโปรแกรม คืออ้างถึงได้โดยทุก functions (ถ้าอยู่ต่างไฟล์กันก็แค่ประกาศว่า extern)
2) local variables : ได้แก่ตัวแปรที่ประกาศภายใน function จะใช้งานได้แค่ภายใน function นั้นๆ
ปัญหาคือ ถ้าสอง functions ต้องการใช้งานค่าบางอย่างด้วยกัน ก็มีวิธีการแค่ 2 วิธี
1) นำค่าไปแชร์ไว้ที่ global variable - ปัญหาของวิธีนี้คือ นอกจากสอง functions ที่ต้องการใช้ค่าตัวแปรนั้นแล้ว ยังพลอยทำให้ทุก functions ในโปรแกรมมีสิทธิอ้างถึงได้ ทำให้มีโอกาสบังเอิญไปแก้ค่าตัวแปรได้โดยไม่ได้ตั้งใจ รวมถึงการกลับมาอ่านโค้ดในภายหลัง จะต้องเช็คทั้งโปรแกรมว่ามี function ใดมาแก้ไขตัวแปรนั้นบ้าง
2) ส่งเป็น parameter (argument) ไปให้อีก function - วิธีนี้จะเห็นภาพการส่งค่ากันไปมาได้ดีกว่าวิธีแรก แต่ข้อเสียคือถ้าต้องการแชร์ค่ากันหลายๆ ตัวแปร จะทำให้รายการ parameter ยาวขึ้น และอ่านโปรแกรมยากขึ้นได้
Instance Variable
ภาษาในยุค OOP เช่นภาษา Java มีตัวแปรประเภทที่ 3 คือ instance variables (หรือที่เรียกว่า fields) เป็นตัวแปรที่ประกาศภายนอก method (function) แต่ประกาศอยู่ภายในคลาส
Instance variable มี scope อยู่ระหว่าง global และ local โดยจะยอมให้ทุก method ในคลาสนั้นอ้างอิงถึงได้ จึงเกิดข้อดีคือ ถ้าภายในคลาสเดียวกัน สามารถแชร์ข้อมูลกันได้ผ่าน instance variables โดยไม่ต้องส่งผ่าน parameter list ยาวๆ เกิดข้อดีคือโค้ดจะอ่านง่ายขึ้น
ปัญหาเรื่องโค้ดภายนอกบังเอิญมาแก้ไขค่าตัวแปร ก็ลดลงเนื่องจากโค้ดที่มีสิทธิใช้ตัวแปรจะอยู่แค่ภายในคลาสเท่านั้น (พูดถึง private field เป็นหลัก) และคลาสใน OOP โดยปกติจะมีขนาดไม่ยาวมาก (อาจจะอยู่ที่ไม่กี่ร้อยบรรทัด โดยเฉลี่ย)
ตัวอย่างในภาพคือ method draw() ซึ่งจะวาด rectangle ลงบน window
กรณีที่ 1: draw(window, rectangle) รับ parameter สองตัวเลย ถ้าออกแบบแบบนี้แสดงว่าเผื่อความเป็นไปได้ที่ rectangle หรือ window อาจจะเปลี่ยนไปตามการ call แต่ละครั้ง กรณีนี้ยืดหยุ่นที่สุดแต่ parameter list ก็จะยาวที่สุดเช่นกัน
กรณีที่ 2: draw() ไม่รับ parameter แต่ใช้การอ้างถึง fields: rectangle กับ window ใน object ข้อดีคือโค้ดอ่านง่าย และทำความเข้าใจง่าย ว่า draw() จะวาด rectangle (ที่เก็บอยู่ใน field) ลงบน window (ที่เก็บอยู่ใน field เช่นกัน) กรณีที่มี window เดียวและ rectangle เดียว ใช้วิธีนี้จะเหมาะสมมาก
กรณีที่ 3: draw(rectangle) หาก client code มีหลาย rectangle แต่มี window เดียว จะเลือกใช้วิธีนี้ การที่มี window เดียวเก็บไว้ใน field ช่วยให้ draw() ไปหยิบใช้งานได้เลย
กรณีที่ 4: draw(window) กรณีนี้ก็แสดงว่า rectangle ที่ต้องการวาดจะเก็บไว้ใน field แล้วส่งแต่ window มาให้ แสดงว่ามี window ที่เป็นไปได้มากกว่า 1 แต่มี rectangle ที่ต้องการวาดแค่ 1 ตัว (เคสนี้ดูแล้วอาจจะเกิดยาก แต่ก็แล้วแต่ลักษณะงาน)
จะเห็นได้ว่าหากเลือกเก็บค่าที่จะแชร์ร่วมกันไว้ใน field อย่างเหมาะสม จะช่วยลด parameter list ลงได้มาก และโค้ดจะอ่านเข้าใจง่ายขึ้นด้วย (ถ้าเลือกใช้กรณีที่ 2 ได้น่าจะดีกว่ากรณี 1 เพราะอ่านโค้ดง่ายกว่า แต่ทั้งนี้ขึ้นกับ logic ของแต่ละงาน)
ตอนที่ 7: ที่อยู่ที่เหมาะสมของ method
เขียนไปเขียนมา ครั้งนี้อาจจะลงลึกหน่อยนะครับ ถ้าใช้ศัพท์เฉพาะมากไป หรือบางจุดไม่เข้าใจอย่างไร เดี๋ยวบทความถัดๆ ไปน่าจะช่วยให้กระจ่างขึ้น แต่เรื่องนี้เป็นหัวใจสำคัญของการออกแบบเชิงวัตถุเลย (Object-Oriented Design) จึงอยากนำมาเล่าก่อน
คำศัพท์ควรรู้
- dependency : ความสัมพันธ์ที่คลาสหนึ่งไปเรียกใช้อีกคลาสหนึ่ง
- reuse : การใช้โค้ดที่มีอยู่เดิมซ้ำ ช่วยให้ไม่ต้องเขียนโค้ดใหม่ซ้ำซ้อน
- parameter (หรือ argument) : สิ่งที่ส่งผ่านกันภายในวงเล็บของ method
- client class : คลาสที่มาเรียกใช้งานคลาสอื่น ทำตัวเป็นเหมือน user ของคลาสอื่น
- field : ตัวแปรที่อยู่ภายในคลาส
เมื่อ method หนึ่งต้องเรียกใช้บางอย่างจากคลาสสองคลาส (เช่นคลาส A กับ B) method นี้ควรอยู่ในคลาสใด
มีวิธีออกแบบอย่างน้อย 3 แบบ
1. อยู่ในคลาส A
2. อยู่ในคลาส B
3. อยู่ใน client class
หรือ
(advance) 4. สร้างคลาสใหม่ (มีหลายเทคนิค ซึ่งจะกล่าวถึงในครั้งต่อๆ ไป)
การเลือกจัดวาง method ลงในคลาสใดก็ตาม มีประเด็นที่จะต้องพิจารณา 2 เรื่องคือ
1. จะทำให้คลาสนั้นใหญ่ขึ้นเกินไปไหม
2. อาจทำให้เกิด dependency กับคลาสอื่นเพิ่มขึ้น เนื่องจาก method นั้นอาจจะมีการใช้งานคลาสอื่นที่ method body หรือที่ parameter
ตามตัวอย่างในภาพ
กรณีที่ 1: draw() อยู่ภายใน client class
- ข้อดี : เขียนง่าย เป็นจุดเริ่มต้นที่ดี
- ข้อเสีย : client class มักมีแนวโน้มจะใหญ่ขึ้นเรื่อยอยู่แล้ว สุดท้าย client อาจจะใหญ่จนอ่านโค้ดยาก
โดยปกติตามความเคยชิน เมื่อจะสร้าง method ใดก็มักจะวางไว้ใกล้กับจุดที่จะเรียกใช้ ซึ่งจริงๆ ก็ไม่ผิด และเป็นจุดเริ่มต้นที่ดี
แต่ปัญหาคือเมื่อทำแบบนี้ไปเรื่อยๆ client class จะมีขนาดใหญ่ขึ้นเรื่อยๆ หรืออย่างที่เรียกว่าเป็น god class ซึ่งแนวโน้มมักเป็นเช่นนี้อยู่แล้ว ดังนั้นหาก method ใดแยกไปอยู่คลาสอื่นได้ก็ควรแยก เพื่อให้ client class ไม่ใหญ่เกินไป
กรณี 1.2 จะแสดงให้เห็นว่าหากเรานำ parameter ไปไว้เป็น fields ในคลาส จะช่วยให้คำสั่ง draw() ไม่ต้องรับ parameter และเรียกใช้งานได้ง่ายขึ้น
กรณีที่ 2: draw() อยู่ภายใน Rectangle
- ข้อดี : 1) แบ่งเบางานจาก client class 2) คลาสอื่นๆ อาจมา reuse method นี้ได้
- ข้อเสีย : dependency เพิ่มขึ้น อาจทำให้ reuse ข้าม project ยาก
กรณีนี้ประเด็นเรื่องคลาสใหญ่ขึ้นไม่น่ามีประเด็นมาก เพราะ Rectangle เป็นคลาสเฉพาะด้าน ซึ่งไม่น่าจะใหญ่มากนักอยู่เดิม และมีขอบเขตงานค่อนข้างชัดเจน
ประเด็นที่ต้องพิจารณาคือ dependency ที่เพิ่มขึ้น คือใน draw มีการรับ window ดังนั้น Rectangle จึงมี dependency เพิ่มขึ้นกับคลาส Window หมายความว่าใครก็ตามที่นำ Rectangle ไปใช้งานก็ต้องนำ Window คลาสไปด้วย
ประเด็นนี้หากไม่ได้จะ reuse Rectangle ข้าม project ก็ไม่ค่อยมีปัญหา แต่หากจะนำ Rectangle ไปใช้กับ project อื่น ที่ไม่ได้สนใจเรื่องวาดภาพลงบน Window หรืออาจจะมี Window library อีกคลาสของตัวเอง การ reuse ก็จะทำได้ยากขึ้น และจะมีคลาสที่ไม่จำเป็นติดมามากเกินไปได้
กรณี 3: draw() อยู่ภายใน Window
ข้อดี: 1) แบ่งเบางานจาก client class 2) คลาสอื่นๆ อาจมา reuse method นี้ได้
ข้อเสีย: เหมือนกรณีที่ 2 และเพิ่มประเด็นว่าอาจทำได้ยากหาก Window เป็น class library ที่เราไม่มีสิทธิไปแก้ไข
กรณีนี้ก็คล้ายกับกรณี 2 แต่มีจุดต่างคือ บางครั้งคลาส Window เป็น library class ซึ่งเราไม่มีสิทธิไปแก้ไข ดังนั้นเราจะเพิ่มคำสั่ง draw(rectangle) ลงไปตรงๆ ไม่ได้ หากติดปัญหานี้ อาจต้องพิจารณาใช้เทคนิคที่ 4 คือสร้างคลาสใหม่ (ซึ่งจะหาเวลามากล่าวถึงต่อไป)
หมายเหตุ : กรณีใช้ภาษา C # หรือ Swift มีเทคนิคที่เรียกว่า extension method ทำให้เสมือนว่าสร้าง method เพิ่มใน class ใดๆ ซึ่งเราไม่มี source code ได้ด้วย เทคนิคนี้เป็นวิธีที่ดีในการเพิ่ม method ลงใน class library อย่างไรก็ตามเทคนิคนี้มีข้อจำกัดคือไม่สามารถ access private member ได้จริงๆ เพราะ method ที่ได้ไม่ใช่ method ในคลาสนั้นจริงๆ เป็นแค่เสมือนอยู่ในคลาสนั้น
สารบัญ
1. Object คืออะไร
www.facebook.com/SimpleOOP/posts/105105135961445
2. ตัวอย่างการสร้าง Object "Stack"
www.facebook.com/SimpleOOP/posts/105173729287919
3. ตัวอย่างการสร้าง object "Vector"
www.facebook.com/SimpleOOP/posts/105997745872184
4. Anatomy of Class
www.facebook.com/SimpleOOP/posts/106718479133444
5. Encapsulation
www.facebook.com/SimpleOOP/posts/107814002357225
6. กำเนิด Method
www.facebook.com/SimpleOOP/posts/108958068909485
7. ที่อยู่ที่เหมาะสมของ Method
www.facebook.com/SimpleOOP/posts/109780982160527
8. ประโยชน์ของ instance variables (fields) ในการลดการส่ง parameter
www.facebook.com/SimpleOOP/posts/110816492056976
9. สองด้านของ Class
www.facebook.com/SimpleOOP/posts/108201045661784
10. ประโยชน์ของ Information Hiding (public - private)
www.facebook.com/SimpleOOP/posts/108283068986915
11. การแยกคลาสด้วยเทคนิค Object Composition
www.facebook.com/SimpleOOP/posts/110538832094672
12. การแยกคลาสด้วยเทคนิค Object Composition ในกรณีมี field ที่ต้องใช้ร่วมกัน
www.facebook.com/SimpleOOP/posts/110572442091311
13. การแยกคลาสออกมาเป็น Method Object (Helper Class)
www.facebook.com/SimpleOOP/posts/111499048665317
14. แนะนำหนังสือ OOP & OOD ที่น่าอ่าน
www.facebook.com/SimpleOOP/posts/145338151948073
15. ควรรวม class หรือแยก class ดี ?
www.facebook.com/SimpleOOP/posts/182182881596933
16. Behavior (method) ควรจัดวางอยู่ในคลาสใดดี ?
www.facebook.com/SimpleOOP/posts/213875941760960
ตอนที่ 6: กำเนิด Method
ในการสร้างคลาส บ่อยครั้งที่เราจะยังไม่ทราบว่าคลาสนั้นควรจะมี method ใดบ้าง จนกระทั่งได้เขียนโปรแกรมไปสักพักหนึ่งจึงจะพบว่ามีโค้ดบางส่วนที่เหมาะจะแยกออกมาเป็น method ได้ กรณีเช่นนี้เราจะใช้เทคนิค refactoring ที่เรียกว่า extract method และ move method ประกอบกัน เพื่อปรับโครงสร้างโค้ดให้เหมาะสมขึ้น
Refactoring - เป็นเทคนิคการปรับปรุงโครงสร้างโค้ดให้ดีขึ้นโดยยังคงทำงานถูกต้องเหมือนเดิม
- Extract Method (สกัดเมท็อด) : คือการดึงส่วนของโค้ดออกมาสร้างเป็น method ใหม่
- Move Method (ย้ายเมท็อด) : คือการย้าย method จากคลาสหนึ่งไปยังอีกคลาสหนึ่ง
ทั้งสองเทคนิคจะต้องปรับตัวแปรที่เรียกใช้งาน และอาจต้องปรับ argument ของ method ให้เหมาะสมด้วย
ตัวอย่าง
เริ่มต้น มีคลาส Rectangle แทนรูปสี่เหลี่ยมผืนผ้า โดยมี field 4 ค่าได้แก่ left, top, right, bottom แทนตำแหน่งทั้ง 4 ของสี่เหลี่ยม แต่ยังไม่ทราบว่าควรมี method ใดบ้างอยู่ภายใน
เมื่อได้เรียกใช้งาน Rectangle ไปสักพักใน client code พบว่ามีการใช้คำสั่ง area = abs((r.right-r.left)*(r.top-r.bottom)); ซึ่งคำสั่งนี้ ถ้ามาอ่านในภายหลังอาจจะเข้าใจยากว่าใช้ทำอะไร
1. Extract Method
วิธีการทำให้โค้ดนี้อ่านเข้าใจขึ้นคือการแยกโค้ดส่วนนี้ออกมาเป็น method ชื่อว่า getArea() ดังภาพช่องกลาง
2. Move Method
จากนั้นจะสังเกตเห็นว่า getArea() มีการใช้ field ใน Rectangle หลายค่า และไม่ได้ยุ่งเกี่ยวกับค่าอื่นใดใน client code เลย ดังนั้นที่อยู่ที่เหมาะสมของ method getArea() นี้ น่าจะไปอยู่ในคลาส Rectangle มากกว่า
เกร็ดความรู้เพิ่มเติม
- client code : หมายถึงโค้ดอื่นใดที่มาเรียกใช้งานคลาสที่เรากำลังพูดถึงอยู่ คำว่า client ในที่นี้ใช้ในความหมายของ "ผู้ใช้บริการ" โดยไม่ได้เกี่ยวกับศัพท์ทาง network เรื่อง client / server แต่อย่างใด
- Refactoring : สามารถศึกษาได้จากหนังสือ Refactoring ของ Martin Fowler เป็นหนังสือทาง OOP ที่ดีมาก มีสอง edition: โดย edition 1 ใช้ภาษา Java เป็นตัวอย่าง ส่วน edition 2 ใช้ภาษา Javascript ดังนั้นควรเลือกซื้อให้เหมาะสมกับภาษาที่จะใช้
ตอนที่ 5: Encapsulation
Encapsulation เป็นคุณสมบัติสำคัญทาง OOP หมายถึง A) การรวมกลุ่มกันของ data กับ functions ที่เกี่ยวข้อง B) การซ่อนบางสิ่งจากผู้เรียกใช้ (information hiding)
A. การรวมกลุ่ม data กับ functions ที่เกี่ยวข้องกันจะรวมกันกลายเป็นจุดกำเนิดของคลาส (class) นั่นเอง
การรวมกลุ่ม data กับ functions ที่เกี่ยวข้องกันมีประโยชน์อย่างไร ?
1. อ่านทำความเข้าใจโปรแกรมได้ง่ายขึ้น โดยสามารถทำความเข้าใจไปทีละคลาส เพราะแต่ละคลาสจะมีภายในที่ประกอบด้วย data & functions ที่เกี่ยวข้องกัน จับกลุ่มไว้ให้อยู่แล้ว
2. ลดปัญหาชื่อตัวแปรภายในคลาส (fields) และชื่อฟังก์ชั่นภายในคลาส (methods) ซ้ำกันกับส่วนอื่นของโปรแกรม เนื่องจากชื่อ fields & methods ในคลาสหนึ่งสามารถซ้ำกันกับคลาสอื่นได้ เราเพียงแค่ต้องระวังไม่ให้ชื่อคลาสซ้ำกันเท่านั้น
3. ภายใน methods สามารถอ้างถึง fields ใน object เดียวกันได้โดยไม่ต้องใช้ชื่อ object นำหน้า ทำให้โค้ดกระชับดูสะอาดตา อ่านง่าย
4. เปิดโอกาสให้ทำ information hiding ได้ คือการซ่อนบางอย่างให้อยู่แค่ภายในคลาส คนภายนอกคลาสไม่สามารถเข้าถึงได้โดยตรง
B. Information hiding คืออะไร ?
เป็นการซ่อนองค์ประกอบภายในคลาสบางอย่างจากผู้เรียกใช้งานภายนอกคลาส องค์ประกอบที่ซ่อนได้ ได้แก่ fields กับ methods โดยในภาษา C++, Java, และ C # จะทำ information hiding โดยใช้คำว่า "private" เพื่อแสดงว่าสิ่งนั้นห้ามคนภายนอกคลาสเรียกใช้งาน (ส่วน "public" ใช้บอกว่าสิ่งนั้นให้คนภายนอกใช้งานได้)
Information hiding มีประโยชน์อย่างไร ?
1. ผู้ใช้งาน object จะเห็นภาพชัดขึ้นว่ามีอะไรให้เรียกใช้ได้บ้าง และอะไรที่ไม่ให้เรียกใช้งาน
2. ป้องกันผู้ใช้งาน object ใช้งานผิดวิธีโดยไม่ได้ตั้งใจ
3. เกิดการแบ่งแยก interface และ implementation ที่ชัดเจนขึ้น
- ส่วนที่ให้โค้ดภายนอกเรียกใช้งานได้ เรียกว่าเป็น interface ของคลาส (ใช้ public keyword กำกับ)
- ส่วนที่ใช้เป็นการภายใน เรียกว่าเป็น implementation ของคลาส (ใช้ private keyword กำกับ)
ตอนที่ 4: Anatomy of Class
จากที่เรารู้ว่า object ประกอบด้วย data + functions ที่เกี่ยวข้องกันรวมกลุ่มเข้าด้วยกัน
แต่การจะสร้าง object ขึ้นมาใช้งานได้ สำหรับภาษาในกลุ่มที่เรียกว่า class-based languages เช่นภาษา Java หรือ C # จะต้องการให้ programmer เขียนโค้ดระบุโครงสร้าง class ขึ้นมาก่อน ตัวอย่างดังในภาพ (ภาษา Java)
Class คือการระบุองค์ประกอบของ object ว่าถ้าจะสร้าง object ขึ้นชิ้นหนึ่ง object นั้นจะมีองค์ประกอบอะไรบ้าง คลาสจึงเป็นเสมือนแม่แบบ (template) ที่ใช้ในการสร้าง object ขึ้นมาจริง ๆ
- class ถูกเขียนขึ้น ณ coding time
- object เกิดขึ้น ณ run time
คลาสคลาสหนึ่งจะสามารถสร้าง object ขึ้นมาได้มากกว่า 1 object ได้ (โดย object เหล่านั้นจะจัดเป็นประเภทคลาสเดียวกันหมด)
Object แต่ละตัวจะจัดเป็นประเภทคลาสใดคลาสหนึ่ง ตามคลาสที่ใช้สร้างมันขึ้นมา
องค์ประกอบภายในคลาสมีสองส่วนเช่นเดียวกับ object ก็คือ
1. ส่วนของข้อมูล (data) หรือตัวแปรภายในคลาสจะเรียกว่า "fields"
2. ส่วนของคำสั่ง (code) หรือ functions ภายในคลาส จะเรียกว่า "methods"
ส่วนคำว่า constructor ก็ใช้เรียก function ชนิดพิเศษที่จะถูกเรียกใช้งานโดยอัตโนมัติเมื่อสร้าง object ขึ้น (เช่นโดยใช้คำสั่ง new) โดยทั่วไปก็จะทำหน้าที่ตั้งค่าเริ่มต้นให้กับ fields ต่างๆ ภายใน object นั้นๆ
ตอนที่ 3: ตัวอย่างการสร้าง object "Vector"
หากเราต้องการเก็บ Vector 2 มิติซึ่งภายในมีค่า x และ y เป็น float ทั้งคู่ และต้องการให้มีสอง functions ดังนี้
1. function คำนวณ size ของ vector (ใช้สูตรปีธากอรัส size = รากที่สองของ (x*x + y*y) )
2. function คำนวณ unit vector (หาได้โดยนำ vector ตั้งต้น หารด้วยขนาดของ vector นั้น)
ตอนที่ 2: ตัวอย่างการสร้าง Object "Stack"
Stack หรือกองซ้อน คือโครงสร้างข้อมูลที่มี operations หลักสองอย่างคือ
- push ทำหน้าที่นำข้อมูลหนึ่งค่าไปใส่อยู่บนสุดของกอง (เปรียบเหมือนมีกองหนังสืออยู่ แล้วเรานำหนังสือเล่มใหม่วางไปบนสุดของกองหนังสือ)
- pop ทำหน้าที่นำข้อมูลตัวบนสุดออกจากกองแล้วส่งค่ากลับมาให้ผู้เรียกใช้งาน (เปรียบเทียบเหมือนเราหยิบหนังสือเล่มบนสุดออกจากกอง)
ในภาพที่ 1 และ 2 ก่อนจะเป็น OOP เรามาดูว่าในภาษาซี จะเขียน stack ขึ้นมาใช้งานอย่างไร
และในภาพที่ 3 จะมาดูการสร้าง Stack ในภาษา Java (แบบใช้ OOP เต็มรูปแบบ)
ตอนที่ 1: Object คืออะไร
ก่อนหน้าที่จะมีแนวคิด object-oriented programming (OOP) การออกแบบโปรแกรมจะมอง data กับ functions แยกกัน เช่นมีตัวแปร 50 ตัว มี functions อีก 100 functions ที่จัดการกับตัวแปรเหล่านั้น
ต่อมาแนวคิด OOP เกิดจากการสังเกตว่า data กับ functions ที่เกี่ยวข้องกันสามารถจับกลุ่มกันได้เป็นกลุ่มๆ การจับกลุ่มรวมกันของ data กับ functions ที่เกี่ยวข้องกันก็เรียกว่า class/object (สองคำนี้มีความแตกต่างกันนิดหน่อย แต่ให้มองรวมๆ ไปก่อน)
ทีนี้เมื่อรวมกลุ่ม data กับ functions เข้าด้วยกันได้ จะพบว่าโค้ดภายนอกที่มาเรียกใช้งาน มักไม่มีความจำเป็นต้องเข้าถึง data หรือ functions บางตัว จึงเกิดแนวคิดของการซ่อนการเข้าถึง (information hiding) ซึ่งมีข้อดีคือทำให้แก้โปรแกรมได้ง่ายขึ้น เพราะส่วนที่ซ่อนไว้ (private) จะไม่ถูกเรียกใช้งานจากภายนอก จึงแก้ได้ง่ายกว่า
หลักการสองอย่างที่กล่าวไปนี้รวมกันเรียกว่าหลักการ Encapsulation คือ 1. รวม data กับ function ที่เกี่ยวข้องกันเข้าด้วยกัน 2. ซ่อน data หรือ function บางอย่างจากคนภายนอก
ประเด็นสำคัญของการคิดแบบ OOP อยู่ที่ว่าอะไรควรรวมกลุ่มกัน อะไรควรแยกกันอยู่ แล้วสิ่งที่แยกกันอยู่ หากยังจำเป็นต้องติดต่อกันก็ใช้การ pass parameter ส่งให้กัน หรือรับเข้ามาแล้วเก็บไว้เป็นตัวแปรภายใน class/object ก็ได้ ความยากของการออกแบบจะอยู่ที่การแบ่งคลาสและออกแบบวิธีการติดต่อกันระหว่างแต่ละคลาสนี่ล่ะครับ
เพจนี้ตั้งใจว่าจะเล่าหลักการเขียนโปรแกรมเชิงวัตถุ (OOP) และการออกแบบเชิงวัตถุ (OOD) ที่ผู้เขียนสนใจศึกษามานานหลายสิบปี อ่านตำรามาก็เยอะ ลองผิดลองถูกมาก็มาก จึงอยากเอามาถ่ายทอดให้ เพื่อลดระยะเวลาในการศึกษาเรื่อง Object-Oriented Programming