ทำความรู้จักกับ Composite Design Pattern
เวลาเราเขียนโปรแกรม เรามักจะเจอปัญหาเรื่องการจัดการกลุ่มข้อมูลที่มีความซับซ้อน เช่น การทำระบบตะกร้าสินค้าที่มีทั้งสินค้าชิ้นเดียว และสินค้าที่เป็นชุดเซต ถ้าเราเขียนโค้ดแยกกันตลอดเวลา เราจะเหนื่อยมากเมื่อต้องเพิ่มฟีเจอร์ใหม่ๆ เข้าไป
Composite Design Pattern (รูปแบบการออกแบบเชิงโครงสร้างที่ช่วยจัดการกลุ่มวัตถุ) คือทางออกของปัญหานี้ มันช่วยให้เรามอง "ของชิ้นเดียว" กับ "กลุ่มของ" เป็นสิ่งเดียวกันได้ผ่านหน้าตาที่เหมือนกัน หรือที่เรียกว่า Interface (ข้อตกลงที่กำหนดว่าคลาสต้องมีฟังก์ชันอะไรบ้าง)
ลองจินตนาการถึงโครงสร้างต้นไม้ดูครับ กิ่งไม้หนึ่งกิ่งอาจจะมีใบไม้ติดอยู่ หรืออาจจะมีกิ่งย่อยแยกออกไปอีกก็ได้ แต่ไม่ว่าจะเป็นใบหรือกิ่ง ทั้งหมดต่างก็เป็นส่วนหนึ่งของต้นไม้เหมือนกัน การใช้แพทเทิร์นนี้จะช่วยให้โค้ดของคุณดูสะอาดและขยายต่อได้ง่ายขึ้นมากครับ
เมื่อไหร่ที่ควรใช้ Composite Pattern
เราควรใช้แพทเทิร์นนี้เมื่อโปรเจกต์ของคุณมีโครงสร้างแบบ Hierarchy (ลำดับชั้นเหมือนต้นไม้) เช่น ระบบไฟล์ในคอมพิวเตอร์ที่มีโฟลเดอร์ซ้อนโฟลเดอร์ หรือระบบองค์กรที่มีพนักงานและแผนกย่อย
ถ้าคุณพบว่าตัวเองต้องเขียนคำสั่งตรวจสอบประเภทข้อมูลบ่อยๆ เช่น "ถ้าเป็นสินค้าเดี่ยวให้ทำแบบนี้ แต่ถ้าเป็นชุดเซตให้ทำอีกแบบ" นั่นคือสัญญาณเตือนว่าโค้ดของคุณเริ่มมีปัญหาแล้ว เพราะเมื่อไหร่ที่มีประเภทข้อมูลใหม่เข้ามา คุณจะต้องกลับมาแก้ไขโค้ดเดิมซ้ำๆ จนเสี่ยงที่จะเกิดบั๊ก (ข้อผิดพลาดในโปรแกรม) ได้ง่าย
การใช้ Composite จะทำให้คุณตัดปัญหาการเช็คประเภทข้อมูลทิ้งไปได้เลย คุณแค่สั่งงานผ่านตัวกลางเดียว ทุกอย่างในระบบจะจัดการตัวเองได้ตามหน้าที่ของมัน ไม่ว่าจะเป็นชิ้นงานเล็กหรือกลุ่มก้อนขนาดใหญ่
สร้างโครงสร้างพื้นฐานด้วย Interface
หัวใจสำคัญคือการสร้างตัวกลางที่ทุกอย่างต้องทำตาม ขั้นตอนนี้สำคัญมาก เพราะมันจะเป็นสัญญาว่าไม่ว่าจะเป็นของเดี่ยวหรือกลุ่ม ต้องมีฟังก์ชันการทำงานที่ชื่อเหมือนกันเป๊ะ
เราจะเริ่มจากการประกาศ Abstract Class (คลาสแม่ที่ทำหน้าที่กำหนดโครงสร้างแต่ยังไม่ใส่รายละเอียด) หรือ Interface เพื่อกำหนดวิธีการทำงาน เช่น ถ้าเป็นระบบตะกร้าสินค้า ก็ต้องมีฟังก์ชันคำนวณราคา
มือใหม่มักพลาดตรงที่ลืมกำหนดให้ทั้งของเดี่ยวและกลุ่มสืบทอดมาจากคลาสเดียวกัน ทำให้ตอนใช้งานจริงต้องมานั่งแยกประเภทข้อมูลอีกรอบ ซึ่งจะทำให้แพทเทิร์นนี้ไม่ทำงานตามความตั้งใจครับ
// ส่วนของ Interface ที่ทุกคนต้องทำตาม
abstract class PriceComponent {
// ฟังก์ชันคำนวณราคาที่ทุกคลาสต้องมี
double getPrice();
}
บรรทัดที่ 2 คือการประกาศคลาสแม่ และบรรทัดที่ 4 คือฟังก์ชัน getPrice() ซึ่งเป็นสัญญาว่าใครที่ดึงคลาสนี้ไปใช้ ต้องเขียนวิธีการคำนวณราคาเองครับ ผลลัพธ์คือเราได้มาตรฐานกลางที่ทุกส่วนในระบบต้องปฏิบัติตาม
แยกชิ้นงานเดี่ยวและกลุ่มออกจากกัน
หลังจากมีสัญญาแล้ว เราจะสร้าง Leaf (วัตถุชิ้นสุดท้ายที่ไม่มีลูก) และ Composite (วัตถุที่บรรจุวัตถุอื่นไว้ข้างใน) ขึ้นมา โดย Leaf จะทำหน้าที่คำนวณราคาของตัวเองตรงๆ ส่วน Composite จะทำหน้าที่รวมราคาจากลูกๆ ของมัน
ข้อดีของวิธีนี้คือ Composite ไม่จำเป็นต้องรู้ว่าลูกของมันเป็นสินค้าเดี่ยวหรือเป็นชุดเซตอื่น มันรู้แค่ว่าลูกทุกคนสามารถตอบคำถาม getPrice() ได้เหมือนกันหมด ทำให้ระบบมีความยืดหยุ่นสูงมาก
จุดที่ต้องระวังคือการจัดการ Collection (กลุ่มข้อมูล) ในคลาส Composite อย่าลืมเขียนฟังก์ชันเพิ่มหรือลบสมาชิกให้เรียบร้อย เพื่อให้เราสามารถจัดกลุ่มข้อมูลได้ตามต้องการในภายหลังครับ
// สินค้าชิ้นเดียว (Leaf)
class CartItem extends PriceComponent {
final double price;
CartItem(this.price);
@override
double getPrice() => price;
}
// กลุ่มสินค้า (Composite)
class ItemBundle extends PriceComponent {
final List<PriceComponent> _items = [];
void add(PriceComponent item) => _items.add(item);
@override
double getPrice() {
// รวมราคาสินค้าทุกชิ้นในกลุ่ม
return _items.fold(0, (sum, item) => sum + item.getPrice());
}
}
ในคลาส CartItem เราแค่ส่งราคาคืนไปตรงๆ ส่วน ItemBundle เราใช้ fold (ฟังก์ชันรวมค่าในรายการ) เพื่อบวกราคาจากทุกคนที่อยู่ในลิสต์ ผลลัพธ์คือเราสามารถคำนวณราคาสินค้าเป็นชิ้น หรือราคาทั้งตะกร้าได้ด้วยคำสั่งเดียว
ตัวอย่างการนำไปใช้งานจริง
เมื่อทุกอย่างพร้อม เราก็สามารถนำมาประกอบกันเป็นโครงสร้างที่ซับซ้อนได้ง่ายๆ คุณสามารถใส่สินค้าลงในชุดเซต และใส่ชุดเซตลงในตะกร้าใหญ่ได้โดยไม่มีปัญหา เพราะทุกอย่างมองว่าเป็น PriceComponent เหมือนกันหมด
การเขียนแบบนี้ช่วยให้ Main Code (โค้ดหลักที่สั่งรันโปรแกรม) ไม่ต้องรับรู้รายละเอียดภายในเลย มันแค่สั่งให้ตะกร้าคำนวณราคา แล้วตะกร้าก็จะไปไล่เรียกราคาจากสินค้าทุกชิ้นเองโดยอัตโนมัติ
มือใหม่มักจะลืมเช็คว่าถ้าลิสต์ว่างเปล่าจะเกิดอะไรขึ้น อย่าลืมเขียนโค้ดรองรับกรณีที่ไม่มีสินค้าในตะกร้าไว้ด้วยนะครับ เพื่อป้องกันโปรแกรมพัง (Crash) ระหว่างรัน
void main() {
final burger = CartItem(100);
final drink = CartItem(50);
final combo = ItemBundle();
combo.add(burger);
combo.add(drink);
// สั่งคำนวณราคาชุดเซต
print('ราคาชุดเซต: ${combo.getPrice()}');
}
โค้ดนี้สร้างสินค้า 2 ชิ้นแล้วเอาไปรวมใน combo จากนั้นสั่ง combo.getPrice() ผลลัพธ์ที่ได้คือ 150.0 ซึ่งเกิดจากการรวมราคา 100 + 50 โดยอัตโนมัติครับ
บทสรุปและการนำไปใช้
Composite Design Pattern คือเครื่องมือทรงพลังที่เปลี่ยนงานยากให้กลายเป็นเรื่องง่าย โดยการมองกลุ่มข้อมูลและข้อมูลเดี่ยวให้เป็นมาตรฐานเดียวกัน สิ่งนี้ช่วยให้โปรแกรมของคุณขยายตัวได้โดยไม่ต้องแก้ไขโค้ดเดิมบ่อยๆ
ลองนำไปปรับใช้กับโปรเจกต์ที่กำลังทำอยู่ดูครับ เช่น ถ้าคุณกำลังทำแอปจัดการรายรับรายจ่าย ลองใช้แพทเทิร์นนี้จัดการหมวดหมู่ค่าใช้จ่ายที่มีรายการย่อยๆ อยู่ข้างในดู มันจะช่วยให้การสรุปยอดรายเดือนกลายเป็นเรื่องง่ายเพียงแค่เรียกฟังก์ชันเดียว
การฝึกฝนใช้ Design Pattern ไม่ใช่แค่การจำรูปแบบ แต่คือการเข้าใจว่า "ทำไม" เราถึงต้องออกแบบโค้ดแบบนี้ หากคุณฝึกทำตามนี้บ่อยๆ คุณจะเริ่มมองเห็นโครงสร้างของปัญหาได้ชัดเจนขึ้น และก้าวเข้าสู่การเป็นโปรแกรมเมอร์ที่เขียนโค้ดได้มืออาชีพมากขึ้นแน่นอนครับ
ที่มา: The Composite Design Pattern: How to Work with Individual Objects and Groups Through the Same Interface — freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More