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

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 ตรงที่ข้อมูลอยู่แล้ว

// Before
class 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;
}
}
// After
class 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);
}
}
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 ที่อิจฉา plan ย้ายไปอยู่บน plan
  1. ไล่ดูทุกอย่างที่ function ใช้ในบ้านปัจจุบัน แล้วยืนยันว่าส่วนใหญ่เป็นของ class ปลายทาง จดสิ่งไม่กี่อย่างที่ยังมาจากต้นทางไว้
  2. ตรวจสอบว่าองค์ประกอบฝั่งต้นทางเหล่านั้นควรเดินทางไปกับ function หรือถูกส่งเข้ามาเป็น parameter
  3. คัดลอก function เข้าไปใน class ปลายทาง และปรับบอดี้ให้ใช้ field ของปลายทางเองโดยตรง
  4. คอมไพล์และแก้ไขการอ้างอิงต่าง ๆ — ปลายทางอาจต้องมี parameter ใหม่สำหรับข้อมูลต้นทางที่ยังเหลืออยู่
  5. เปลี่ยน function เดิมให้เป็น delegator บาง ๆ ที่เรียกบ้านใหม่ หรือแก้ caller ให้เรียกตรงไปเลย
  6. รัน test behavior ต้องไม่เปลี่ยนแปลง
  7. เมื่อ 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 ที่ชัดเจน
Move Function แก้ code smell ใดได้ตรงที่สุด?
อะไรคือสัญญาณที่ชัดเจนที่สุดว่า function ควรอยู่บน class อื่น?
หลังจากย้าย function แล้ว มักเกิดอะไรขึ้นกับ method เดิม?
อะไรคือสิ่งตรงข้ามของ Move Function?