ย้าย If ขึ้นด้านบน ย้าย For ลงด้านล่าง
(matklad.github.io)- หากย้ายการแตกแขนงเงื่อนไขภายในฟังก์ชันขึ้นไปยังฝั่งผู้เรียก และย้ายงานที่ทำซ้ำกับชุดข้อมูลลงมาเป็น การดำเนินการแบบแบตช์ แทนการเรียกทีละรายการ จะช่วยให้ทั้งโฟลว์ของโค้ดและประสิทธิภาพเรียบง่ายขึ้นได้
- เมื่อย้าย
ifขึ้นไป จะรับประกันเงื่อนไขตั้งต้นด้วย type หรือ assert ได้ง่ายขึ้น และลด การตรวจสอบซ้ำ กับการแตกแขนงที่ไม่จำเป็นภายในฟังก์ชัน - หากตรรกะการแตกแขนงกระจายอยู่ในหลายฟังก์ชัน จะหาเงื่อนไขที่ตายแล้วหรือการตัดสินใจซ้ำได้ยาก และโครงสร้างที่สร้าง enum แล้ว match อีกครั้งก็อาจกลายเป็นรูปแบบที่ ทำซ้ำการแตกแขนงในรูปของโครงสร้างข้อมูล เช่นกัน
- เมื่อย้าย
forลงมา จะประมวลผลหลายอ็อบเจกต์พร้อมกันเพื่อกระจายต้นทุนเริ่มต้น และเปิดช่องให้ปรับแต่งได้ เช่น การจัดลำดับการประมวลผลใหม่หรือ vectorization - เมื่อใช้กฎทั้งสองร่วมกัน จะลดการประเมินเงื่อนไขซ้ำภายในลูปและการแตกแขนงใน hot loop ทำให้โครงสร้างใกล้เคียงกับการย้ายต้นทุนการตัดสินใจของ control plane ไปเป็นการประมวลผลแบบแบตช์ใน data plane
ย้าย if ขึ้นไปยังฝั่งผู้เรียก
- หากมีเงื่อนไข
ifภายในฟังก์ชัน สิ่งแรกที่พิจารณาได้คือย้ายเงื่อนไขนั้นไปยัง ผู้เรียก ได้หรือไม่- ฟังก์ชันที่รับ
Walrusโดยตรงเป็นรูปแบบที่เรียบง่ายกว่าฟังก์ชันที่รับOption<Walrus>แล้วคืนค่าหากเป็นNone - แทนที่ฟังก์ชันจะตรวจสอบเงื่อนไขตั้งต้นแล้ว “ไม่ทำอะไร” ผู้เรียกสามารถตรวจสอบก่อน แล้วรับประกันด้วย type หรือ assert ได้
- ฟังก์ชันที่รับ
- วิธีการย้ายการตรวจสอบเงื่อนไขตั้งต้นขึ้นไปอาจแพร่ไปทั่วเส้นทางการเรียก และท้ายที่สุดจะเป็นแรงจูงใจให้ลด จำนวนครั้งของการตรวจสอบ
- control flow และ
ifมักเพิ่มความซับซ้อนของโค้ดและโอกาสเกิดบั๊กได้ง่าย- โครงสร้างที่อ่านง่ายคือการรวมตรรกะการแตกแขนงที่ซับซ้อนไว้ในฟังก์ชันระดับบนเพียงแห่งเดียว แล้วมอบหมายงานจริงให้ฟังก์ชันระดับล่างที่เป็นเส้นตรง
- เมื่อ control flow ที่ซับซ้อนถูกรวมอยู่ในฟังก์ชันเดียวภายในหน้าจอเดียว จะสังเกตเห็นเงื่อนไขซ้ำและเงื่อนไขที่ตายแล้วได้ง่ายกว่าการกระจายไปทั่วทั้งไฟล์
- การรีแฟกเตอร์แบบ “dissolving enum” มีประโยชน์เมื่อการสร้าง enum และการ match ทำซ้ำการแตกแขนงเดียวกัน
- หาก
f()สร้างE::FooหรือE::Barตามเงื่อนไข แล้วg()ก็ match อีกครั้งเพื่อเรียกfoo()หรือbar()การแตกแขนงจะปรากฏสองครั้ง - เมื่อดึงเงื่อนไขขึ้นไป จะกลายเป็นรูปแบบที่
main()เรียกfoo(x)หรือbar(y)โดยตรงด้วยเงื่อนไขเดียวกัน
- หาก
ย้าย for ลงไปยังการดำเนินการแบบแบตช์
- ในแนวคิดแบบ data-oriented โปรแกรมมักจัดการกับกลุ่มอ็อบเจกต์หลายรายการ และ hot path ก็มักร้อนขึ้นเพราะต้องประมวลผล entity จำนวนมาก
- การนำแนวคิดเรื่อง แบตช์ ของอ็อบเจกต์เข้ามา และให้เวอร์ชันสเกลาร์เป็นกรณีพิเศษของเวอร์ชันแบตช์ เป็นวิธีที่มีประโยชน์
frobnicate_batch(walruses)เป็นรูปแบบที่ดีกว่าfor walrus in walruses { frobnicate(walrus) }
- ประโยชน์หลักของการประมวลผลแบบแบตช์คือ ประสิทธิภาพ
- เมื่อจัดการหลายเป้าหมายพร้อมกัน จะกระจายต้นทุนเริ่มต้นได้
- สามารถเปลี่ยนลำดับการประมวลผลได้อย่างยืดหยุ่น และลดความจำเป็นที่ต้องประมวลผล entity ตามลำดับเฉพาะ
- สามารถใช้เทคนิค vectorization หรือ struct-of-array ที่ประมวลผลฟิลด์หนึ่งของทุก entity ก่อน แล้วจึงไปยังฟิลด์อื่นได้
- ตัวอย่างด้านประสิทธิภาพแบบสุดขั้วเชื่อมโยงไปยัง Vectorized Interpreters Talk
- ตัวอย่างที่น่าสนใจคือ การคูณพหุนามด้วย FFT
- วิธีประเมินพหุนามพร้อมกันที่หลายจุดอาจเร็วกว่าการประเมินทีละจุดหลายครั้ง
- สามารถใช้กฎ
ifและforร่วมกันได้- หากเงื่อนไขอยู่นอกลูป ก็ไม่ต้องประเมิน
conditionซ้ำไปมา - การแตกแขนง ถูกกำจัดออกจาก hot loop
- เปิดโอกาสให้ทำ vectorization
- หากเงื่อนไขอยู่นอกลูป ก็ไม่ต้องประเมิน
- แพตเทิร์นเดียวกันใช้ได้ทั้งในระดับจุลภาคและระดับมหภาค
- โครงสร้างของ TigerBeetle เป็นวิธีที่ประมวลผลแบตช์ของอ็อบเจกต์พร้อมกันใน data plane เพื่อกระจายต้นทุนการตัดสินใจของ control plane
- แรงจูงใจหลักของกฎ
forคือประสิทธิภาพ แต่ก็ช่วยด้านพลังในการสื่อความได้เช่นกันjQueryทำงานกับคอลเลกชันขององค์ประกอบ และเคยประสบความสำเร็จในอดีต- ภาษาของปริภูมิเวกเตอร์เชิงนามธรรมอาจเป็นเครื่องมือช่วยคิดที่ดีกว่าชุดสมการแยกตามพิกัดในบางกรณี
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
โมเดลความคิดแปลก ๆ ของผมคือ มีต้นไม้ของสถานะที่เป็นไปได้และโฟลว์ของโปรแกรมอยู่ แล้วคำสั่งเงื่อนไขก็จะตัดกิ่งต้นไม้นั้น
ควรตัดกิ่งให้เร็วที่สุดเท่าที่ทำได้ เพื่อให้ทำงานกับกิ่งที่น้อยลงเท่านั้น
สุดท้ายแล้วผมอยากให้ฟังก์ชันโฟกัสอย่างใดอย่างหนึ่ง ระหว่างการเดินไปตามต้นไม้ของโปรแกรม หรือการทำงานจริง
นิพจน์จะถูก “เขียนใหม่” ซ้ำ ๆ ตามกฎการลดรูปแล้วจึงประเมินค่า ตัวอย่างเช่น
(1 + 2) + 4จะกลายเป็น3 + 4แล้วกลายเป็น7ตรงนี้มีกฎความสอดคล้องที่กำหนดว่าจะประเมินนิพจน์ย่อยส่วนไหนถัดไป และมีกฎการคำนวณที่เปลี่ยนนิพจน์จริง ๆ เพื่อเปลี่ยนสถานะของโปรแกรม
ภาษาที่เข้มงวด หรือก็คือภาษาแบบไม่เลื่อนการประเมิน ส่วนใหญ่จะประเมินนิพจน์ย่อยทั้งหมดก่อนนิพจน์แม่ แต่โครงสร้างพิเศษอย่างเงื่อนไขและลูปไม่สิ้นสุดเป็นข้อยกเว้น
ในเงื่อนไข กฎการคำนวณจะถูกใช้ก่อนที่กฎความสอดคล้องจะสั่งให้ประเมินนิพจน์ย่อยทั้งหมด จึงเป็นการตัดกิ่งต้นไม้นิพจน์ตามตัวอักษร
[1]: แนะนำหนังสือ Benjamin C. Pierce, Types and Programming Languages
ต้องดูคุณลักษณะของโดเมน แพตเทิร์นของ codebase เดิม โค้ดอยู่ในขั้นตอนไหนของ data pipeline คุณลักษณะด้านประสิทธิภาพ ฯลฯ
เมื่อก่อนผมเคยพยายามสร้างกฎและ heuristic สำหรับการจัดโครงสร้างโค้ดแบบนี้ แต่พอเขียนโค้ดมากพอ ก็ยอมรับว่าระดับ abstraction มันผิด และไม่คุ้มที่จะยึดติดนาน
การที่การถกเถียงแบบนี้ต้องพึ่งชื่อฟังก์ชันสมมติหรือชื่อตัวแปรตัวอักษรเดียวก็ชวนให้คิดเหมือนกัน เพราะใน “เกาะโค้ด” ที่ไม่มีบริบทภายนอก แทบทุกกฎก็ดูสมเหตุสมผลได้
ต้องสมมติแบบสะดวก ๆ ว่า
gเป็นผู้เรียกhเพียงรายเดียว และจะเป็นอย่างนั้นต่อไป จึงจะพูดได้ว่ากฎนี้เผยให้เห็น branch ที่ตายแล้วใน codebase จริง โดยปกติมักมีเหตุผลที่ไม่ได้รวม
gกับhเข้าด้วยกันตั้งแต่แรกกฎที่ทั่วไปกว่าคือวาง
ifไว้ใกล้แหล่งกำเนิดของอินพุต: https://gieseanw.wordpress.com/2024/06/24/dont-push-ifs-up-p...ประเด็นสำคัญคือหาให้เจอว่าจุดนั้นคือจุดไหน รวมถึง entry point ที่เข้ามาจากภายนอกสู่โปรแกรม และข้อมูลที่ดึงมาจากบริการอื่น แล้วปรับแต่งให้สร้างหลักประกันให้มากที่สุดเท่าที่ทำได้ ก่อนจะไปถึง core logic โดยเฉพาะส่วนที่ใช้ทรัพยากรมาก
ถ้าเป็นไปได้ก็ควร encode หลักประกันนั้นไว้ใน type
การคาดเดาหลวม ๆ แบบ “ถ้าในฟังก์ชันมีเงื่อนไข
ifอยู่ ให้พิจารณาว่าย้ายไปฝั่ง caller ได้ไหม” มีตัวอย่างโต้แย้งมากเกินไปถ้าฟังก์ชันถูกเรียกจาก 37 จุด ต้องเขียน
ifซ้ำทุกจุดหรือ?ถ้าฟังก์ชันนั้นคือ
getaddrinfoหรือEnterCriticalSectionต้องผลักifออกไปให้ผู้ใช้ API จัดการหรือ?ผมมองว่าการแปลงแบบนี้คิดได้เฉพาะเมื่อเป็นฟังก์ชันภายในที่ถูกเรียกไม่เกินสองจุด และการตัดสินนั้นอยู่นอกความรับผิดชอบของฟังก์ชัน
อีกวิธีคือให้ฟังก์ชันทำแค่
ifแล้วเรียก helper function สองตัวถ้า caller ต้องยกเงื่อนไขออกไปนอกลูป ก็ใช้ “helper ตีความเงื่อนไข” ระดับต่ำได้ ส่วน caller ที่ไม่ต้องแยกใน/นอกลูป แต่ต้องใช้
ifเพียงครั้งเดียว ก็ใช้ฟังก์ชันอำนวยความสะดวกที่ซ่อนifได้แต่ทั้งหมดนี้เป็นเรื่องเพื่อการ optimize และการ optimize ก็มักขัดกับโครงสร้างโปรแกรมที่ดี
ใน OOP การตัดสินด้วย
ifภายใน callee จะปรากฏเป็นmethod dispatch ที่เลือกว่าจะเรียกเมธอดไหนเทคนิคการย้าย method dispatch ออกไปนอกลูปก็อาจสวนทางกับ flow ของการออกแบบได้
เช่น ถ้าจะเติมออบเจ็กต์ canvas ด้วยภาพ raster คงไม่อยากวนผ่าน pixel ของภาพแล้วเรียก
canvas.putpixel(x, y, color)หรอก ควรมีเมธอดสำหรับ blit ภาพไปยัง canvas หรือบริเวณสี่เหลี่ยมของมันconsiderบทความนี้เล็งไปที่ปัญหาการออกแบบที่ค่อนข้างเฉพาะเจาะจง ซึ่งมักเกิดขึ้นโดยเฉพาะตอนใช้สิ่งอย่าง tagged union
DRY อาจรู้สึกเหมือนเป็นคำตอบที่ถูก แต่ต้องดูตัวอย่างโค้ดจริงถึงจะตัดสินได้
ถ้าเป็นฟังก์ชันไลบรารี ตำแหน่งของมันมีความพิเศษ มันอยู่ที่ขอบเขตความเป็นเจ้าของ ข้อมูลข้ามโดเมน และถ้าพูดแบบ DDD ก็คือข้ามbounded context ดังนั้นมันต้องปกป้องพื้นที่ของตัวเอง
EnterCriticalSectionชี้ให้เห็น code path ที่เหมาะกับการตรวจสอบอย่างเข้มตอนเข้า รวมถึงการตรวจสอบด้วยเงื่อนไขifและควรมองว่าเป็นขอบเขตโดเมนในทางกลับกัน ถ้ากำลังเขียนแอปพลิเคชันแล้วฟังก์ชันทั่วไปของแอปพลิเคชันมีคำสั่ง
ifอยู่ ก็สามารถผลักออกไปข้างนอกได้อย่างปลอดภัยแม้ในไลบรารีหรือส่วนโค้ดสำคัญ การยก
ifขึ้นไปไว้ที่ขอบ แทนที่จะอยู่ลึกด้านใน ก็ปลอดภัยคำแนะนำว่าให้จัดการโดเมนของตัวเอง อย่าไปเรียกร้องจากโดเมนของคนอื่น และภายในโดเมนนั้นให้ย้าย control flow ไปที่ขอบ ฟังดูสมเหตุสมผล
แน่นอนว่า idiom ก็เป็นแค่ idiom และในโลกจริงต้องให้คนที่เข้าใจบริบทและตัดสินอย่างมีเหตุผลเป็นผู้ประเมิน
ตัวอย่าง “การรีแฟกเตอร์แบบแยก enum” โดยแก่นแล้วคือ พหุสัณฐาน (polymorphism)
สามารถเปลี่ยน
matchให้เป็นการเรียกเมธอดแบบพหุสัณฐานบน enum ได้เป้าหมายคือการแยกจุดที่สร้างการแยกกรณีออกจากจุดที่มี
ifแรกและเรียกใช้fooหรือbarตามนั้นการแยกกรณีจะถูกพกพาไปกับอ็อบเจกต์ ซึ่งในที่นี้คือค่า enum หรือ closure และไม่จำเป็นต้องทำซ้ำอีกที่จุดเรียก
กล่าวคือ เมื่อการแยกกรณีเปลี่ยน ก็เปลี่ยนเฉพาะจุดที่สร้างการแยกนั้น และไม่ต้องเปลี่ยนจุดต่าง ๆ ที่กระตุ้นพฤติกรรมตามแต่ละกรณี
แต่ก็มี trade-off อยู่ การเห็นกรณีแต่ละแบบที่ต้องพิจารณาโดยตรง ณ จุดที่พฤติกรรมถูกเรียกใช้อาจช่วยได้ แต่แลกกับการเพิ่ม dependency ระดับโค้ดต่อรายการกรณีแต่ละแบบ
ตัวสแกนความซับซ้อนของโค้ด สุดท้ายแล้วบังคับให้ดัน
ifลงไปข้างล่าง แต่บทความแนะนำตรงกันข้ามเมื่อยก
ifขึ้นไปข้างบน control flow มักถูกรวมศูนย์ไว้ในฟังก์ชันเดียว ฟังก์ชันนั้นจะมีลอจิกแตกแขนงที่ซับซ้อน แต่ตัวงานจริงถูกมอบหมายให้ subroutine ด้านล่างที่เป็นเส้นตรง⁰ https://docs.sonarsource.com/sonarqube-server/latest/user-gu...
if (weShouldDoThis()) { doThis(); }นี่เป็นส่วนเสริมหรือเป็นส่วนหนึ่งของ functional core และ imperative shell
ถ้าแยกการตรวจสอบออกมาไว้ต่างหาก ก็ทดสอบได้ง่ายขึ้น และถ้ากังวลเรื่องความซับซ้อน ก็แยกแต่ละ clause ในการตรวจสอบออกเป็นฟังก์ชันได้
โดยเฉพาะ Sonar รายงาน “code smell” ไม่ใช่บั๊กจริง
การแก้รายการที่ “ไม่ใช่บั๊ก” แบบนี้ทำให้ความเสี่ยงที่จะเกิดข้อผิดพลาดใหม่เพิ่มจาก 0 เป็นค่าที่มากกว่า 0 และอาจเปลืองเวลานักพัฒนาที่ควรใช้จัดการปัญหา production จริง
คำว่า “local” หมายถึงมันเป็นเช่นนั้นแค่จนกว่าข้อกำหนดจะเปลี่ยน หรือพบกรณียกเว้นจนต้องมีการแตกแขนงนอกลูปด้วย
ถ้ามีการแตกแขนงทั้งในและนอกลูป การให้เหตุผลจะยากขึ้น
ถ้าค่อนข้างมั่นใจว่าเงื่อนไขจะมีผลเฉพาะในลูปเท่านั้น จะวางไว้ตรงนั้นก็ได้
แต่ถ้าไม่ยากที่จะจินตนาการถึงข้อกำหนดที่ต้องแตกแขนงนอกลูปด้วย การออกแบบโครงสร้างเผื่อไว้ตั้งแต่แรกอาจดีกว่า
โค้ดอาจยืดยาวขึ้น แต่ตามอ่านง่ายกว่า และโอกาสที่จะกลายเป็นสปาเกตตีในภายหลังก็ลดลง
นี่คือเหตุผลที่ผมเลิกใช้ Haskell Haskell ทำให้อยากเขียนตรรกะที่กระชับที่สุดและเป็น “local optimum” แต่สิ่งนั้นเอนเอียงไปทางการแสดงตัวตรรกะเอง มากกว่าความตั้งใจของตรรกะ เมื่อข้อกำหนดเปลี่ยนเล็กน้อย อาจต้องคลี่แก้อย่างน่าสยดสยอง
การมีลอจิกอยู่ในที่เดียวอ่านง่ายกว่ามาก และควรค่อยแยกก็ต่อเมื่อรายละเอียดเริ่มทำให้มองภาพใหญ่ไม่ออก
บางครั้งผมชอบให้ลอจิกเงื่อนไขอยู่ในฟังก์ชันที่ถูกเรียก เพราะมันช่วยกันไม่ให้ผู้เรียกเผลอทำงานใน ลำดับที่ผิด
เช่น ถ้าต้องการสร้าง operation แบบ idempotent ก็อาจตรวจสอบก่อนว่างานนั้นเสร็จไปแล้วหรือยัง ถ้ายังก็ทำงานนั้น
ถ้าดันเงื่อนไขนั้นออกไปนอกผู้เรียก ผู้เรียกทุกจุดของฟังก์ชันจะต้องตรวจเองว่าเรียกอย่างถูกต้องเพื่อให้ได้หลักประกันเรื่อง idempotency และเราไม่สามารถ abstract หลักประกันนั้นให้ได้
บางกรณีต้องรันชุดการตรวจสอบก่อนทำงานบางอย่างภายใน database transaction ด้วย ถ้าจะใช้ปรัชญานี้แล้วอยากให้การตรวจสอบยังอยู่ภายในขอบเขต transaction ควรทำอย่างไร?
ถ้าดันเงื่อนไขออกไปนอกผู้เรียก ฟังก์ชันนั้นก็ไม่เป็น idempotent อีกต่อไป ดังนั้นย่อมให้หลักประกันไม่ได้
แต่ถ้าฟังก์ชันเดี่ยว ๆ ต้อง implement การจัดการสถานะเพื่อให้ idempotency ได้ โครงสร้างนั้นก็น่าจะค่อนข้างแปลก และดูเหมือนมีลอจิกมากเกินไปเกิดขึ้นในฟังก์ชันเดียว
โค้ดแบบ idempotent โดยทั่วไปแบ่งเป็นสองกลุ่ม
กลุ่มหนึ่งคือกรณีที่ data model และ operation ที่ทำมีความเป็น idempotent โดยเนื้อแท้ อาจเป็น operation ไร้สถานะ หรือ operation สไตล์
PUTที่ข้อมูลอินพุตมีสถานะทั้งหมดที่ต้องบันทึกอยู่แล้วอีกกลุ่มคือ business operation ที่ซับซ้อนกว่า ซึ่งสร้าง abstraction แบบ idempotent ด้วยการให้ abstraction ของการ apply แบบ atomic ที่ทำ rollback หรือรับประกันว่า partial failure จะไม่ทำให้สถานะเสีย
ในกรณีแรก ไม่จำเป็นต้องตรวจลำดับการทำงาน เพราะมันเป็น idempotent โดยเนื้อแท้ ก็แค่ทำซ้ำได้เลย
ในกรณีที่สอง ใช้ abstraction ง่าย ๆ ไม่ได้ ต้องบันทึกงานที่ต้องการทำ รับประกันให้งานนั้นสำเร็จหรือ failed แล้วทำให้สถานะสำเร็จหรือ failed นั้นคงอยู่ถาวร
ลอจิกประเภทนั้นไม่ใช่สิ่งที่ควรใส่ไว้ในฟังก์ชันเดียวแล้วนำไป compose กับ operation อื่น
คำแนะนำเหล่านี้มีจุดยืนแรงมาก จึงไม่ควรปฏิบัติเหมือนเป็นกฎจากประสบการณ์
ผมมองว่าแทบไม่มีกฎจากประสบการณ์อยู่ในนี้เลย ถ้าจะฝืนสร้างขึ้นมา ก็น่าจะพูดกลับกัน
ifควรถูกดันลงไปข้างล่างเพราะ DRYถ้าประสิทธิภาพเอื้อ ควรพิจารณายก
forขึ้นไปข้างบน เพราะจะสามารถใช้filter/map/reduceและการประกอบฟังก์ชัน เพื่อเลือกว่าจะนำพฤติกรรมใดไปใช้กับอ็อบเจกต์ใด และโดยเนื้อแท้ก็ทำให้โค้ดเป็น เวกเตอร์ไลซ์ ได้การดัน
ifลงไปข้างล่างมักขัดขวางการเวกเตอร์ไลซ์กรณีที่บทความพูดถึงคือกรณีที่ไม่ DRY โดยเฉพาะเมื่อ type ถูก tag ไว้ภายใน ทำให้การแตกแขนงคล้าย ๆ กันต้องแพร่กระจายไปยังฟังก์ชันจำนวนมากที่อยู่ลึกลงไปใน stack
ผมไม่มั่นใจว่านี่เป็นกฎ “ที่ดี” ที่ควรทำตาม
บางครั้งอาจใช่ แต่ขึ้นกับ บริบท มากเกินไปจนสรุปได้ยาก
ให้ความรู้สึกคล้ายกฎ “i มาก่อน e ยกเว้นหลัง c” มีข้อยกเว้นมากจนแทบไม่ต่างจากไม่มีกฎเลย
ได้เวอร์ชันที่คล้ายกันนี้มาจาก 99 Bottles of OOP ของ Sandi Metz
โดยรวมแล้วไม่ใช่สไตล์ของผม แต่เวลาเจอโค้ดเบสที่ส่ง flag จำนวนมากลงไปลึกหลายชั้น ประเด็นที่ว่าให้ย้ายทางแยกของลอจิกขึ้นไปไว้ด้านบนของ call stack นั้นน่าเชื่อถือมาก
https://sandimetz.com/99bottles
การเอา branching เข้าไปไว้ในลูป
forคือ abstraction ที่บอกว่า “ลูปforคือกฎ และ branching คือพฤติกรรม”แต่บ่อยมากที่ requirement ใหม่มาทำลาย abstraction นั้น
จากนั้นก็ต้องอ้อม และโค้ดที่ได้ก็จะมี abstraction ที่ใช้ได้ในบางกรณีแต่ใช้ไม่ได้ในบางกรณี หรือไม่ก็ยัดพารามิเตอร์จำนวนมากเข้าไปใน abstraction เพื่อให้ใช้ได้ทุกที่จนตามอ่านยาก
ถ้าไม่ได้สร้าง abstraction นั้นตั้งแต่แรก โค้ดที่ได้อาจแก้ไขและทำความเข้าใจได้ง่ายกว่า
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
เพื่อความอ่านง่ายของโค้ด การดันทุกอย่างลงไปข้างล่างมักจะดีกว่า
printInvoice(invoice, options)ดีกว่าif(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}}มากลูปก็เช่นกัน
printInvoices(invoices)ดีกว่าfor(invoice of invoices){ printInvoice(invoice) }สุดท้ายแล้วความอ่านง่ายของโค้ดสำคัญมาก แต่ encapsulation สำคัญกว่า จึงต้องผสมทั้งสองอย่างให้เหมาะสม
printInvoiceควรพิมพ์ใบแจ้งหนี้ถ้าหนึ่งในเงื่อนไขที่ชื่อระบุไว้เป็นเท็จจนพิมพ์ใบแจ้งหนี้ไม่ได้ จะเกิดอะไรขึ้น?
อาจโยน exception หรือคืนค่า sentinel หรือชนิด error กลับมาได้ แต่ในกรณีนั้นสิ่งที่ควรทำต่อไม่ได้ชัดเจนทันที
โดยเฉพาะในภาษาอย่าง Java หรือ C++ ที่มักไม่ชอบใช้ exception กับ control flow ทั่วไป และ error แบบ monad ก็ไม่แพร่หลาย การจัดโครงสร้างให้ใกล้กับสไตล์ที่สองอาจดีกว่า
อย่างไรก็ตาม ถ้ารูปแบบแนวตั้งไม่ได้หมายถึงข้อผิดพลาด ก็ควรให้เครื่องพิมพ์ใบแจ้งหนี้เป็นฝ่ายจัดการ
encapsulation ดูเหมือนจะเป็นเครื่องมือหลัก ๆ เพื่อความอ่านง่ายของโค้ดในระยะยาว, การ refactor และการเปลี่ยนแปลงเฉพาะจุด, รวมถึงความสามารถในการสนใจเฉพาะ object ในบริเวณนั้นแล้วอนุมานพฤติกรรมโดยรวมได้
ดังนั้นการเอาความอ่านง่ายกับ encapsulation มาเทียบกันแล้วมองว่าอย่างใดอย่างหนึ่งสำคัญกว่าจึงรู้สึกเหมือนเป็น category error
ควรทำแบบนี้แทน
if(!printerReady){ return; }if(!printerHasInk){ return; }if(!printerHasPaper){ return; }if(!invoiceFormatIsPortrait){ return; }อ่านง่ายกว่าลูกศรที่ยิ่งขยายใหญ่ขึ้นมาก
แต่การเอาลูปไปไว้ในฟังก์ชันของมันเอง และจัดการสมมติฐานอื่น ๆ ไว้เรียบร้อยแล้วนั้นเป็นเรื่องดี
ตอนกระดาษหมด ตัวเครื่องพิมพ์เองต้องไม่ปล่อยให้ล้อหมุนฟรีเด็ดขาด ผมจะเอาการตรวจสอบนั้นไว้ในฟังก์ชัน
maybe_print_invoiceซึ่งผมชอบแบบนี้มากกว่า