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 functionfunction 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 togetherclass 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); }}# Before — data threaded through every functiondef base_rate(month): return 0.12 if 6 <= month <= 9 else 0.10
def base_charge(reading): return base_rate(reading.month) * reading.quantity
def taxable_charge(reading): return max(0, base_charge(reading) - 30)
# After — data and behaviour live togetherclass ReadingCharge: def __init__(self, reading): self.reading = reading
@property def base_rate(self): return 0.12 if 6 <= self.reading.month <= 9 else 0.10
@property def base_charge(self): return self.base_rate * self.reading.quantity
@property def taxable_charge(self): return max(0, self.base_charge - 30)// Before — data threaded through every functionfunc baseRate(month int) float64 { if month >= 6 && month <= 9 { return 0.12 } return 0.10}
func baseCharge(r Reading) float64 { return baseRate(r.Month) * r.Quantity}
func taxableCharge(r Reading) float64 { return math.Max(0, baseCharge(r)-30)}
// After — struct holds the data, methods hang off ittype ReadingCharge struct { reading Reading}
func (rc ReadingCharge) baseRate() float64 { if rc.reading.Month >= 6 && rc.reading.Month <= 9 { return 0.12 } return 0.10}
func (rc ReadingCharge) BaseCharge() float64 { return rc.baseRate() * rc.reading.Quantity}
func (rc ReadingCharge) TaxableCharge() float64 { return math.Max(0, rc.BaseCharge()-30)}// Before — data threaded through every functionfn base_rate(month: u32) -> f64 { if (6..=9).contains(&month) { 0.12 } else { 0.10 }}
fn base_charge(reading: &Reading) -> f64 { base_rate(reading.month) * reading.quantity}
fn taxable_charge(reading: &Reading) -> f64 { (base_charge(reading) - 30.0).max(0.0)}
// After — struct owns the data, impl holds the methodsstruct ReadingCharge { reading: Reading,}
impl ReadingCharge { fn base_rate(&self) -> f64 { if (6..=9).contains(&self.reading.month) { 0.12 } else { 0.10 } }
fn base_charge(&self) -> f64 { self.base_rate() * self.reading.quantity }
fn taxable_charge(&self) -> f64 { (self.base_charge() - 30.0).max(0.0) }}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 กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เลือก record ร่วมที่ทุก function ใช้ ถ้ายังเป็นโครงสร้างข้อมูลเปล่า ๆ ให้ทำ Encapsulate Record ก่อน เพราะต้องมี object สักตัวมายึด class ไว้
- สร้าง class (หรือ struct + impl) ที่รับ record นั้นเข้า constructor แล้วเก็บไว้เป็น field
- ย้าย function เข้า class ทีละตัว เปลี่ยน parameter ข้อมูลให้ไปอ้าง field ที่เก็บไว้แทน แล้วรัน test หลังย้ายทุกครั้ง
- แทนที่การเข้าถึง parameter เดิม (เช่น
reading.month) ด้วยการเข้าถึง field บน object - เมื่อ method เริ่มเรียกกันเอง ให้ทิ้ง argument ที่ตอนนี้ซ้ำซ้อนที่เคยส่งกันออกไป
- รัน 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 data | data นั้นเป็น primitive ที่ไม่ควร wrap |
| ต้องการส่ง “bundle of functions + data” ไปรอบ ๆ | เพื่อ convenience เท่านั้น ไม่ใช่ cohesion |
⚠️ ไม่ควร Combine Functions into Class เมื่อ:
- function เหล่านั้นทำงานกับ data ที่ต่างกัน — cohesion ต่ำ
- สร้าง class เพื่อ OOP แต่ภาษาที่ใช้ถนัด functional มากกว่า
- ยังไม่แน่ใจว่า data เหล่านี้จะอยู่ด้วยกันตลอด