Beyond Functional Core: นักพัฒนาถกประเด็นสถาปัตยกรรมที่สำคัญจริงๆ

ทีมชุมชน BigGo
Beyond Functional Core: นักพัฒนาถกประเด็นสถาปัตยกรรมที่สำคัญจริงๆ

ในสาขาวิศวกรรมซอฟต์แวร์ การหาวิธีจัดโครงสร้างโค้ดที่เหมาะสมเป็นความท้าทายตลอดกาล แม้รูปแบบ Functional core, Imperative shell จะได้รับความนิยมสำหรับการแยกตรรกะธุรกิจออกจากผลข้างเคียง แต่การสนทนาล่าสุดในชุมชนเผยให้เห็นข้อมูลเชิงลึกที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับสิ่งที่ทำให้โค้ดสามารถบำรุงรักษาและทดสอบได้จริง นักพัฒนากำลังตั้งคำถามว่าการแบ่งแยกระหว่าง Functional กับ Imperative อาจกำลังมองข้ามภาพที่ใหญ่กว่ากันอยู่หรือไม่

การอภิปรายหลักปรัชญา: Generic กับ Specific

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

Functional กับ imperative เป็นประเด็นรองมากในความเห็นของผม ส่วนใหญ่เป็นสิ่งที่ทำให้ไขว้เขว ปรัชญาที่ถูกต้องคือ 'Generic core, specific shell'

มุมมองนี้ท้าทายข้อสันนิษฐานที่ว่าการเขียนโปรแกรมเชิงฟังก์ชันเป็นทางเดียวสู่สถาปัตยกรรมที่สะอาดตา ในทางกลับกัน มันเน้นย้ำว่าชั้นธุรกิจควรเป็นรากฐานของแอปพลิเคชันที่มีโครงสร้างดีใดๆ ก็ตาม โดยไม่คำนึงว่ามันจะถูกนำไปใช้ในรูปแบบ Functional หรือ Imperative ข้อมูลเชิงลึกที่สำคัญคือการแยกสิ่งที่ระบบของคุณทำออกจากวิธีการทำนั้น สำคัญกว่าการใช้กระบวนทัศน์การเขียนโปรแกรมใดๆ

รูปแบบโครงสร้างโค้ดที่นำมาเปรียบเทียบ:

  • Functional Core, Imperative Shell: แยกตmanticsิกทางธุรกิจที่บริสุทธิ์ออกจาก side effects
  • Generic Core, Specific Shell: ตรรกะทางธุรกิจที่สามารถนำกลับมาใช้ใหม่ได้โดยไม่ขึ้นกับการ implement
  • Command-Query Separation: ฟังก์ชันจะทำการดำเนินการ หรือ ส่งคืนข้อมูล ไม่ทำทั้งสองอย่างพร้อมกัน

ความกังวลและการนำไปใช้จริงทางเลือก

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

ชุมชนการเขียนโปรแกรมต่างๆ ได้พัฒนาวิธีแก้ปัญหาของตนเองสำหรับปัญหานี้ นักพัฒนา Elixir ชี้ไปที่ตัวดำเนินการ pipe ของพวกเขาในฐานะวิธีแก้ปัญหาที่สง่างาม ในขณะที่ผู้ที่ชื่นชอบ Python ชอบนิพจน์ generator นักพัฒนา JavaScript เสนอให้แบ่งการดำเนินการออกเป็นฟังก์ชันขนาดเล็กที่ประกอบเข้าด้วยกันได้มากขึ้น หัวข้อทั่วไปคือการหาวิธีรักษาการแยกส่วนของความกังวลโดยไม่เสียสละการอ่านและการแก้ไขข้อบกพร่อง

นักพัฒนาบางส่วนสนับสนุนแนวทางที่ตรงไปตรงมามากขึ้น นั่นคือการเขียนแต่ละขั้นตอนในบรรทัดของตัวเองด้วยชื่อตัวแปรที่ชัดเจน รูปแบบการดำเนินงาน (Procedural) นี้ แม้จะดูไม่สวยหรูเท่าการประกอบกันแบบ Functional แต่มักพิสูจน์แล้วว่าง่ายกว่าสำหรับทีมในการบำรุงรักษาและแก้ไขข้อบกพร่องในสภาพแวดล้อมองค์กร ทางเลือกระหว่างโค้ดบรรทัดเดียวที่สวยงามกับการประมวลผลหลายขั้นตอนที่ชัดเจนขึ้นอยู่กับประสบการณ์ของทีมและบริบทเฉพาะเป็นอย่างมาก

การใช้งานเฉพาะในแต่ละภาษา:

  • Elixir: ใช้ pipe operator เพื่อให้ function chains อ่านง่าย
  • JavaScript: อาศัย method chaining ด้วย filter/map
  • Python: เลือกใช้ generator expressions เพื่อประสิทธิภาพด้านหน่วยความจำ
  • Traditional: การกำหนดค่าตัวแปรทีละขั้นตอนอย่างชัดเจนเพื่อการ debugging

การพิจารณาด้านการทดสอบและความปลอดภัย

การแยกระหว่างการดำเนินการสอบถาม (Querying) และการสั่งการ (Commanding) นำมาซึ่งประโยชน์ด้านการทดสอบและความปลอดภัยที่สำคัญ ด้วยการแยกโค้ดการตัดสินใจบริสุทธิ์ออกจากการดำเนินการที่มีผลข้างเคียง ทีมสามารถเขียนการทดสอบหน่วยที่เร็วขึ้นและมีโฟกัสมากขึ้นสำหรับตรรกะธุรกิจของพวกเขา อย่างไรก็ตาม การแยกนี้ยังเพิ่มความกังวลด้านความปลอดภัยที่ต้องได้รับการจัดการอย่างรอบคอบ

หลักการ Command-Query Separation ชี้ให้เห็นว่าฟังก์ชันควรทำการดำเนินการ (Commands) หรือส่งคืนข้อมูล (Queries) อย่างใดอย่างหนึ่ง แต่ไม่ใช่ทั้งสองอย่าง ในขณะที่สิ่งนี้สร้างขอบเขตที่สะอาดขึ้น แต่มันอาจเปิดช่องโหว่ด้านความปลอดภัยได้หากคำสั่งต่างๆ ไม่ได้ตรวจสอบเงื่อนไขล่วงหน้าของพวกเขาอย่างเหมาะสม นักพัฒนาต้องมั่นใจว่าแม้ในขณะที่แยกส่วนของความกังวล การตรวจสอบความปลอดภัยยังคงมีความแข็งแกร่งทั่วทั้งระบบ

การพิจารณาด้านประสิทธิภาพก็เข้ามามีบทบาทเช่นกัน เมื่อตรรกะการตรวจสอบความถูกต้องถูกแยกออกจากการดำเนินการคำสั่ง นักพัฒนาต้องเผชิญกับการแลกเปลี่ยนระหว่างประสิทธิภาพและความปลอดภัย การตรวจสอบซ้ำๆ อาจส่งผลกระทบต่อประสิทธิภาพ ในขณะที่การข้ามการตรวจสอบอาจสร้างช่องโหว่ด้านความปลอดภัย การหาจุดสมดุลที่เหมาะสมต้องอาศัยการออกแบบอย่างรอบคอบและความเข้าใจในข้อกำหนดของแอปพลิเคชันเฉพาะ

ประเด็นการแลกเปลี่ยนที่สำคัญที่ระบุได้:

  • ความอ่านง่าย vs. ความสามารถในการประกอบ: ฟังก์ชันแบบเชื่อมโซ่เทียบกับขั้นตอนที่ชัดเจน
  • ประสิทธิภาพ vs. ความปลอดภัย: การวางตำแหน่งการตรวจสอบความถูกต้องในการรันคำสั่ง
  • ความบริสุทธิ์ vs. ความเป็นจริง: การจัดการกับการดำเนินการที่เป็นคำสั่งโดยธรรมชาติ เช่น ธุรกรรม

เกินกว่ารูปแบบ: การปรับตัวในโลกจริง

การอภิปรายเผยให้เห็นว่าทีมที่ประสบความสำเร็จมักปรับรูปแบบสถาปัตยกรรมให้สอดคล้องกับความต้องการเฉพาะของพวกเขา แทนที่จะนำไปใช้อย่างดื้อดัน นักพัฒนาบางคนได้พบรูปแบบที่คล้ายกันผ่านประสบการณ์เชิงปฏิบัติ ในขณะที่คนอื่นๆ พบว่าสถานการณ์จริงบางอย่าง—เช่น การจัดการธุรกรรม—ไม่เข้ากับโมเดล Functional core อย่างสมบูรณ์

การดำเนินการจัดการทรัพยากร เช่น การเปิดการเชื่อมต่อฐานข้อมูลหรือการจัดการธุรกรรม มักต้องการแนวทางแบบ Imperative มากขึ้น ดังที่นักพัฒนาคนหนึ่งระบุ บางสิ่งโดยธรรมชาติแล้วเป็นแบบ Imperative อย่างชัดเจน เปิด/ปิด/ได้รับ/ปล่อย ล้วนเข้ามาในความคิด นี่ชี้ให้เห็นว่าในขณะที่รูปแบบ Functional core ให้คำแนะนำที่มีค่า แต่มันไม่ใช่วิธีแก้ปัญหาที่เหมาะกับทุกสถานการณ์

การนำไปใช้ที่ประสบความสำเร็จมากที่สุดดูเหมือนจะเป็นผู้ที่เข้าใจหลักการพื้นฐาน มากกว่าเพียงแค่ทำตามรูปแบบอย่างกลไก ไม่ว่าจะผ่าน Monad ในภาษา Functional, Dependency injection ในระบบเชิงวัตถุ หรือการแยกแบบ Procedural อย่างง่าย เป้าหมายยังคงเหมือนเดิม นั่นคือการสร้างระบบที่สามารถทดสอบได้ บำรุงรักษาได้ และปรับตัวให้เข้ากับการเปลี่ยนแปลงได้

การสนทนาที่ดำเนินต่อไปในชุมชนนักพัฒนาแสดงให้เห็นว่ารูปแบบสถาปัตยกรรมยังคงพัฒนาต่อไป ในขณะที่ Functional core, Imperative shell ให้กรอบการทำงานที่มีประโยชน์ แต่คุณค่าที่แท้จริงมาจากการทำความเข้าใจว่าทำไมการแยกนี้จึงสำคัญ และการปรับหลักการให้เหมาะสมกับความต้องการของโครงการเฉพาะและความสามารถของทีม

อ้างอิง: Simplify Your Code: Functional Core, Imperative Shell