Move Function
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”นำ function ที่อ่านแล้วเอาแต่พูดถึง object อื่น — field ของ object นั้น method ของ object นั้น กฎของ object นั้น — ย้ายไปไว้กับ object ดังกล่าวเสียเลย บ้านใหม่เป็นเจ้าของข้อมูลที่ function ต้องใช้ ตัว function จึงเรียบง่ายขึ้น และ coupling ระหว่างสอง class ก็ลดลง
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”นี่คือยารักษา Feature Envy คือ method ที่สนใจ class อื่นมากกว่า class ที่ตัวเองอาศัยอยู่ สัญญาณสังเกตอยู่ที่รายการ parameter กับ body ถ้า function รับ argument plan แล้วอ่าน plan.tier, plan.dailyRate และ plan.discount ส่วน object ของตัวเองแทบไม่แตะเลย แปลว่า function ตัวนี้อิจฉา plan ย้ายไปอยู่ที่นั่นซะ แล้วการเอื้อมข้ามไปเรียกจะกลายเป็นการอ่าน field ธรรมดา
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”ค่าธรรมเนียมเบิกเกินบัญชี (overdraft charge) อาศัยอยู่บน Account แต่คำนวณเกือบทั้งหมดจาก plan ของบัญชี ตอนแรก function ต้องเอื้อมเข้าไปใน plan เพื่อดึงทุกอย่างออกมา หลัง refactor ก็ย้ายไปอยู่บน plan ตรงที่ข้อมูลอยู่แล้ว
// Beforeclass AccountPlan { constructor(public tier: string, public dailyRate: number) {}}
class Account { constructor(private plan: AccountPlan, private daysOverdrawn: number) {}
overdraftCharge(): number { if (this.plan.tier === "premium") { let base = 10; if (this.daysOverdrawn > 7) { base += (this.daysOverdrawn - 7) * this.plan.dailyRate * 0.85; } return base; } return this.daysOverdrawn * this.plan.dailyRate; }}
// Afterclass AccountPlan { constructor(public tier: string, public dailyRate: number) {}
overdraftCharge(daysOverdrawn: number): number { if (this.tier === "premium") { let base = 10; if (daysOverdrawn > 7) { base += (daysOverdrawn - 7) * this.dailyRate * 0.85; } return base; } return daysOverdrawn * this.dailyRate; }}
class Account { constructor(private plan: AccountPlan, private daysOverdrawn: number) {}
overdraftCharge(): number { return this.plan.overdraftCharge(this.daysOverdrawn); }}# Beforeclass AccountPlan: def __init__(self, tier, daily_rate): self.tier = tier self.daily_rate = daily_rate
class Account: def __init__(self, plan, days_overdrawn): self.plan = plan self.days_overdrawn = days_overdrawn
def overdraft_charge(self): if self.plan.tier == "premium": base = 10 if self.days_overdrawn > 7: base += (self.days_overdrawn - 7) * self.plan.daily_rate * 0.85 return base return self.days_overdrawn * self.plan.daily_rate
# Afterclass AccountPlan: def __init__(self, tier, daily_rate): self.tier = tier self.daily_rate = daily_rate
def overdraft_charge(self, days_overdrawn): if self.tier == "premium": base = 10 if days_overdrawn > 7: base += (days_overdrawn - 7) * self.daily_rate * 0.85 return base return days_overdrawn * self.daily_rate
class Account: def __init__(self, plan, days_overdrawn): self.plan = plan self.days_overdrawn = days_overdrawn
def overdraft_charge(self): return self.plan.overdraft_charge(self.days_overdrawn)// Beforetype AccountPlan struct { Tier string DailyRate float64}
type Account struct { Plan AccountPlan DaysOverdrawn int}
func (a Account) OverdraftCharge() float64 { if a.Plan.Tier == "premium" { base := 10.0 if a.DaysOverdrawn > 7 { base += float64(a.DaysOverdrawn-7) * a.Plan.DailyRate * 0.85 } return base } return float64(a.DaysOverdrawn) * a.Plan.DailyRate}
// Aftertype AccountPlan struct { Tier string DailyRate float64}
func (p AccountPlan) OverdraftCharge(daysOverdrawn int) float64 { if p.Tier == "premium" { base := 10.0 if daysOverdrawn > 7 { base += float64(daysOverdrawn-7) * p.DailyRate * 0.85 } return base } return float64(daysOverdrawn) * p.DailyRate}
type Account struct { Plan AccountPlan DaysOverdrawn int}
func (a Account) OverdraftCharge() float64 { return a.Plan.OverdraftCharge(a.DaysOverdrawn)}// Beforestruct AccountPlan { tier: String, daily_rate: f64,}
struct Account { plan: AccountPlan, days_overdrawn: i64,}
impl Account { fn overdraft_charge(&self) -> f64 { if self.plan.tier == "premium" { let mut base = 10.0; if self.days_overdrawn > 7 { base += (self.days_overdrawn - 7) as f64 * self.plan.daily_rate * 0.85; } base } else { self.days_overdrawn as f64 * self.plan.daily_rate } }}
// Afterstruct AccountPlan { tier: String, daily_rate: f64,}
impl AccountPlan { fn overdraft_charge(&self, days_overdrawn: i64) -> f64 { if self.tier == "premium" { let mut base = 10.0; if days_overdrawn > 7 { base += (days_overdrawn - 7) as f64 * self.daily_rate * 0.85; } base } else { days_overdrawn as f64 * self.daily_rate } }}
struct Account { plan: AccountPlan, days_overdrawn: i64,}
impl Account { fn overdraft_charge(&self) -> f64 { self.plan.overdraft_charge(self.days_overdrawn) }}flowchart LR
subgraph Before["Before"]
A["Account"]
A --> B["overdraftCharge()<br/>reads plan.tier<br/>reads plan.dailyRate"]
C["AccountPlan"]
end
subgraph After["After"]
D["Account"]
E["AccountPlan"]
E --> F["overdraftCharge()<br/>reads own tier<br/>reads own dailyRate"]
D -.->|"delegates"| F
end
Before -.->|"Move Function"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ไล่ดูทุกอย่างที่ function ใช้ในบ้านปัจจุบัน แล้วยืนยันว่าส่วนใหญ่เป็นของ class ปลายทาง จดสิ่งไม่กี่อย่างที่ยังมาจากต้นทางไว้
- ตรวจสอบว่าองค์ประกอบฝั่งต้นทางเหล่านั้นควรเดินทางไปกับ function หรือถูกส่งเข้ามาเป็น parameter
- คัดลอก function เข้าไปใน class ปลายทาง และปรับบอดี้ให้ใช้ field ของปลายทางเองโดยตรง
- คอมไพล์และแก้ไขการอ้างอิงต่าง ๆ — ปลายทางอาจต้องมี parameter ใหม่สำหรับข้อมูลต้นทางที่ยังเหลืออยู่
- เปลี่ยน function เดิมให้เป็น delegator บาง ๆ ที่เรียกบ้านใหม่ หรือแก้ caller ให้เรียกตรงไปเลย
- รัน test behavior ต้องไม่เปลี่ยนแปลง
- เมื่อ caller ทุกตัวเรียกผ่านตำแหน่งใหม่หมดแล้ว ให้ลบ delegator ทิ้งถ้าไม่คุ้มจะเก็บไว้
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ย้าย function เมื่อเห็นว่าเอื้อมข้ามไปหา object อื่นอยู่ประจำ เมื่อหลาย function บน class หนึ่งจะเรียบง่ายขึ้นถ้าย้ายไปรวมกันบนอีก class หรือเมื่อย้ายแล้วลบ parameter ที่มีไว้ขนข้อมูลอย่างเดียวออกได้
ต้นทุนคือ caller อาจต้องถือ reference ไปยัง object ปลายทาง และถ้าย้ายเพลินเกินไป logic ที่เกี่ยวข้องกันก็จะกระจัดกระจาย ทางกลับกันคือย้ายกลับ ถ้าวันหลังการเปลี่ยนแปลงทำให้ function พึ่งบริบท เดิม มากกว่า ก็ Move Function กลับบ้านเก่าได้เลย
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Move Function เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| function ใช้ data ของ class อื่นมากกว่า class ตัวเอง | function ถูก override ใน subclass |
| function ต้องถูก call จาก context ที่ต่างกันหลายจุด | การย้ายจะสร้าง circular reference |
| Feature Envy ชัดเจน — function “สนใจ” class อื่น | function นั้นเป็น core logic ของ class ปัจจุบัน |
⚠️ ไม่ควร Move Function เมื่อ:
- function เข้าถึง private data ของ class ปัจจุบัน — ย้ายแล้วจะต้องเปิด access
- ไม่มีที่ที่เหมาะกว่าให้ย้ายไป — อาจต้องสร้าง class ใหม่แทน
- การย้ายทำให้ test เดิมเสีย โดยไม่มีเหตุผลด้าน design ที่ชัดเจน