Composite
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Composite ให้คุณสร้างโครงสร้างต้นไม้แบบ part-whole แล้วปฏิบัติต่อ object เดี่ยวกับกลุ่ม object ผ่าน interface เดียวกัน ฝั่ง client จึงไม่ต้องรู้เลยว่าสิ่งที่ถืออยู่เป็นใบหรือกิ่งทั้งกิ่ง
ลองนึกถึงระบบไฟล์ ไฟล์มีขนาดของตัวเอง ส่วนโฟลเดอร์บรรจุไฟล์และโฟลเดอร์อื่นต่อ ๆ กันไป ขนาดของโฟลเดอร์คือผลรวมของทุกอย่างข้างใน ถ้าคุณ model ไฟล์กับโฟลเดอร์เป็นคนละ type ที่ไม่เกี่ยวกัน code ฝั่ง client ทุกจุดที่เดินสำรวจต้นไม้ต้องถามก่อนว่า “นี่ไฟล์หรือโฟลเดอร์?” แล้วค่อยแยกทาง เงื่อนไขแบบนี้จะกระจายไปทั่วทั้งระบบ และพังทันทีที่คุณเพิ่ม node ชนิดที่สาม
Composite ตัดคำถามนั้นทิ้งไปเลย ทั้งใบและตัวบรรจุต่างก็ implement interface ร่วมตัวเดียวกัน เช่น node ที่มี method size() ใบคืนค่าของตัวเอง ส่วนตัวบรรจุ delegate ลงไปหาลูกแล้วรวมผลลัพธ์ ทำแบบนี้ซ้ำลึกลงไปเท่าที่ต้นไม้จะลึก
ฝั่ง client แค่เรียก size() ที่ root ก็ได้ยอดรวม โดยไม่ต้องสนใจรูปร่างข้างใต้เลย และการเพิ่ม node ชนิดใหม่ก็แค่ implement interface เดิม ไม่ต้องไล่แก้ทุกจุดที่เดินสำรวจต้นไม้
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Node {
<<interface>>
+size() int
+name() string
}
class File {
-bytes: int
+size() int
}
class Folder {
-children: Node[]
+add(child)
+size() int
}
Node <|.. File
Node <|.. Folder
Folder o--> Node : contains children - Component (
Node) — interface ร่วมสำหรับทุกอย่างในต้นไม้ ประกาศการดำเนินการที่ client เรียกใช้ - Leaf (
File) — node ที่ไม่มีลูก implement การดำเนินการเองโดยตรง - Composite (
Folder) — node ที่ถือ component ลูกไว้ และ implement การดำเนินการด้วยการ delegate ลงไปหาลูกแล้วรวมผลลัพธ์ - Client — ทำงานอิงกับ component interface และปฏิบัติต่อใบและ composite แบบเดียวกัน
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ต้นไม้ของระบบไฟล์ที่ทั้งไฟล์และโฟลเดอร์เปิด size() ออกมาเหมือนกัน ขนาดของโฟลเดอร์คือผลรวมของลูกทั้งหมด คำนวณแบบ recursive
interface Node { size(): number;}
class File implements Node { constructor(private readonly bytes: number) {} size = () => this.bytes;}
class Folder implements Node { private children: Node[] = []; add(child: Node): this { this.children.push(child); return this; } size = () => this.children.reduce((sum, c) => sum + c.size(), 0);}
const root = new Folder() .add(new File(100)) .add(new Folder().add(new File(20)).add(new File(30)));
console.log(root.size()); // 150from abc import ABC, abstractmethod
class Node(ABC): @abstractmethod def size(self) -> int: ...
class File(Node): def __init__(self, bytes_: int) -> None: self._bytes = bytes_
def size(self) -> int: return self._bytes
class Folder(Node): def __init__(self) -> None: self._children: list[Node] = []
def add(self, child: Node) -> "Folder": self._children.append(child) return self
def size(self) -> int: return sum(child.size() for child in self._children)
root = Folder()root.add(File(100)).add(Folder().add(File(20)).add(File(30)))print(root.size()) # 150package main
import "fmt"
type Node interface { Size() int}
type File struct{ Bytes int }
func (f File) Size() int { return f.Bytes }
type Folder struct{ children []Node }
func (fo *Folder) Add(child Node) *Folder { fo.children = append(fo.children, child) return fo}
func (fo *Folder) Size() int { total := 0 for _, c := range fo.children { total += c.Size() } return total}
func main() { inner := (&Folder{}).Add(File{20}).Add(File{30}) root := (&Folder{}).Add(File{100}).Add(inner) fmt.Println(root.Size()) // 150}trait Node { fn size(&self) -> u64;}
struct File { bytes: u64,}
impl Node for File { fn size(&self) -> u64 { self.bytes }}
struct Folder { children: Vec<Box<dyn Node>>,}
impl Folder { fn new() -> Self { Folder { children: Vec::new() } } fn add(mut self, child: Box<dyn Node>) -> Self { self.children.push(child); self }}
impl Node for Folder { fn size(&self) -> u64 { self.children.iter().map(|c| c.size()).sum() }}
fn main() { let inner = Folder::new() .add(Box::new(File { bytes: 20 })) .add(Box::new(File { bytes: 30 })); let root = Folder::new() .add(Box::new(File { bytes: 100 })) .add(Box::new(inner)); println!("{}", root.size()); // 150}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: client ปฏิบัติต่อ object เดี่ยวและ subtree ทั้งกิ่งได้แบบเดียวกัน ขจัดเงื่อนไขการตรวจสอบ type
- ข้อดี: การเพิ่มชนิด component ใหม่ปลายเปิด คือเพียง implement interface ร่วม
- ข้อดี: การดำเนินการแบบวนซ้ำ เช่น การหายอดรวม การ render หรือการค้นหา เกิดขึ้นอย่างเป็นธรรมชาติจากโครงสร้าง
- ข้อเสีย: component interface ที่กว้างเกินไปจะบังคับให้ใบต้อง implement การดำเนินการเกี่ยวกับลูก ทั้งที่ใบไม่มีลูก
- ข้อเสีย: ความเป็นเอกภาพอาจซ่อนต้นทุนไว้ คือการเรียกเพียงครั้งเดียวที่ดูไร้พิษภัยอาจวนซ้ำไปทั่วต้นไม้ขนาดมหึมา
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Decorator ก็อาศัย composition แบบ recursive เหมือนกัน แต่เพิ่มพฤติกรรมให้ object ที่ห่อไว้ตัวเดียว ไม่ได้รวมผลจากลูกหลายตัว
- Builder ใช้ประกอบต้นไม้ composite ที่ซับซ้อนทีละขั้นได้ดี
| Composite | Decorator | Iterator | |
|---|---|---|---|
| จุดประสงค์ | จัดโครงสร้างแบบ tree เพื่อ treat leaf = branch | เพิ่ม behavior โดยไม่แก้ class | วนซ้ำ collection ด้วย interface เดียว |
| โครงสร้าง | tree (ลูกหลายตัว) | chain (ห่อชั้นเดียว) | external cursor |
| client รู้ความต่าง | ไม่รู้ (interface เดียวกัน) | ไม่รู้ | ไม่รู้ |
| เมื่อใช้ | file system, DOM, org chart | middleware, logging, caching | list, tree, stream |
💡 หมายเหตุสำหรับ developer
Pattern นี้พบได้บ่อยใน:
- DOM tree — element ทุกตัวไม่ว่าจะเป็น leaf หรือ container ถูกเข้าถึงผ่าน interface เดียวกัน (
Node)- React component tree — component ประกอบกันเป็น tree โดย parent ไม่ต้องรู้ว่า child เป็น leaf หรือ composite
- File system — folder และ file ถูกเข้าถึงผ่าน operation ร่วมกัน (
ls,size,path)