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

Move Statements into Function

เมื่อ code บรรทัดหนึ่งจับคู่กับการเรียก function ตัวหนึ่งเสมอ คือรันก่อนหรือหลังทันทีทุกจุดที่เรียก บรรทัดนั้นควรอยู่ ข้างใน function ย้ายเข้าไปเลย แล้ว function จะเป็นเจ้าของงานทั้งชุดอย่างครบถ้วน ส่วน caller ก็หดเหลือการเรียกครั้งเดียวที่ทำพลาดไม่ได้

คุณเจอ statement เตรียมการหรือปิดท้ายชุดเดิม copy-paste อยู่เหนือหรือใต้การเรียก function ทุกจุด เช่น caller ของ renderName ทุกตัวเขียน opening tag ก่อน หรือ caller ของ fetchOrder ทุกตัว log บรรทัดเดียวกันต่อท้าย statement ที่ซ้ำนั้นเป็นส่วนหนึ่งของ behavior ที่ caller คาดหวังอยู่แล้ว แต่ดันไปวางไว้นอก function คนที่มาเขียน caller ใหม่จึงต้องจำเองว่าต้องใส่ ลืมครั้งเดียวก็ได้บั๊กเงียบ ๆ ความซ้ำแบบนี้คือสัญญาณว่า statement นั้นอยากย้ายเข้าไปข้างใน

function ตัวนี้ปล่อย HTML รูปภาพของบุคคล ส่วน caller ทุกตัวเขียนบรรทัด heading <p> ชุดเดิมก่อนเรียกทุกครั้ง จริง ๆ แล้ว heading นั้นเป็นส่วนหนึ่งของงาน “render บล็อกรูปภาพ” เราจึงย้ายเข้าไปข้างใน

// Before
function photoData(photo: Photo): string {
return [
`<p>location: ${photo.location}</p>`,
`<p>date: ${photo.date.toDateString()}</p>`,
].join('\n');
}
function renderPerson(person: Person, photo: Photo): string {
return [
`<p>${person.name}</p>`,
`<p>title: ${photo.title}</p>`, // repeated at every call site
photoData(photo),
].join('\n');
}
function emitPhoto(photo: Photo): string {
return [
`<p>title: ${photo.title}</p>`, // repeated here too
photoData(photo),
].join('\n');
}
// After
function photoData(photo: Photo): string {
return [
`<p>title: ${photo.title}</p>`, // moved in — now owned by photoData
`<p>location: ${photo.location}</p>`,
`<p>date: ${photo.date.toDateString()}</p>`,
].join('\n');
}
function renderPerson(person: Person, photo: Photo): string {
return [`<p>${person.name}</p>`, photoData(photo)].join('\n');
}
function emitPhoto(photo: Photo): string {
return photoData(photo);
}
  1. ยืนยันว่าคำสั่งที่เป็นตัวเลือกนั้นรันติดกับการเรียกที่ ทุก จุด ถ้าแม้เพียง caller รายเดียวทำต่างออกไป การย้ายนี้ผิด — ความแตกต่างนั้นเป็นของจริง
  2. ถ้าจุดเรียกมี code อื่นอยู่ ให้ใช้ Extract Function ก่อนเพื่อแยกเฉพาะการเรียกบวกกับคำสั่งที่ทำซ้ำออกเป็น helper เดียว ตอนนี้คุณมีที่ทำงานเพียงที่เดียว
  3. ย้าย statement ที่ซ้ำเข้าไปใน function เป้าหมาย วางไว้บนสุดถ้าเดิมรันก่อน วางล่างสุดถ้าเดิมรันหลัง
  4. รัน test
  5. ลบคำสั่งที่ตอนนี้ซ้ำซ้อนออกจาก caller แต่ละราย ทีละราย โดยรัน test หลังการลบแต่ละครั้ง
  6. ถ้าสร้าง helper ชั่วคราวไว้ในขั้นที่ 2 ให้ inline ทิ้งเมื่อ caller สะอาดครบทุกตัวแล้ว

ใช้ท่านี้เมื่อ statement หนึ่งเป็นความรับผิดชอบของ function จริง ๆ แต่บังเอิญไปยืนอยู่ข้างนอก พับเข้าไปแล้วความซ้ำจะหายไป และ function ก็กลายเป็นหน่วยที่ครบถ้วนจนใช้ผิดได้ยาก

อย่าใช้ถ้า caller แต่ละตัวไม่เหมือนกัน คือบางตัวรัน statement นั้น บางตัวไม่รัน หรือรันคนละแบบ การยัดขั้นตอนร่วมลงไปทั้งที่ caller บางตัวอยากข้าม เท่ากับแลกความซ้ำมาเป็น flag parameter ซึ่งแย่กว่าเดิม ท่ากลับกันคือ Move Statements to Callers เมื่อ function มัดขั้นตอนที่มีแค่ caller บางตัว ต้องการเอาไว้ ก็ดันออกไปข้างนอก

ใช้ Move Statements into Function เมื่อหลีกเลี่ยงเมื่อ
statement เดิมซ้ำทุกครั้งก่อนหรือหลัง callstatement นั้นบางครั้งต้องการ แต่บางครั้งไม่
statement นั้นเป็นส่วนหนึ่งของ invariant ของ functioncaller ที่ต่างกันต้องการ behavior ที่ต่างกันหลัง call
ทุก caller ทำ statement เดิมหลัง call เสมอการย้ายเข้าไปจะซ่อน side effect จากผู้อ่าน caller

⚠️ ไม่ควร Move Statements into Function เมื่อ:

  • มี caller บางตัวที่ไม่ต้องการ statement นั้น
  • statement นั้นขึ้นกับ state ที่ function ไม่รู้จัก
  • ต้องการ flexibility ให้ caller เลือก behavior เอง
เมื่อใดที่คำสั่งหนึ่งเป็นตัวเลือกที่ดีในการย้ายเข้าไปใน function?
ถ้าจุดเรียกมี code อื่นที่ไม่เกี่ยวข้องอยู่ คุณควรทำอะไรก่อน?
อะไรคือสิ่งตรงข้ามของ Move Statements into Function?
อะไรคือสัญญาณว่าคุณไม่ควรย้ายคำสั่งเข้าไป?