Preserve Whole Object
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”caller ถือ object อยู่ในมืออยู่แล้ว แต่กลับขุด field สองสามตัวออกมาเพื่อส่งเข้า function ทีละตัว ให้ส่ง ทั้ง object ไปเลยดีกว่า รายการ argument จะสั้นลง function เอื้อมไปหยิบ field ไหนก็ได้ที่ต้องใช้ และถ้าวันหลัง object งอก field ใหม่ที่เกี่ยวข้อง signature ก็ไม่ต้องเปลี่ยน
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”ที่จุดเรียกใช้คุณจะเห็นชุดของการเรียก accessor ที่ป้อนตรงเข้าไปในอีกการเรียกหนึ่ง: plot(point.x, point.y), fits(range.low, range.high) ค่าที่ถูกแกะออกมาทั้งหมดมาจาก object เดียวกันและเดินทางไปด้วยกัน รายการ parameter ของ function สะท้อนเนื้อในของ object ทั้งสองจึงถูกผูกติดกันอย่างเงียบ ๆ ทั้งที่ดูเหมือนเป็นอิสระต่อกัน
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”ค่าที่อ่านได้ถูกตรวจเทียบกับ range ของอุณหภูมิ โดย caller ดึง low กับ high ออกจาก range มาส่งเข้าไป แทนที่จะทำแบบนั้น ให้ส่งตัว range ไปเลย แล้วปล่อยให้ฝั่งตรวจถาม range เอาเอง
// Beforefunction fitsRange(reading: number, low: number, high: number): boolean { return reading >= low && reading <= high;}
const ok = fitsRange(today, range.low, range.high);
// Afterinterface Range { low: number; high: number; }
function fitsRange(reading: number, range: Range): boolean { return reading >= range.low && reading <= range.high;}
const ok = fitsRange(today, range);# Beforedef fits_range(reading, low, high): return low <= reading <= high
ok = fits_range(today, range_.low, range_.high)
# Afterdef fits_range(reading, range_): return range_.low <= reading <= range_.high
ok = fits_range(today, range_)// Beforefunc FitsRange(reading, low, high float64) bool { return reading >= low && reading <= high}
ok := FitsRange(today, r.Low, r.High)
// Aftertype Range struct { Low float64 High float64}
func FitsRange(reading float64, r Range) bool { return reading >= r.Low && reading <= r.High}
ok := FitsRange(today, r)// Beforefn fits_range(reading: f64, low: f64, high: f64) -> bool { reading >= low && reading <= high}
let ok = fits_range(today, range.low, range.high);
// Afterstruct Range { low: f64, high: f64,}
fn fits_range(reading: f64, range: &Range) -> bool { reading >= range.low && reading <= range.high}
let ok = fits_range(today, &range);flowchart LR
subgraph Before["Before"]
A["caller pulls low + high<br/>from range"] --> B["fits(low, high)"]
end
subgraph After["After"]
C["caller passes range"] --> D["fits(range)"]
end
Before -.->|"Preserve Whole Object"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เพิ่ม parameter ใหม่สำหรับทั้ง object โดยปล่อย parameterfield ที่มีอยู่ไว้ก่อน
- ภายใน function แทนที่การใช้ field ที่ถูกแกะแต่ละจุดด้วยการอ่านจากทั้ง object
- อัปเดต caller แต่ละจุดให้ส่ง object และเลิกส่ง field แต่ละตัว รัน test หลังจากแต่ละจุด
- เมื่อแปลง caller ทุกจุดแล้ว ให้ลบ parameterfield ที่ตอนนี้ไม่ถูกใช้แล้วทิ้ง
- มองหา logic ใน caller ที่ยุ่งอยู่กับ field เหล่านั้นอย่างเดียว เพราะ logic นั้นอาจควรย้ายเข้าไปใน function หรือกลายเป็น method บนตัว object เลย
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบท่านี้มาใช้เมื่อ field ที่แกะออกมาทั้งหมดเป็นของ object เดียวกัน เมื่อหลาย function ดึง field กลุ่มเดียวกันออกมาใช้ หรือเมื่อ function น่าจะได้ประโยชน์จากการเข้าถึง object มากกว่าที่ได้อยู่ตอนนี้ ท่านี้มักเผย Feature Envy ออกมาด้วย คือ code ที่เอาแต่ยุ่งกับข้อมูลของ object อื่น ซึ่งคุณย้ายไปเป็น method บน object นั้นได้เลย
ข้อแลกเปลี่ยนคือ dependency แน่นขึ้น เพราะตอนนี้ function ต้องรู้จัก type ของ object ไม่ใช่แค่รับตัวเลขสองตัว ถ้าปลายทางไม่ควรรู้จัก type นั้น ไม่ว่าจะเพราะอยากเอาไปใช้ซ้ำในบริบทที่ไม่เกี่ยวกัน หรือเพราะต้องเลี่ยง circular dependency ก็ให้คง parameter แบบหลวม ๆ ไว้ และถ้า caller ยังไม่มี object อยู่ในมือ การประกอบขึ้นมาใหม่เพียงเพื่อจะเรียกก็ถือว่าถอยหลัง
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Preserve Whole Object เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| หลาย field ที่แกะมาล้วนมาจาก object เดียวกัน | ต้องการแยก function จาก type นั้นเพื่อ reuse |
| function น่าจะต้องการ field เพิ่มในภายหลัง | มี circular dependency ถ้าส่ง whole object |
| รายการ parameter ยาวเพราะ accessor chain | caller ยังไม่มี whole object ต้องสร้างขึ้นมาใหม่ |
⚠️ ไม่ควร Preserve Whole Object เมื่อ:
- function ที่ถูกเรียกอยู่คนละ layer และไม่ควรรู้จัก type นั้น
- ส่ง whole object สร้าง tight coupling ที่ทำให้ test ยากขึ้น
- fields ที่ส่งมาไม่ได้อยู่ใน object เดียวกันจริง ๆ