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

Move Field

field อยู่บน record หนึ่ง แต่คนที่อ่านและอัปเดตส่วนใหญ่กลับเป็นอีก record ให้ย้าย field ไปอยู่กับ record ที่ใช้จริง ข้อมูลจะได้อยู่ติดกับ behavior ที่ต้องใช้ และ record ทั้งสองจะเลิกเอื้อมข้ามขอบเขตที่ขีดไว้ผิดที่ตั้งแต่แรก

โครงสร้างข้อมูลคือโครงกระดูกของโปรแกรม field ที่วางผิดที่จึงบิดทุกอย่างที่สร้างทับลงไป สัญญาณที่ควรจับตามีสามแบบ คือ field บน record A ที่ต้องส่งคู่ไปกับ record B เสมอ field ที่ method ของ B เป็นคนอ่านและเขียน ส่วน A แค่เก็บไว้เฉย ๆ และ record สองตัวที่ต้องอัปเดตพร้อมกันเพื่อให้ค่าตรงกัน ทุกแบบชี้ไปทางเดียวกันว่าเจ้าของตัวจริงของ field คือ B

discountRate เก็บอยู่บน Customer แต่คนกำหนดค่าทั้งหมดคือ PricingPlan ของลูกค้า ตอนแรกอัตราอยู่บนลูกค้า แล้ว plan ต้องเอื้อมข้ามมาหยิบไปคำนวณราคา หลัง refactor อัตราย้ายไปอยู่บน plan ตรงที่กฎการคิดราคาอยู่แล้ว

// Before
class PricingPlan {
constructor(public name: string) {}
finalPrice(base: number, discountRate: number): number {
return base * (1 - discountRate);
}
}
class Customer {
constructor(
public name: string,
public plan: PricingPlan,
public discountRate: number,
) {}
quote(base: number): number {
return this.plan.finalPrice(base, this.discountRate);
}
}
// After
class PricingPlan {
constructor(public name: string, public discountRate: number) {}
finalPrice(base: number): number {
return base * (1 - this.discountRate);
}
}
class Customer {
constructor(public name: string, public plan: PricingPlan) {}
quote(base: number): number {
return this.plan.finalPrice(base);
}
}
flowchart LR
  subgraph Before["Before"]
    A["Customer<br/>name<br/>discountRate"]
    B["PricingPlan"]
    A -.->|"plan logic reads<br/>discountRate"| B
  end
  subgraph After["After"]
    C["Customer<br/>name<br/>plan"]
    D["PricingPlan<br/>discountRate"]
    C --> D
  end
  Before -.->|"Move Field"| After
field ที่เก็บบน Customer ย้ายไปอยู่กับ PricingPlan ที่ใช้จริง
  1. ถ้า field เป็น public ให้ห่อไว้หลัง accessor ก่อน ทุกการอ่านและเขียนจะได้ผ่านจุดเดียว ตอนย้ายจริงจึงแก้ที่เดียวจบ
  2. เพิ่ม field ลงใน record ปลายทาง พร้อมกับ accessor ที่นั่น
  3. ตัดสินใจว่าปลายทางจะเอาค่ามาอย่างไร จะคำนวณเอง เก็บไว้ตอนสร้าง หรือรับเข้ามาเป็น parameter ก็ได้
  4. เปลี่ยนทิศทาง accessor แต่ละตัวบนต้นทางให้มอบหมายไปยังสำเนาของ field ที่ปลายทาง
  5. รัน test behavior ต้องไม่เปลี่ยนแปลง ณ จุดตรวจสอบนี้
  6. ลบ field ออกจากต้นทางเมื่อไม่มีใครอ่านในที่เดิมแล้ว พร้อมตัด parameter ที่มีไว้ขนค่าออกไปด้วย
  7. รัน test อีกครั้ง ชุด test สีเขียวยืนยันว่าค่าอยู่ในที่เดียวแล้ว

ย้าย field เมื่อ method ของ record อื่นใช้มากกว่าเจ้าของปัจจุบัน เมื่อ field ต้องเดินทางคู่ไปกับ object อื่นตลอด หรือเมื่อการคอยไล่ให้สำเนาสองชุดตรงกันกลายเป็นแหล่งบั๊กประจำ

ต้นทุนคือการต้องแตะ caller ทุกตัวที่อ่าน field โดยตรง — ซึ่งนั่นเองคือเหตุผลที่การห่อหุ้มก่อนคุ้มค่า ไม่มีสิ่งตรงข้ามที่มีชื่อแยกต่างหาก: หากการเปลี่ยนแปลงภายหลังทำให้ record เดิม กลายเป็นผู้ใช้งานหนักกว่า คุณก็เพียงแค่ Move Field กลับ

ใช้ Move Field เมื่อหลีกเลี่ยงเมื่อ
field ถูกใช้โดย class อื่นบ่อยกว่า class ที่เป็นเจ้าของการย้ายจะสร้าง circular dependency
field เป็นส่วนหนึ่งของ Extract Class ที่กำลังทำfield นั้น computed จาก data ใน class ปัจจุบัน
เปลี่ยน field นี้ต้องแก้ class อื่นเสมอทีมไม่มี test ครอบคลุม access patterns ของ field

⚠️ ไม่ควร Move Field เมื่อ:

  • field นั้นถูก access จากทั้งสอง class ในปริมาณพอ ๆ กัน
  • การย้ายทำให้ API เปลี่ยน ซึ่ง break external caller
  • ยังไม่แน่ใจว่า class ใหม่จะมีชีวิตยืนยาว
เมื่อใดที่คุณควรย้าย field ไปยังอีก record?
ทำไมจึงควรห่อ field ไว้หลัง accessor ก่อนย้าย?
อะไรคือสัญญาณที่หนักแน่นว่า field อยู่ผิดที่?
คุณควรทำอะไรที่จุดตรวจสอบแต่ละจุดในกลไก (mechanics)?