ข้ามไปยังเนื้อหา

Combine Functions into Class

เมื่อมี function อิสระหลายตัวทำงานบนข้อมูลชุดเดียวกัน แต่ละตัวรับข้อมูลชุดนั้นเข้ามาเป็น argument แล้วคำนวณค่าออกมา ให้รวมทั้งกลุ่มเข้าเป็น class ข้อมูลที่ใช้ร่วมกันจะกลายเป็น field ของ object ส่วน function กลายเป็น method และ data clump ก็เลิกถูกร้อยส่งผ่านทุกการเรียก

คุณเห็น function กลุ่มหนึ่งรับ record ตัวเดียวกันแล้วดึงค่าออกมาใช้ เช่น baseCharge(reading), taxableCharge(reading), calculateBaseCharge(reading) ตัว record เดินทางจาก function หนึ่งไปอีก function ในรูป parameter และ function พวกนี้ก็เกาะกันเป็นครอบครัวชัดเจน แต่ไม่มีอะไรตั้งชื่อให้ครอบครัวนั้นเลย นี่คือ Data Clump ที่มี behavior โคจรอยู่รอบ ๆ พอห่อข้อมูลไว้ใน class ครอบครัวนี้ก็ได้บ้าน method เรียกกันเองได้โดยไม่ต้องส่งข้อมูลซ้ำ และ function ที่เกี่ยวข้องตัวถัดไปก็รู้ว่าจะไปลงตรงไหน

function สามตัวต่างก็รับ reading ของมิเตอร์แล้วคำนวณค่าใช้จ่าย เรารวมทั้งสามเข้าเป็น class Reading ที่เก็บข้อมูลของ reading ไว้เป็น field

// Before — data threaded through every function
function baseRate(month: number): number {
return month >= 6 && month <= 9 ? 0.12 : 0.1;
}
function baseCharge(reading: Reading): number {
return baseRate(reading.month) * reading.quantity;
}
function taxableCharge(reading: Reading): number {
return Math.max(0, baseCharge(reading) - 30);
}
// After — data and behaviour live together
class ReadingCharge {
constructor(private reading: Reading) {}
private get baseRate(): number {
return this.reading.month >= 6 && this.reading.month <= 9 ? 0.12 : 0.1;
}
get baseCharge(): number {
return this.baseRate * this.reading.quantity;
}
get taxableCharge(): number {
return Math.max(0, this.baseCharge - 30);
}
}
flowchart LR
  subgraph Before["Before"]
    D["reading data<br/>(customer, quantity, month)"]
    F1["baseCharge(reading)"]
    F2["taxableCharge(reading)"]
    F3["calculateBaseCharge(reading)"]
    D -.-> F1
    D -.-> F2
    D -.-> F3
  end
  subgraph After["After"]
    C["Reading (class)<br/>fields: customer, quantity, month"]
    C --> M1["baseCharge()"]
    C --> M2["taxableCharge()"]
    C --> M3["calculateBaseCharge()"]
  end
  Before -.->|"Combine Functions into Class"| After
data clump พร้อม function ที่โคจรรอบ ๆ กลายเป็น class เดียว
  1. เลือก record ร่วมที่ทุก function ใช้ ถ้ายังเป็นโครงสร้างข้อมูลเปล่า ๆ ให้ทำ Encapsulate Record ก่อน เพราะต้องมี object สักตัวมายึด class ไว้
  2. สร้าง class (หรือ struct + impl) ที่รับ record นั้นเข้า constructor แล้วเก็บไว้เป็น field
  3. ย้าย function เข้า class ทีละตัว เปลี่ยน parameter ข้อมูลให้ไปอ้าง field ที่เก็บไว้แทน แล้วรัน test หลังย้ายทุกครั้ง
  4. แทนที่การเข้าถึง parameter เดิม (เช่น reading.month) ด้วยการเข้าถึง field บน object
  5. เมื่อ method เริ่มเรียกกันเอง ให้ทิ้ง argument ที่ตอนนี้ซ้ำซ้อนที่เคยส่งกันออกไป
  6. รัน test หลังทุกขั้น แล้วอัปเดต call site เดิมแต่ละจุดให้สร้าง object และเรียก method

ใช้ท่านี้เมื่อ function ชุดหนึ่งเป็นของข้อมูลชิ้นเดียวชัด ๆ และคุณต้องคอยส่งข้อมูลนั้นวิ่งไปมาระหว่างกัน class จะตั้งชื่อให้กลุ่มก้อนนี้ ตัดการร้อย parameter ซ้ำซากออก และเตรียมบ้านไว้ให้ logic ที่เกี่ยวข้องซึ่งจะเพิ่มมาทีหลัง ท่านี้ยังเข้าคู่ได้ดีกับ Split Phase และ Extract Function เพราะพอ function ใช้ object ร่วมกัน ค่าที่คำนวณได้ก็กลายเป็น query สะอาด ๆ บน object นั้น

ข้อแลกเปลี่ยนคือคุณกำลังนำ object เข้ามาในที่ที่เคยมีแต่ function ธรรมดา ซึ่งคุ้มก็ต่อเมื่อ function เหล่านั้นเกาะกลุ่มกันรอบข้อมูลร่วมจริง ๆ ถ้า function เพียงบังเอิญรับ argument คล้ายกันแต่มุ่งเป้าที่ไม่เกี่ยวกัน class ก็จะจับสิ่งที่ไม่ได้เป็นพวกเดียวกันมารวมกัน ทางเลือกของการรวบรวม เมื่อ function ผลิตค่าออกมาแทนที่จะใช้ state ที่เปลี่ยนได้ร่วมกัน คือ Combine Functions into Transform

ใช้ Combine Functions into Class เมื่อหลีกเลี่ยงเมื่อ
function หลายตัวใช้ data record เดียวกันเสมอfunction เหล่านั้นไม่มี shared state จริง ๆ
ต้องการ method ที่ computed จาก shared datadata นั้นเป็น primitive ที่ไม่ควร wrap
ต้องการส่ง “bundle of functions + data” ไปรอบ ๆเพื่อ convenience เท่านั้น ไม่ใช่ cohesion

⚠️ ไม่ควร Combine Functions into Class เมื่อ:

  • function เหล่านั้นทำงานกับ data ที่ต่างกัน — cohesion ต่ำ
  • สร้าง class เพื่อ OOP แต่ภาษาที่ใช้ถนัด functional มากกว่า
  • ยังไม่แน่ใจว่า data เหล่านี้จะอยู่ด้วยกันตลอด
อะไรคือตัวกระตุ้นให้ทำ Combine Functions into Class?
เกิดอะไรขึ้นกับข้อมูลที่ใช้ร่วมกันเมื่อ function กลายเป็น class?
ใน Go และ Rust การ refactor นี้แสดงออกมาอย่างไร?
เมื่อไรที่คุณไม่ควรรวม function เข้าเป็น class?