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 บล็อกรูปภาพ” เราจึงย้ายเข้าไปข้างใน
// Beforefunction 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');}
// Afterfunction 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);}# Beforedef photo_data(photo): return "\n".join([ f"<p>location: {photo.location}</p>", f"<p>date: {photo.date:%Y-%m-%d}</p>", ])
def render_person(person, photo): return "\n".join([ f"<p>{person.name}</p>", f"<p>title: {photo.title}</p>", # repeated at every call site photo_data(photo), ])
def emit_photo(photo): return "\n".join([ f"<p>title: {photo.title}</p>", # repeated here too photo_data(photo), ])
# Afterdef photo_data(photo): return "\n".join([ f"<p>title: {photo.title}</p>", # moved in — now owned by photo_data f"<p>location: {photo.location}</p>", f"<p>date: {photo.date:%Y-%m-%d}</p>", ])
def render_person(person, photo): return "\n".join([f"<p>{person.name}</p>", photo_data(photo)])
def emit_photo(photo): return photo_data(photo)// Beforefunc photoData(p Photo) string { return fmt.Sprintf("<p>location: %s</p>\n<p>date: %s</p>", p.Location, p.Date.Format("2006-01-02"))}
func renderPerson(person Person, p Photo) string { return fmt.Sprintf("<p>%s</p>\n<p>title: %s</p>\n%s", person.Name, p.Title, photoData(p)) // title repeated everywhere}
func emitPhoto(p Photo) string { return fmt.Sprintf("<p>title: %s</p>\n%s", p.Title, photoData(p))}
// Afterfunc photoData(p Photo) string { return fmt.Sprintf("<p>title: %s</p>\n<p>location: %s</p>\n<p>date: %s</p>", p.Title, p.Location, p.Date.Format("2006-01-02")) // moved in}
func renderPerson(person Person, p Photo) string { return fmt.Sprintf("<p>%s</p>\n%s", person.Name, photoData(p))}
func emitPhoto(p Photo) string { return photoData(p)}// Beforefn photo_data(p: &Photo) -> String { format!("<p>location: {}</p>\n<p>date: {}</p>", p.location, p.date)}
fn render_person(person: &Person, p: &Photo) -> String { // title line repeated at every call site format!("<p>{}</p>\n<p>title: {}</p>\n{}", person.name, p.title, photo_data(p))}
fn emit_photo(p: &Photo) -> String { format!("<p>title: {}</p>\n{}", p.title, photo_data(p))}
// Afterfn photo_data(p: &Photo) -> String { // moved in — now owned by photo_data format!("<p>title: {}</p>\n<p>location: {}</p>\n<p>date: {}</p>", p.title, p.location, p.date)}
fn render_person(person: &Person, p: &Photo) -> String { format!("<p>{}</p>\n{}", person.name, photo_data(p))}
fn emit_photo(p: &Photo) -> String { photo_data(p)}กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ยืนยันว่าคำสั่งที่เป็นตัวเลือกนั้นรันติดกับการเรียกที่ ทุก จุด ถ้าแม้เพียง caller รายเดียวทำต่างออกไป การย้ายนี้ผิด — ความแตกต่างนั้นเป็นของจริง
- ถ้าจุดเรียกมี code อื่นอยู่ ให้ใช้ Extract Function ก่อนเพื่อแยกเฉพาะการเรียกบวกกับคำสั่งที่ทำซ้ำออกเป็น helper เดียว ตอนนี้คุณมีที่ทำงานเพียงที่เดียว
- ย้าย statement ที่ซ้ำเข้าไปใน function เป้าหมาย วางไว้บนสุดถ้าเดิมรันก่อน วางล่างสุดถ้าเดิมรันหลัง
- รัน test
- ลบคำสั่งที่ตอนนี้ซ้ำซ้อนออกจาก caller แต่ละราย ทีละราย โดยรัน test หลังการลบแต่ละครั้ง
- ถ้าสร้าง helper ชั่วคราวไว้ในขั้นที่ 2 ให้ inline ทิ้งเมื่อ caller สะอาดครบทุกตัวแล้ว
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้ท่านี้เมื่อ statement หนึ่งเป็นความรับผิดชอบของ function จริง ๆ แต่บังเอิญไปยืนอยู่ข้างนอก พับเข้าไปแล้วความซ้ำจะหายไป และ function ก็กลายเป็นหน่วยที่ครบถ้วนจนใช้ผิดได้ยาก
อย่าใช้ถ้า caller แต่ละตัวไม่เหมือนกัน คือบางตัวรัน statement นั้น บางตัวไม่รัน หรือรันคนละแบบ การยัดขั้นตอนร่วมลงไปทั้งที่ caller บางตัวอยากข้าม เท่ากับแลกความซ้ำมาเป็น flag parameter ซึ่งแย่กว่าเดิม ท่ากลับกันคือ Move Statements to Callers เมื่อ function มัดขั้นตอนที่มีแค่ caller บางตัว ต้องการเอาไว้ ก็ดันออกไปข้างนอก
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Move Statements into Function เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| statement เดิมซ้ำทุกครั้งก่อนหรือหลัง call | statement นั้นบางครั้งต้องการ แต่บางครั้งไม่ |
| statement นั้นเป็นส่วนหนึ่งของ invariant ของ function | caller ที่ต่างกันต้องการ behavior ที่ต่างกันหลัง call |
| ทุก caller ทำ statement เดิมหลัง call เสมอ | การย้ายเข้าไปจะซ่อน side effect จากผู้อ่าน caller |
⚠️ ไม่ควร Move Statements into Function เมื่อ:
- มี caller บางตัวที่ไม่ต้องการ statement นั้น
- statement นั้นขึ้นกับ state ที่ function ไม่รู้จัก
- ต้องการ flexibility ให้ caller เลือก behavior เอง