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

Combine Functions into Class

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

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

function สามตัวต่างก็ทำงานจาก reading ของมิเตอร์ตัวเดียวกัน (โดยตรง หรือผ่าน month ของมัน) แล้วคำนวณค่าใช้จ่าย เรารวมทั้งสามเข้าเป็น 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["baseRate(month)"]
    F2["baseCharge(reading)"]
    F3["taxableCharge(reading)"]
    D -.-> F1
    D -.-> F2
    D -.-> F3
  end
  subgraph After["After"]
    C["Reading (class)<br/>fields: customer, quantity, month"]
    C --> M1["baseRate"]
    C --> M2["baseCharge"]
    C --> M3["taxableCharge"]
  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 กลายเป็น class?
ใน Go และ Rust การ refactor นี้แสดงออกมาอย่างไร?
เมื่อไรที่คุณไม่ควรรวม function เข้าเป็น class?