[Design Pattern#3] รู้จัก Composition over Inheritance หลักการออกแบบคลาสให้ยืดหยุ่น
ผมเชื่อว่าเพื่อนๆหลายคนเวลาออกแบบคลาสคิดอะไรไม่ออกก็จับ inherite ไว้ก่อนใช่ไหมฮะ inheritance หรือการสืบทอดนั้นมีประโยชน์ในตัวมันเองอยู่ แต่หลายคนมักนำสิ่งนี้ไปสร้างความปวดร้าวระยะยาวในโค๊ดของเราเอง บทความนี้จะพาเพื่อนๆไปรู้จักการสืบทอดอย่างลึกซึ้ง รวมถึงเส้นทางอันไม่ได้โรยด้วยกลีบกุหลาบของการใช้ inheritance แต่เป็นตะปูเรือใบและหนามต้นงิ้ว ตบท้ายด้วยการขายของคือ composition วิธีที่เรานิยมใช้กันมากกว่าครับ
คิดไม่ออก Inherit ก่อนเลย
อยากได้คุณสมบัติที่มีในคลาส A มาใส่คลาส B แต่คิดไม่ออกว่าจะออกแบบยังไงดี เอาหวะงั้นก็ให้ B inherits มาจาก A ซะเลยหมดเรื่อง แค่นี้คลาส B ของเราก็สืบทอดอะไรที่เราอยากได้มาจาก A แล้วง่ายจัง ปิดจ๊อบแล้วไปจิบกาแฟในห้องประชุม
โลกสีน้ำเงินใบนี้มี desing pattern ตัวหนึ่งชื่อ Active Record Pattern เจ้าแพทเทิร์นนี้บอกไว้ว่าให้เราสร้างอ็อบเจ็กต์ที่เป็นตัวแทนของ record ในฐานข้อมูลเรา บวกกับความสามารถในการ insert, read, update และ delete เข้าไปในคลาสซะเลย มีหลายเฟรมเวิร์คที่นำแพทเทิร์นนี้มาประยุกต์ใช้เพราะมันง่าย เช่น Ruby on Rails
1# ใน ActiveRecord::Base นิยาม create, update, delete ไว้เรียบร้อย2# คลาสนี้เมื่อสืบทอดมาจาก ActiveRecord::Base จึงได้ความสามารถในการ CRUD ไปด้วย3# นอกจากนี้อ็อบเจ็กต์ของคลาสนี้เป็นตัวแทนของข้อมูลแต่ละ record ในฐานข้อมูล4# เราจึงเปลี่ยนแปลงแก้ไขข้อมูลได้โดยตรงภายใต้คลาสนี้5class Book < ActiveRecord::Base6end78# ตัวอย่างการใช้งาน9# สร้าง book ขึ้นมาโดยมี title เป็น BabelCoder: The first book!10book = Book.create(title: 'BabelCoder: The first book!')11# อัพเดท title ก็ได้12book.update(title: 'Foo-Bar')13# เข้าถึงข้อมูลใน record นี้ก็ได้14book.title
พอเป็นการ inherit ปุ๊บ ชีวิตดี๊ดี ทุกอย่างดูง่ายไปหมด โลกเป็นสีชมพูเหมือนมีแฟนนั่งเขียนโค๊ดอยู่ข้างๆ~
ความเชื่อผิดๆเกี่ยวกับ Inheritance
ถ้าเราจำสมัยเราเรียนในมหาวิทยาลัยได้ อาจารย์ก็จะบอกว่าเรามีความสัมพันธ์อยู่สองแบบนะ แบบแรกคือ is-a relationship และอีกแบบคือ has-a relationship สำหรับเพื่อนๆที่ลืมแล้ว เรามาทบทวนกันหน่อยดีกว่าครับ
- is-a relationship คือความสัมพันธ์ที่สิ่งหนึ่งเป็นอีกสิ่งด้วยในเวลาเดียวกัน ก็นะ is มันแปลว่าเป็นหรือคือไง ตัวอย่างความสัมพันธ์แบบนี้เช่น สุนัขเป็นสัตว์ หมึกเป็นหอย ผู้ชายเป็นคน(เจ็บเป็นเหมือนกันนะ) และความสัมพันธ์ประเภทนี้หละครับที่เราจะใช้การเชื่อมความสัมพันธ์แบบ inheritance
- has-a relationship คือความสัมพันธ์ที่สิ่งหนึ่งมีอีกสิ่งหนึ่ง ตัวอย่างเช่น มนุษย์มีความสามารถในการเดิน ผู้หญิงมีความสามารถในการตั้งครรภ์ เป็นต้น ความสัมพันธ์ประเภทนี้เราจะเรียกว่า composition (การนำมาประกอบกัน)
กลับไปดู Book และ ActiveRecord กันซะหน่อย เพื่อนๆจะพบว่า Book ไม่ได้เป็น ActiveRecord นะ แต่เรากำลังจับมันให้เกี่ยวข้องกันผ่านการสืบทอด การออกแบบเช่นนี้จึงผิดหลักการ
Book ของเราเมื่อสืบทอดมาจาก ActiveRecord แล้ว ตัวมันเองจึงอุดมไปด้วยสารพัดเมธอดจากคลาสแม่ แม้ว่าตัวมันเองจะไม่ได้ต้องการเลยก็ตาม การนำสองสิ่งที่ไม่สัมพันธ์กันมาสืบทอดกันจึงเป็นการผูกความสัมพันธ์ให้แน่นขึ้นและแยกออกจากกันยาก
จินตนาการถึงการทดสอบ Book ของเราครับ เราทราบว่า ActiveRecord นั้นต้องเชื่อมต่อกับฐานข้อมูลใช่ไหมครับ นั่นหมายความว่าในการทดสอบ Book ของเรา เราจะทดสอบทุกๆอย่างไม่ได้หากปราศจากฐานข้อมูล (ได้แค่บางอย่าง) ทั้งๆที่เราควรจะทดสอบ Book ได้ด้วยความพยายามที่น้อยที่สุดแท้ๆ
ปัญหาของ Inheritance
อุตสาหกรรมซอฟต์แวร์นั้นความต้องการของลูกค้าไม่เคยนิ่ง โค๊ดของเราจึงมีโอกาสเปลี่ยนได้ตลอดเวลา ลองมาดูกันซิว่าถ้าความต้องการลูกค้าเปลี่ยนไป คลาสที่อาศัยการสืบทอดเป็นหลักจะกระทบอะไรบ้าง
ปัญหาแรก
แรกเริ่มเรามี Dog, Cat และ Fish ทั้งสามชนิดนี้ล้วนเป็น Animal ใช่ไหมครับ ดังนั้นเราเลยจับมันสืบทอดมาจากคลาส Animal ซะเลย โดยสัตว์ทั้งสามประเภทนี้ล้วนหายใจ (breathe) ได้เราจึงย้ายเมธอดหายใจไปไว้ใน Animal เมธอดที่จำเพาะเฉพาะสัตว์แต่ละชนิดก็เก็บไว้ในคลาสตนเอง ได้แก่
- Dog สามารถเห่า (bark) ได้
- Cat สามารถร้องเหมียวๆ (meow) ได้
- Fish สามารถว่ายน้ำ (swim) ได้
ต่อมาลูกค้าของเราให้โจทย์ใหม่ว่า สุนัขและแมวต้องเดินได้นะแต่สำหรับปลา... ถ้าไม่ใช่ปลาตีนก็ไม่ควรเดินได้เปล่า? เราจะออกแบบคลาสยังไงดี? ครั้นจะเอา walk ไปใส่ทั้งในคลาส Dog และ Cat มันก็เป็นการซ้ำซ้อน (duplicate) ใช่ไหมครับ? งั้นเราย้ายไปใส่ Animal แล้วกันโค๊ดจะได้ไม่ซ้ำซ้อนไง
ปัญหาแรกเกิดกับเราแล้ว ปลามันเดินได้ซะที่ไหนหละ ถ้าเราเอา walk ไปใส่ที่ Animal แสดงว่าปลาที่สืบทอดคุณสมบัติจาก Animal มาด้วยก็ต้องเดินได้ด้วยซิ? ครั้นจะแยก walk ไปใส่แยกที่ Dog และ Cat ก็กลายเป็นโค๊ดซ้ำซ้อน
ปัญหาสอง
ต่อมาลูกค้าผู้เรื่องมากของเราบอกว่า นอกจากจะให้มี Dog แล้ว ยังให้มีตุ๊กตาที่เดินได้ด้วย ด้วยความที่เราฉลาดเราเลยออกแบบไว้ล่วงหน้าซะเลย ให้ WalkableDoll (ตุ๊กตาเดินได้) สืบทอดมาจาก Doll โดยมีเมธอดหนึ่งตัวคือเดิน (walk)
ความต้องการของลูกค้าไม่มีที่สิ้นสุดแม้กำลังเงินจะหยุดจ่ายไปแล้ว เธออยากให้เรามีผลิตภัณฑ์ในตระกูลตุ๊กตาเดินได้ตัวใหม่เป็นตุ๊กตาสุนัขเดินได้ โดยตุ๊กตาตัวนี้ต้องเหมือนสุนัขเลยคือนอกจากจะเดินได้แล้วต้องเห่าได้ด้วย
เราทราบดีว่า WalkableDoll มี walk สำหรับเดิน และ Dog มี bark สำหรับเห่า ถ้าเราอยากให้ตุ๊กตาสุนัขของเราเดินได้ด้วยและเห่าได้ด้วย เราก็ต้องสืบทอด DogDoll ของเรามาจากทั้ง Dog และ WalkableDoll ช่างน่าเสียดายภาษาโปรแกรมสมัยใหม่ไม่อนุญาตให้คุณทำ Multiple Inheritance หรือสืบทอดจากหลายคลาส เพื่อป้องกันปัญหาที่เรียกว่า Diamond Problem1 1: https://en.wikipedia.org/wiki/Multiple_inheritance หัวข้อ The diamond problem
และนี่หละครับคือปัญหาของการสืบทอดคลาส
กอบกู้จักรวาลด้วย Composition
Composition นั้นคือความสัมพันธ์แบบ has-a relationship เป็นความสัมพันธ์แบบที่มนุษย์เข้าใจง่าย เพราะโดยปกติเรามักมองวัตถุว่ามันประกอบด้วยอะไรหรือมันสามารถทำอะไรได้บ้าง มากกว่าที่จะมองว่าวัตถุนั้นสืบทอดมาจากใคร ตัวอย่างมุมมองแบบ composition เช่น เมื่อเรามองรถสีฟ้าคันหนึ่งเราจะนึกคิดทันทีว่า รถคันนี้ประกอบด้วยล้อสีดำ โครงรถสีฟ้า หรือถ้าเรามองหมึกเราก็จะบอกว่าหมึกนี้มีหนวด มีกระดองหมึกด้วย มากกว่าที่จะบอกว่าหมึกนั้นสืบทอดมาจากสัตว์ตระกูลหอย จริงไหมครับ?
แก้ปัญหาแรกด้วย composition
ย้อนมาที่ปัญหาแรกกันครับ เราต้องการเพิ่ม walk ให้กับ Dog และ Cat ถ้าเป็นมุมมองของ composition เราจะมองใหม่ว่า Dog และ Cat มีความสามารถ แทนการบอกว่าสืบทอด ดังภาพข้างล่าง
จากภาพด้านบนเรามี Walkable สำหรับให้คลาสอะไรก็ได้ที่ต้องการความสามารถในการเดินดึงมันไปทำ composition และมีคลาส Breathable เพื่อให้คลาสที่ต้องการความสามารถในการหายใจดึงมันไปทำ composition เช่นกัน ดังนั้น Dog และ Cat ที่ต้องการจะเดินได้จึงต้องผูกความสัมพันธ์แบบ has-a กับ Walkable ในขณะที่น้องปลาผู้น่าสงสารเดินไม่ได้ จึงผูกความสัมพันธ์แบบ has-a กับ Breathable ที่มีเมธอด breathe คือหายใจแค่นั้นพอ
แก้ปัญหาที่สองด้วย composition
การแก้โดยใช้ composition นั้นทำเหมือนกับที่ผมทำให้ดูในปัญหาแรกเลยครับ ตรงนี้ขอเว้นไว้ให้เพื่อนๆลองคิดเป็นแบบฝึกหัดแล้วกันนะครับ
ลงมือเขียนโค๊ดด้วยแนวคิดของ Composition
ตัวอย่างโปรแกรมนี้ผมแสดงด้วย JavaScript นะครับ ในการใช้งาน composition นั้น เพื่อนๆไม่จำเป็นต้องใช้คู่กับคลาสเสมอไปครับ ดังนั้นแล้วแม้แต่ภาษาที่ไม่ใช่ OOP ก็ยังคงประยุกต์เรื่องนี้เข้ากับโค๊ดในภาษานั้นได้เช่นกัน
โจทย์ของเราคือต้องการสร้าง Dog และ Cat โดยมีคุณสมบัติดังนี้
- Dog: เห่าได้ พิมพ์ชื่อออกมาได้และเดินได้
- Cat: เห่าไม่ได้ พิมพ์ชื่อออกมาได้และเดินได้
1// ใช้เดิน2const Walkable = {3 walk() {4 console.log("I'm walking.")5 },6}78// ใช้พิมพ์ชื่อ9const Printable = {10 print() {11 console.log(this.name)12 },13}1415// ใช้เห่า16const Barkable = {17 bark() {18 console.log('ฮ่งๆ')19 },20}2122// หมาพิมพ์ชื่อได้ เดินได้และเห่าได้23const Dog = (name) => ({24 name,25 ...Printable,26 ...Walkable,27 ...Barkable,28})2930// แมวพิมพ์ชื่อได้ เดินได้แต่เห่าไม่ได้31const Cat = (name) => ({32 name,33 ...Printable,34 ...Walkable,35})3637const dog = new Dog('Boo')38dog.print() // Boo39dog.bark() // ฮ่งๆ4041const cat = new Cat('Meaw')42cat.print() // Meaw43cat.walk() // I'm walking
Composition over Inheritance
การที่เราจะนิยามว่าสิ่งหนึ่งสืบทอดจากสิ่งหนึ่งมันไม่เข้ากับความเป็นจริงทางการพัฒนาซอฟร์แวร์ครับ เพราะถ้าเราออกแบบการสืบทอดแล้วแสดงว่าเรานิยามและออกแบบคลาสของเราให้ผูกพันธ์กันเองแต่แรกเลย เมื่อลูกค้าต้องการให้เปลี่ยนแปลงเราจึงแก้ไขโค๊ดที่ผูกติดกันได้ยาก ในทางกลับกัน composition เป็นความสัมพันธ์ที่ยืดหยุ่นกว่าเพราะเรานิยามความสามารถของคลาสแยกออกมา เมื่อคลาสต้องการมีความสามารถใหม่เราก็แค่เลือกความสามารถใหม่ผูกติดเข้าไป เมื่อคลาสไม่ต้องการความสามารถนั้นแล้ว เราก็แค่ถอดความสามารถนั้นออกจากความเป็น has-a
เมื่อการสืบทอดมีข้อจำกัดและ composition ดูใกล้เคียงความจริงของการพัฒนาโปรแกรมมากกว่า นักพัฒนาสมัยใหม่จึงนิยมใช้ composition มากกว่า inheritance และนั่นหละครับคือสิ่งที่เรียกว่า Composition over Inheritance
สรุป
เวลาเพื่อนๆออกแบบคลาสอยากให้จินตนาการก่อนครับว่าคลาสนั้นมีความสามารถอะไรบ้าง พยายามแยกความสามารถนั้นออกเป็นอีกคลาส ในภาษาโปรแกรมสมัยใหม่เริ่มมีคอนเซ็ปต์ที่เรียกว่า Mixin เพื่อนๆสามารถใช้สิ่งนี้สร้างความสัมพันธ์แบบ has-a ได้เช่นกัน
เหนือสิ่งอื่นใด เมื่อเพื่อนๆคิดจะสืบทอดคลาส B มาจากคลาส A โปรดถามตัวเองอีกครั้งครับว่า B ของเราเป็นชนิดหนึ่ง (is-a) ของ A หรือไม่ ถ้าไม่ใช่แล้วอย่าเด็ดขาดที่จะทำการสืบทอดครับ
สารบัญ
- คิดไม่ออก Inherit ก่อนเลย
- ความเชื่อผิดๆเกี่ยวกับ Inheritance
- ปัญหาของ Inheritance
- กอบกู้จักรวาลด้วย Composition
- ลงมือเขียนโค๊ดด้วยแนวคิดของ Composition
- Composition over Inheritance
- สรุป