7 คะแนน โดย GN⁺ 5 시간 전 | 5 ความคิดเห็น | แชร์ทาง WhatsApp
  • โค้ดที่ซ้ำกันมีต้นทุนถูกกว่า นามธรรมที่ผิดพลาด มาก และการ รีบทำให้เป็นส่วนกลางเร็วเกินไปจะเพิ่มต้นทุนการบำรุงรักษาในระยะยาว
  • แม้การแยกออกมาในตอนแรกจะดูสมเหตุสมผล แต่เมื่อความต้องการค่อย ๆ เปลี่ยนไป ก็จะมี พารามิเตอร์และเงื่อนไข if เพิ่มเข้ามาจนทำให้เจตนาเดิมพร่าเลือน
  • เมื่อนามธรรมร่วมเริ่มต้องแบกรับหลายแนวคิด โค้ดจะกลายเป็นกระบวนการที่ขับเคลื่อนด้วยเงื่อนไข และยิ่งเพิ่มฟีเจอร์ใหม่ก็ยิ่งแตกหักง่าย
  • ควรระวัง กับดักต้นทุนจม ที่พยายามปกป้องความพยายามที่ลงไปกับโค้ดเดิม และหากจำเป็นควรอินไลน์นามธรรมกลับไปยังจุดเรียกใช้ เพื่อให้เหลือเฉพาะโค้ดที่ต้องใช้จริง
  • หากเห็นชัดว่านามธรรมนั้นผิดพลาด การนำความซ้ำกลับมาใหม่เพื่อสังเกตจุดร่วมของความต้องการปัจจุบัน แล้วค่อยแยกออกอีกครั้ง มักจะเร็วกว่า

กระบวนการที่ทำให้เกิดนามธรรมที่ผิดพลาด

  • ประโยค “duplication is far cheaper than the wrong abstraction” เป็นส่วนหนึ่งของงานพูดที่ RailsConf 2014 แต่หลังจากนั้นก็ยังถูกอ้างถึงอยู่เรื่อยมา
  • เส้นทางความล้มเหลวที่พบบ่อยมีดังนี้
    • นักพัฒนา A พบ ความซ้ำซ้อน
    • แยกความซ้ำออกเป็นเมธอดหรือคลาส ตั้งชื่อให้ และสร้างนามธรรมใหม่
    • แทนที่โค้ดซ้ำในจุดเรียกใช้ด้วยการเรียกนามธรรมใหม่นั้น
    • เมื่อเวลาผ่านไป ก็เกิดความต้องการใหม่ที่เกือบจะเหมือนเดิมแต่ไม่เหมือนกันทั้งหมด
    • นักพัฒนา B พยายามคงนามธรรมเดิมไว้ จึงเพิ่มพารามิเตอร์และใส่เงื่อนไขที่พาไปตามเส้นทางต่างกันตามค่า
    • หลังจากนั้น ทุกครั้งที่มีความต้องการใหม่ พารามิเตอร์และเงื่อนไขก็จะเพิ่มขึ้น ทำให้โค้ดเข้าใจได้ยากขึ้นเรื่อย ๆ
  • โค้ดที่สร้างขึ้นมาแล้วมักดูเหมือนเป็น การลงทุนที่ต้องรักษาไว้
    • จิตวิทยาที่เสียดายความพยายามที่ลงไปแล้วจะเริ่มทำงาน
    • ยิ่งโค้ดซับซ้อนและเข้าใจยาก ก็ยิ่งดูเหมือนเป็นสิ่งสำคัญและใช้เวลานาน จนยิ่งทิ้งได้ยาก
    • สิ่งนี้เชื่อมโยงกับ กับดักต้นทุนจม

กลับไปหาความซ้ำ แล้วค่อยแยกออกใหม่

  • หากยังคงพัฒนาความต้องการใหม่บนฐานของนามธรรมที่ผิดพลาดต่อไป โค้ดที่ใช้ร่วมกันจะกลายเป็นโค้ดที่ขับเคลื่อนด้วยเงื่อนไข และยิ่งเพิ่มฟีเจอร์ก็ยิ่งไม่เสถียร
  • ในเวลานั้น ทางลัดที่เร็วที่สุดไม่ใช่การฝืนเดินหน้าต่อ แต่คือ การถอยกลับ
    • อินไลน์โค้ดที่ถูกนามธรรมไว้กลับเข้าไปในแต่ละจุดเรียกใช้ เพื่อนำความซ้ำกลับมาอีกครั้ง
    • ดูจากพารามิเตอร์ที่แต่ละจุดเรียกใช้ส่งเข้าไป เพื่อดูว่าโค้ดใดถูกทำงานจริง
    • ลบโค้ดที่ไม่จำเป็นต่อจุดเรียกใช้นั้นออก
  • กระบวนการอินไลน์นี้จะลบนามธรรมและเงื่อนไขออกพร้อมกัน และย่อแต่ละจุดเรียกใช้ให้เหลือเพียงโค้ดที่ตัวเองต้องการ
  • โค้ดที่ดูเหมือนเรียกใช้นามธรรมเดียวกัน แท้จริงแล้วแต่ละจุดเรียกใช้อาจกำลังวิ่งผ่าน เส้นทางโค้ดที่เฉพาะตัว อย่างมาก
  • ต้องลบนามธรรมเดิมออกให้หมดก่อน จึงจะกลับมาสังเกตความซ้ำ และแยกนามธรรมใหม่ที่เหมาะกับความต้องการปัจจุบันได้
  • หากยังมีการเพิ่มพารามิเตอร์และเส้นทางเงื่อนไขเข้าไปในโค้ดที่ใช้ร่วมกันอย่างต่อเนื่อง ก็มีโอกาสสูงว่านามธรรมนั้นจะไม่เหมาะอีกต่อไป
    • ในตอนแรกมันอาจเป็นนามธรรมที่เหมาะสมก็ได้
    • แต่เมื่อความต้องการเปลี่ยนไป ก็อาจรักษารูปแบบเดิมไว้ได้ยากอีกต่อไป
  • สำหรับนามธรรมที่ผิดพลาด การนำความซ้ำกลับมาใหม่ไม่ใช่การถอยหลัง แต่คือ การเดินหน้าไปสู่สิ่งที่ดีกว่า

5 ความคิดเห็น

 
dieafterwork 1 시간 전

ผม/ฉันไม่แน่ใจว่านี่เป็นประเด็นที่จำเป็นต้องตีความแบบแบ่งเป็นสองขั้วหรือไม่

 
hanje3765 2 시간 전

โอ้ เห็นด้วยมากครับ
สิ่งที่ยังไม่เป็นระเบียบก็จัดระเบียบทีหลังได้
แต่สิ่งที่จัดเป็นระเบียบไว้แล้ว ดูเหมือนว่าจะมีต้นทุนในการรื้อกลับมากกว่ามากครับ

 
jimmy2056 3 시간 전

ponytail เพิ่งโพสต์ขึ้นไป แล้วก็มีบทความแบบนี้ออกมาทันทีเลย 555

 
shakespeares 4 시간 전

เป็นประเด็นที่ถกเถียงกันอยู่เสมอครับ

 
GN⁺ 5 시간 전
ความคิดเห็นจาก Hacker News
  • มองว่าควรยึดหลัก single source of truth ไว้เสมอ
    ถ้าเป็นโค้ดซ้ำที่พอไม่ตรงกันเมื่อไรจะกลายเป็นบั๊ก ก็ควรรีแฟกเตอร์ ไม่อย่างนั้นจะเกิด การพ่วงกันระยะไกล ที่ทำให้นักพัฒนาในอนาคตรู้ตัวยากมากจนกว่าบั๊กจะระเบิด
    แต่ถ้าไม่ได้ละเมิดหลักนั้น abstraction ก็เป็นแค่ความสะดวกเท่านั้น และถ้ามันเริ่มใช้งานไม่สะดวก ก็แปลว่ามันไม่ทำหน้าที่ของมันแล้ว จึงไม่มีเหตุผลจะใช้ต่อ ถ้าฟังก์ชันต้องพึ่งหลายแฟลกเพื่อให้ทำงานแบบปรับแต่งได้ ก็มักเป็น abstraction ที่ผิดพลาดหรือไม่ก็ละเมิดหลัก single responsibility
    ถ้าจำเป็นต้องปรับแต่งเยอะจริง ๆ การรับฟังก์ชัน/ฟังก์เตอร์เป็นอาร์กิวเมนต์มักดีกว่า เช่นทำเป็น solve(f:double -> double, stopping_criteria: StoppingCriteriaClass) แทน solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)

    • ประเด็นสำคัญของบทความคือกรณีที่ยังไม่ชัดว่ามี แหล่งความจริง อยู่กี่แห่ง
      ยังไม่แน่ชัดว่าโค้ดสองจุดใช้ algorithm เดียวกัน หรือเป็นคนละเวอร์ชันที่ต่างกันเล็กน้อย และที่สำคัญกว่านั้นคือมันจะเปลี่ยนด้วยเหตุผลเดียวกันหรือไม่
      คำคมในหัวข้อบอกว่า การฝืนทำให้สิ่งที่ต่างกันเป็นสิ่งเดียวกันนั้นเจ็บปวดกว่าการทำของที่เหมือนกันซ้ำไว้ก่อนแล้วค่อยแยกให้ต่างกันทีหลัง ซึ่งผมก็เห็นด้วย อย่างหลังแค่แก้การเปลี่ยนเดียวกันสองครั้ง หรือรีแฟกเตอร์เพื่อใส่ abstraction ก็พอ แต่แบบแรกต้องคอยแปะเพิ่มบน abstraction เดิมหรือไม่ก็ย้อนกลับมัน
      โดยเฉพาะเพราะมันทำลาย locality ซึ่งเวลาแก้ไขแล้วนี่คือคุณสมบัติที่สำคัญจริง ๆ แค่อยากเปลี่ยนตรงนี้แล้วไม่ต้องกังวลว่าจะมีผลข้างเคียงกับส่วนที่ไม่เกี่ยวของระบบ
    • ถ้าซอฟต์แวร์ถูกแรงกดดันอย่างหนักจนถูกบีบให้มี สองแหล่งความจริง วิธีที่ค่อนข้างใช้ได้คือเพิ่มการทดสอบใน CI ที่จะไม่ยอมให้ merge เข้า main ถ้าสองแหล่งนั้นไม่ตรงกัน
      ตัวอย่างชัด ๆ คือการซิงก์ pyproject.toml / requirements.txt ซึ่งบางครั้งก็เป็นทางเลือกที่ดีที่สุดจริง ๆ และน่าจะประยุกต์ใช้ได้กว้างกว่านั้น เงื่อนไขคืองานมันพังมาถึงจุดที่มี single source of truth ไม่ได้แล้ว จึงเป็นการลดความเสียหายมากกว่าการรักษา
    • เกณฑ์ว่า “ถ้าต่างกันเมื่อไรจะเป็นบั๊ก” เป็นกฎคร่าว ๆ ที่ดีมาก
      เคยเจอบ่อยว่าตอนแรกโค้ดสองก้อนดูคล้ายกันเลย abstract มากเกินไป แล้วสุดท้ายภายหลังก็แยกทางกัน
    • ในทางทฤษฎีมันใช่ แต่ในโลกจริงมีคนจำนวนมากที่พยายามเลี่ยง duplication ทุกชนิดแบบสุดโต่ง
      โดยเฉพาะนักพัฒนารุ่นจูเนียร์ที่บางครั้งมองว่า ความซ้ำซ้อน คือรากเหง้าของความชั่วร้ายทั้งปวง
  • บางทีก็คิดเรื่องนี้เหมือนกัน เพิ่งเจอในโปรเจกต์ส่วนตัวตอนจัดการ สไปรต์ 2D สำหรับยูนิตในเกม RTS โดยสไปรต์ยูนิตถูกใส่ไว้ในสไปรต์ชีตอย่างสม่ำเสมอ: 8 ทิศทาง ทิศทางละ 5 สไปรต์ โดยมี 3 ทิศทางที่ใช้การ mirror และเรียงเป็น stand, move, attack, die
    เลยสร้าง loader ที่รับ action + direction แล้วคืนอาร์เรย์สไปรต์ที่จะเล่นกลับมา
    แต่พอมีสไปรต์ระเบิดที่ไม่มีทิศทาง สไปรต์ศพที่มีแค่ 4 ทิศทางและ mirror เพียง 2 แบบ แถมหลังจากสี่ตัวแรกแล้วออร์คกับมนุษย์ยังใช้ร่วมกันเป็นส่วนใหญ่ ก็เริ่มเจอปัญหา
    เคยนั่งคิดอยู่พักหนึ่งว่าจริง ๆ abstraction ร่วมของทั้งหมดนี้คืออะไรกันแน่ แต่สุดท้ายก็แยกออกมาแค่บางส่วนของโค้ดโหลด แล้วทำ UnitLoader, CorpseLoader และ EffectLoader จากนั้นก็ไปต่อ ทั้งสาม loader จัดการสิ่งที่คล้ายกันเล็กน้อยจึงอาจมี abstraction ที่ดีกว่าอยู่ก็ได้ แต่ถ้าจะพบก็ค่อยพบทีหลัง ตอนนี้การลบ duplication ทีหลังยังง่ายกว่าการสร้าง EverythingLoader ที่ซับซ้อนเพื่อพยายามรองรับทุกกรณีตั้งแต่แรก

    • ชอบคำพูดที่ว่า “สิ่งต่าง ๆ ควรเรียบง่ายให้มากที่สุด แต่ไม่ควรเรียบง่ายไปกว่านั้น”
      ในการเขียนโปรแกรม เรามีสัญชาตญาณจะทำให้โค้ดง่ายขึ้นด้วยการทำให้เป็น generalization แต่ความจริงมันสกปรกกว่านั้น จึงมักกลายเป็นการทำให้ง่ายเกินไป อย่างในบทความนี้ พอเวลาผ่านไปและมีความต้องการใหม่ ก็จะเห็นว่ามันคือ การทำให้ง่ายเกินไปเร็วเกินควร
      จะมีคำคมว่า “abstraction ที่รีบเกินไปคือรากเหง้าของความห่วยจำนวนมาก” ก็คงไม่ผิด
    • เป็นไปได้มากว่า abstraction ร่วมถูกแยกออกไว้แล้ว นั่นคือโค้ดที่โหลดและแสดงพิกเซลของสไปรต์เดี่ยว
      แต่ในชั้นถัดขึ้นไปซึ่งเป็นการตีความการจัดวางในสไปรต์ชีตและโหมดการเล่นนั้นมีรูปแบบย่อยหลายแบบ และอาจไม่มี abstraction ร่วมที่ใช้ได้กับทุกกรณี
      ผมชอบวิธีทำแบบตอนนี้มากกว่าการฝืนสร้าง abstraction ที่มองไม่เห็น หรือพยายามยัดทุกอย่างให้เข้ากับ abstraction ที่ยังไม่สมบูรณ์ การรอจนกว่า abstraction จะชัดเจนสมบูรณ์และความจำเป็นเด่นชัดจึงเป็นเรื่องที่ดี
      ฝั่งตรงข้ามของ DRY ยังมี WET ซึ่งหมายถึงให้เขียนทุกอย่างซ้ำสอง/สามครั้ง ที่สำคัญกว่านั้นคือผมคิดว่าควร abstract เฉพาะ use case ที่พิสูจน์แล้วจริง ๆ ซึ่งมักจะโผล่มาให้เห็นก่อนในรูปแบบของ duplication โค้ดที่เขียนเผื่อ use case ในอนาคตที่ยังไม่มี มักไปขัดขวางการ abstract สิ่งที่เรามีอยู่จริง และทุกครั้งที่เกิดแบบนั้นก็น่าขำดี
    • วิธีนี้แหละถูกแล้ว การทำเกมมันควรจะสนุกตั้งแต่แรก
      งานยากและน่าเบื่อค่อยไปทำตอนถึง 10% สุดท้ายของโปรเจกต์ก็ได้
      แถมบางที “บั๊ก” ที่เกิดจาก duplication ก็กลายเป็นฟีเจอร์สนุก ๆ ที่ผู้เล่นชอบด้วย
  • ตอนที่ยังใช้ OOP เคยทรมานกับ abstraction มาก แต่หลังจากย้ายมาใช้แนวทางแบบ functional ที่เกือบบริสุทธิ์แล้ว การซ้ำของโค้ดก็แทบไม่ค่อยเกิด
    แค่สร้างฟังก์ชันขึ้นมาแล้วเรียกใช้จากสองที่ก็พอ ปัญหา abstraction หลัก ๆ อยู่ที่โครงสร้างข้อมูล ซึ่งอินเทอร์เฟซของ TypeScript ก็เป็น duck typing โดยเนื้อแท้อยู่แล้ว เลยไม่ค่อยมีปัญหามากนัก
    เพราะงั้น duplication ของโค้ดที่เกิดจากปัญหา abstraction จึงพบไม่บ่อย สิ่งที่เจอบ่อยกว่ามากคือ duplication ของโค้ดที่เกิดจาก นักพัฒนาทำงานเป็นไซโล

    • ผมใช้ภาษาเชิงฟังก์ชันเป็นงานอดิเรก และคิดว่าประเด็นสำคัญที่ต้องจำคือเรื่องเทคนิค
      ภาษาสมัยใหม่ส่วนใหญ่สามารถวางอยู่บนทฤษฎีของ functional programming ได้ไม่ยาก และไม่จำเป็นต้องรู้ Haskell ก็ได้ แน่นอนว่าสมองแต่ละคนทำงานต่างกัน แต่สำหรับผมแนวคิดที่ว่าชิ้นส่วนเล็ก ๆ เรียบง่าย และบางครั้งก็ยืดหยุ่นมาประกอบกันเป็นภาพรวม มันเข้าท่ามาก
      ซึ่งตรงข้ามกับเครื่องแปลงรูปร่างขนาดใหญ่ ซับซ้อน และทำได้ทุกอย่าง
    • การเจอ duplication ของโค้ดไม่ได้แปลว่านักพัฒนาต้องทำงานแบบไซโลเสมอไป
      เมื่อทีมใหญ่เกินระดับหนึ่งจนไม่มีใครรู้ได้ทั้งหมดว่าคนอื่นกำลังทำอะไรอยู่ โค้ดซ้ำ ก็ค่อนข้างหลีกเลี่ยงไม่ได้ ต่อให้ทุกคนเขียนในสไตล์ functional ก็เหมือนกัน
      เดือนก่อนที่บริษัทก็เพิ่งเจอแบบนี้ ผมเขียน pure helper function ใหม่ไว้ตรงต้นไฟล์ แล้วอีกสัปดาห์ต่อมาเพื่อนร่วมงานก็มาบอกว่าจริง ๆ มี helper function คล้ายกันมากที่ทำหน้าที่แทบเหมือนกันแต่มี signature ต่างออกไป อยู่ท้ายไฟล์เดียวกันนี่เอง
    • สงสัยว่าคำว่า “เรียกฟังก์ชันจากสองส่วน” นี่หมายถึงอะไรกันแน่
  • ในบริบทเดียวกับบทความหลัก ใครที่เคยเจอมาทั้งสองแบบก็น่าจะเห็นด้วยว่า โค้ดเบสที่ออกแบบไม่มากเกินไป จัดการได้ง่ายกว่าโค้ดเบสที่ออกแบบเกินจำเป็นมาก

  • โค้ดที่แย่ที่สุดที่เคยต้องดูแลคือโค้ดที่พยายามทำตาม DRY แต่ไม่ได้พยายามเข้าใจเจตนาเดิมของหลักการนั้นจริง ๆ
    วิธีเดียวที่จะหนีออกจากความเละเทะนั้นได้คือการนำ การทำโค้ดซ้ำ ในวงกว้างกลับเข้ามาอีกครั้ง

    • ไม่เป็นไร ไม่ต้องกังวล แค่เพิ่ม พารามิเตอร์บูลีน กำกวมเข้าไปอีกไม่กี่ตัวในฟังก์ชันที่ใช้ซ้ำ แล้วปล่อยขึ้นระบบเพื่อรองรับ use case ใหม่ก็พอ
    • ประเด็นสำคัญคือ “ได้พยายามแล้ว” ทำแบบนั้นไปสักพักก็จะไปถึงจุดที่ไม่สามารถยึดตามมันอย่างซื่อตรงได้อีก เพราะ abstraction นั้นผิดตั้งแต่แรก
  • ตรงนี้ทำให้นึกถึงสองงานบรรยาย: Mike Acton กับ Data-Oriented Design and C++ [1] และ Brian Cantrill กับ The Complexity of Simplicity [2]
    งานของ Mike พูดว่าโซลูชันในโค้ดไม่จำเป็นต้องจำลองโลกความเป็นจริง ข้อมูลที่ต่างกันก็ก่อปัญหาคนละแบบ และจึงต้องการโซลูชันคนละแบบเช่นกัน คงถ่ายทอดงานบรรยายนี้ออกมาได้ไม่ดีพอ แต่สำหรับฉันมันมีอิทธิพลมาก
    งานของ Brian ว่าด้วย abstraction โดยรวม และความยากของการหา abstraction ที่ “ถูกต้อง”

    1. https://www.youtube.com/watch?v=rX0ItVEVjHc
    2. https://www.youtube.com/watch?v=Cum5uN2634o
    • บางครั้งแม้แต่วิศวกรที่ฉลาดมากก็ยังให้ความสำคัญกับ อุปมาอิงโลกความจริง มากกว่าความต้องการจริงของโค้ดเบส ซึ่งรู้สึกแปลกเสมอ
      ตอนที่ฉันเพิ่งเรียนจบมาได้ไม่กี่ปี เคยกำลังทำ connection pool ใน Rust และแนวทางที่สมเหตุสมผลที่สุดคือให้ออบเจ็กต์ connection ถือ weak reference ไปยัง pool เพื่อจะได้คืนกลับอัตโนมัติเมื่อ drop
      ผู้จัดการของฉันซึ่งเป็นผู้จัดการที่มีประสบการณ์มาก ไม่ชอบไอเดียนี้เพราะ “ห้องสมุดถือหนังสือ ไม่ใช่หนังสือถือห้องสมุด” แม้ฉันจะไม่รู้สึกว่านี่เป็นเหตุผลที่หนักแน่นพอจะเปลี่ยนแบบได้ แต่เขาก็ไม่ยอมจัดการปัญหานี้นอกกรอบของอุปมานั้น
      สุดท้ายทางตันก็คลี่คลายเมื่อผู้จัดการอีกคนเสนอว่า “หนังสือห้องสมุดไม่ได้มีตัวห้องสมุดอยู่ในตัว แต่มีตราประทับชื่อห้องสมุดที่ชี้ไปยังจุดคืนหนังสืออยู่ด้านหลัง” ดูเหมือนผู้จัดการคนนั้นจะเห็นว่าการขยายอุปมานี้สมเหตุสมผล
      ถ้าตอนนั้นฉันมีประสบการณ์มากกว่านี้ บางทีอาจหาวิธีคุยกันภายในอุปมานั้นได้โดยไม่ต้องยอมประเด็น แต่จนถึงตอนนี้ก็ยังรู้สึกว่าการยึดอุปมานั้นเป็นกรอบมาตรฐาน แทนที่จะพิจารณาผลลัพธ์ของ abstraction ในโค้ดและประสบการณ์การใช้ไลบรารี เป็นเรื่องประหลาดอย่างยิ่ง
  • ไม่มีใครฟังหรอก ไม่มีใครฟังจริง ๆ บริษัท 90% มีสิ่งที่เรียกกันว่านักพัฒนา senior ที่เคลิบเคลิ้มทุกครั้งที่ได้สร้าง abstraction ใหม่
    การออกแบบเกินจำเป็น, abstraction และการ optimize เร็วเกินไป คือ 3 มหันตภัยของงานวิศวกรรม
    แต่ในอีกด้านหนึ่งก็ดีใจที่สิ่งเหล่านี้ยังมีอยู่ เพราะมันทำให้มีงานทำเสมอ

    • Kubernetes, microservices ที่มีมากกว่าจำนวนวิศวกร, โปรโตคอลซับซ้อนเพื่อประหยัด overhead แค่ไม่กี่ไบต์, ทุกอย่างต้องขึ้นคลาวด์, และคลาสมากมายที่จริง ๆ แล้วอาจเป็นแค่ฟังก์ชันธรรมดาได้ ล้วนเป็นตัวอย่างแบบนั้น
  • คล้ายกันคือ นักพัฒนาบางคนดูเหมือนจะคิดว่าสตริงแบบ inline หรือค่าคงที่ตัวเลขทั้งหมดเป็นสิ่งชั่วร้าย เคยเห็นแบบนี้ใน PR หนึ่ง
    HTTPS_SCHEME = 'https'
    DOMAIN = 'www.example.com'
    url = HTTPS_SCHEME + '://' + DOMAIN
    ฉันไม่เห็นว่ามันให้อะไรเลยนอกจากทำตามคำว่า “อย่า hardcode ค่าคงที่” แบบ cargo cult แถมการประกาศค่าคงที่ยังอยู่บนสุดของไฟล์ ส่วนโค้ดที่ประกอบ URL อยู่ห่างออกไปอีกหลายร้อยบรรทัด

    • สำหรับโค้ด ฉันชอบ ความใกล้กัน มาก ชอบให้ประกาศสิ่งต่าง ๆ ไว้ใกล้จุดที่ใช้งานที่สุด นี่เป็นนิสัยที่น่ารำคาญจริง ๆ
      regex ก็ไม่จำเป็นต้องไว้บนสุดของไฟล์ เอาไว้ตรงที่ใช้ก็ได้ ภาษาโปรแกรมฉลาดพอ น่าจะรู้ได้เองว่าเป็นค่าคงที่
      ถ้าเป็นฟังก์ชันเล็กมาก ก็ใช้ lambda ไปเลย อยากไม่ให้มีการสร้างฟังก์ชันหนึ่งบรรทัดที่ใช้แค่ครั้งสองครั้งไว้ไกลลิบ
    • การเอาค่าคงที่ไว้ด้านบนทำให้ปรับแต่งได้ง่ายกว่า โดยเฉพาะถ้าไฟล์นี้ถูกคัดลอกไปใช้อีกที่
      ถ้าใน test หรือ staging ต้องเปลี่ยนจาก https เป็น http การแยก scheme กับ domain ออกมาและวางค่าคงที่ไว้ด้านบนหรือในอีกไฟล์หนึ่งก็สมเหตุสมผล url ถูกประกอบหลายที่หรือที่เดียวก็สำคัญเหมือนกัน
      การมี named constants อยู่บนสุดของไฟล์เป็นสไตล์ที่พบได้บ่อยมาก และบางครั้งก็เป็นส่วนหนึ่งของมาตรฐานการเขียนโค้ดของทีม
      อาจมีเหตุผลอื่นอีกด้วย ดังนั้นนึกถึง Chesterton’s Fence ไว้ก็ดี อย่างน้อย การฟันธงว่าเป็น cargo cult ไม่ใช่ความคิดที่ดี ใครบางคนก็อาจบอกได้เหมือนกันว่าการใช้ inline literals ก็เป็น cargo cult แบบเดียวกัน ถ้ามันดูแปลกก็ถามได้ อาจมีเหตุผลดี ๆ หรืออาจเป็นแค่ไม่มีใครสนใจ และถ้าคุณ refactor ให้ inline ค่าคงที่กลับเข้าไป ทุกคนก็อาจยินดีด้วยซ้ำ
    • ฉันเองก็เคยเจอแบบนี้ ถ้า Event มีชื่ออยู่ตรง ๆ ก็สามารถ grep หาได้ทันทีทั่วทั้งโมโนลิธขนาดใหญ่หรือชุด repository ของ microservices เพื่อหาทุกไฟล์ที่เกี่ยวกับอีเวนต์นั้น
      แต่ถ้าแยกมันออกไปเป็นค่าคงที่ ก็ต้องกลับไปเปิดแต่ละโปรเจกต์แล้ว ค้นหาจุดใช้งาน ใหม่
  • ถ้าใช้ microservices คุณก็ทำได้ทั้งสองอย่าง

    • รู้ว่าเป็นมุกนะ แต่ในโลกอุดมคติของ microservices จะไม่มีแนวคิดเรื่อง โค้ดซ้ำ ระหว่างบริการ
      ถ้าคุณเป็นผู้ดูแลบริการหนึ่ง ก็ไม่มีเหตุผลต้องสนใจโค้ดในอีกบริการหนึ่ง จะไปสนใจทำไม ในเมื่อเป็นโค้ดของอีกทีม? คุณไม่จำเป็นต้องรู้ด้วยซ้ำว่าทีมนั้นมีอยู่ และในระบบใหญ่ บางครั้งก็เป็นไปไม่ได้ในทางปฏิบัติที่จะรู้ว่ามีแอปพลิเคชันทั้งหมดอะไรบ้าง
    • เดี๋ยวก่อน! ยังไม่หมด!
      เพียง $19.95 เราจะเปลี่ยน single point of failure หนึ่งจุดของคุณให้กลายเป็น single points of failure หลายจุด!
    • 9 ใน 10 ครั้ง microservices จะพึ่งพากันอย่างหนักจนกลายเป็น distributed monolith
      ใช้ service-oriented architecture แต่ deploy เป็นโมโนลิธไปเลยยังดีกว่า ทดสอบง่ายกว่า และยังเลี่ยงชั้นส่วนเกินของการ serialize/deserialize ได้ด้วย
  • คนระดับซีเนียร์ส่วนใหญ่น่าจะรู้ว่าไม่ควรทำตาม DRY แบบมืดบอด ถึงอย่างนั้น พวกเราหลายคนก็ยังรู้สึกไม่สบายใจกับแนวคิดที่ว่าต้องดูแล แหล่งที่มาของโค้ดที่ซ้ำกัน หลายแห่ง
    หากจะรับมือกับเรื่องนี้ ต้องพิจารณาโมเดลง่าย ๆ ที่ผู้เรียกใช้สองรายพึ่งพาโค้ดส่วนกลางอย่างละเอียด ถ้าโค้ดส่วนกลางต้องเปลี่ยนเพราะความต้องการของผู้เรียกใช้เพียงรายเดียว แสดงว่าโค้ดนั้นไม่ได้เป็นส่วนกลางจริง ๆ
    เป้าหมายที่ผิดของ DRY คือพยายามแก้เรื่องนี้ด้วยการห่อหุ้ม (encapsulation) การห่อหุ้มเป็นการย้ายงาน refactoring จากฝั่งผู้เรียกใช้ไปไว้ที่โค้ดส่วนกลาง แต่ผลกระทบจากการอัปเดตโค้ดส่วนกลางนั้นใหญ่กว่าฝั่งผู้เรียกใช้มาก จึงไม่ใช่ทิศทางที่ต้องการ
    เราสามารถรักษา DRY ได้โดยไม่ต้องพึ่งการห่อหุ้ม ทางที่ดีกว่าคือมี abstraction แบบบางหลายชั้นที่ผู้เรียกใช้ต้องรับรู้ ใน OOP เราเรียนรู้สิ่งนี้ผ่าน SRP และ IoC และใน procedural programming มันมักปรากฏขึ้นอย่างเป็นธรรมชาติในรูปแบบของการเรียกชุด helper functions