ฟังก์ชั่นการเขียนโปรแกรมเป็นทางเลือกที่ทำงานได้กับรูปแบบการฉีดพึ่งพาหรือไม่


21

เมื่อเร็ว ๆ นี้ฉันได้อ่านหนังสือที่ชื่อว่าFunctional Programming ใน C #และมันเกิดขึ้นกับฉันว่าการเขียนโปรแกรมเชิงหน้าที่ที่ไม่เปลี่ยนรูปแบบและไร้รัฐได้ผลลัพธ์ที่คล้ายคลึงกับรูปแบบการฉีดแบบพึ่งพาและอาจเป็นวิธีที่ดีกว่าโดยเฉพาะในเรื่องการทดสอบหน่วย

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


10
สิ่งนี้ไม่สมเหตุสมผลสำหรับฉัน
Telastyn

ฉันยอมรับว่ามันไม่ได้ลบการพึ่งพา มันอาจจะเป็นความเข้าใจของฉันที่ไม่ถูกต้อง แต่ฉันได้ทำการอนุมานเพราะถ้าฉันไม่สามารถเปลี่ยนวัตถุดั้งเดิมได้ฉันต้องส่งผ่านมันไป (ฟังก์ชั่น) ไปยังฟังก์ชันใด ๆ ที่ใช้งานมัน
Matt Cashatt


5
นอกจากนี้ยังมีวิธีหลอกผู้เขียนโปรแกรม OO ให้รักการเขียนโปรแกรมฟังก์ชั่นซึ่งเป็นการวิเคราะห์รายละเอียดของ DI จากทั้ง OO และมุมมอง FP
Robert Harvey

1
คำถามนี้บทความที่ลิงก์ไปยังและคำตอบที่ยอมรับอาจมีประโยชน์เช่น: stackoverflow.com/questions/11276319/… ละเว้นคำ Monad ที่น่ากลัว ในฐานะที่เป็น Runar ชี้ให้เห็นในคำตอบของเขามันไม่ได้เป็นแนวคิดที่ซับซ้อนในกรณีนี้ (เพียงฟังก์ชั่น)
itsbruce

คำตอบ:


27

การจัดการการพึ่งพาเป็นปัญหาใหญ่ใน OOP ด้วยเหตุผลสองประการต่อไปนี้:

  • การมีเพศสัมพันธ์อย่างแน่นหนาของข้อมูลและรหัส
  • การใช้ผลข้างเคียงอย่างแพร่หลาย

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

การใช้ผลข้างเคียงทำให้เกิดปัญหาที่คล้ายกัน หากคุณใช้ผลข้างเคียงสำหรับฟังก์ชั่นบางอย่าง แต่ต้องการที่จะสลับการใช้งานไปได้คุณแทบไม่มีทางเลือกอื่นนอกจากต้องพึ่งพาการใช้งานนั้น

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

  • ป้อนที่อยู่หน้าเว็บเอาท์พุทข้อความของหน้านั้น
  • ป้อนข้อความของหน้าออกรายการลิงก์จากหน้านั้น
  • ป้อนข้อความของหน้าออกรายการที่อยู่อีเมลในหน้านั้น
  • ป้อนรายการที่อยู่อีเมลส่งออกรายการที่อยู่อีเมลโดยลบรายการที่ซ้ำกัน
  • ป้อนที่อยู่อีเมลส่งอีเมลสแปมสำหรับที่อยู่นั้น
  • ป้อนอีเมลสแปมออกคำสั่ง SMTP เพื่อส่งอีเมลนั้น

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

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

โปรดทราบว่าไม่ใช่การพึ่งพาตัวเองที่เป็นปัญหา มันเป็นการพึ่งพาที่ชี้ทางที่ผิด เลเยอร์ถัดไปขึ้นอาจมีฟังก์ชันดังนี้:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

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

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

การจัดระเบียบใหม่นี้ทำได้โดยไม่มีผลข้างเคียง ฟังก์ชั่นเลเยอร์ที่ต่ำกว่าเป็นอิสระจากกันอย่างสมบูรณ์ เลเยอร์ถัดไปอาจเลือกสิ่งที่processTextใช้จริงตามการกำหนดค่าผู้ใช้บางอย่าง:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

ไม่ใช่ปัญหาเนื่องจากการอ้างอิงทั้งหมดชี้ทางเดียว เราไม่จำเป็นต้องสลับการพึ่งพาบางอย่างเพื่อให้พวกเขาชี้ไปในทางเดียวกันเพราะฟังก์ชั่นที่บริสุทธิ์ได้บังคับให้เราทำเช่นนั้น

โปรดทราบว่าคุณสามารถทำสิ่งนี้ได้มากขึ้นควบคู่ไปกับการส่งconfigผ่านไปยังเลเยอร์ต่ำสุดแทนที่จะตรวจสอบที่ด้านบน FP ไม่ได้ป้องกันไม่ให้คุณทำสิ่งนี้ แต่มีแนวโน้มที่จะทำให้มันน่ารำคาญมากขึ้นถ้าคุณลอง


3
"การใช้ผลข้างเคียงจะสร้างปัญหาที่คล้ายกันหากคุณใช้ผลข้างเคียงสำหรับฟังก์ชั่นบางอย่าง แต่ต้องการที่จะสลับการใช้งานไปได้คุณแทบไม่มีทางเลือกอื่นนอกจากต้องพึ่งพาการใช้งานแบบนั้น" ฉันไม่คิดว่าผลข้างเคียงเกี่ยวข้องกับสิ่งนี้ หากคุณต้องการใช้งานแลกเปลี่ยนใน Haskell, คุณยังคงต้องทำฉีดพึ่งพา เปิดคลาสประเภทและคุณจะผ่านส่วนต่อประสานเป็นอาร์กิวเมนต์แรกของทุกฟังก์ชั่น
Doval

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

@ Doval - ขอบคุณสำหรับความคิดเห็นที่น่าสนใจและกระตุ้นความคิดของคุณ ฉันอาจเข้าใจผิดคุณ แต่ฉันถูกต้องในการอนุมานจากความคิดเห็นของคุณว่าถ้าฉันจะใช้รูปแบบการทำงานของการเขียนโปรแกรมมากกว่าสไตล์ DI (ในความรู้สึกแบบดั้งเดิม C #) แล้วฉันจะหลีกเลี่ยงการแก้จุดบกพร่อง ความละเอียดของการอ้างอิง?
Matt Cashatt

@MatthewPatrickCashatt ไม่ใช่เรื่องของสไตล์หรือกระบวนทัศน์ แต่เป็นคุณสมบัติทางภาษา หากภาษาไม่รองรับโมดูลเป็นสิ่งที่ดีที่สุดคุณจะต้องทำบางอย่างในรูปแบบไดนามิกและการพึ่งพาการฉีดเพื่อแลกเปลี่ยนการใช้งานเพราะไม่มีวิธีที่จะแสดงการพึ่งพาแบบคงที่ ที่จะนำมันแตกต่างกันเล็กน้อยถ้าโปรแกรมของคุณ C # System.Stringใช้สตริงก็มีการพึ่งพายากรหัสบน ระบบโมดูลจะช่วยให้คุณแทนที่System.Stringด้วยตัวแปรเพื่อให้ตัวเลือกของการใช้งานสตริงไม่ได้รับการเข้ารหัสอย่างหนัก แต่ยังคงได้รับการแก้ไขในเวลารวบรวม
Doval

8

ฟังก์ชั่นการเขียนโปรแกรมเป็นทางเลือกที่ทำงานได้กับรูปแบบการฉีดพึ่งพา?

สิ่งนี้ทำให้ฉันเป็นคำถามแปลก ๆ ฟังก์ชั่นการเขียนโปรแกรมฟังก์ชั่นส่วนใหญ่จะสัมผัสกับการฉีดพึ่งพา

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

แต่มันไม่ได้ลบการพึ่งพา การดำเนินการของคุณยังคงต้องการข้อมูล / การดำเนินการทั้งหมดที่พวกเขาต้องการเมื่อสถานะของคุณไม่แน่นอน และคุณยังต้องได้รับการพึ่งพาเหล่านั้นที่นั่น ดังนั้นฉันจะไม่พูดว่าวิธีการเขียนโปรแกรมการทำงานแทนที่ DI เลยไม่มีทางเลือก

หากมีสิ่งใดพวกเขาเพิ่งแสดงให้คุณเห็นว่ารหัส OO ที่ไม่ดีสามารถสร้างการพึ่งพาโดยนัยได้อย่างไรกว่าโปรแกรมเมอร์ไม่ค่อยคิด


ขอขอบคุณอีกครั้งที่มีส่วนร่วมในการสนทนา Telastyn ตามที่คุณได้ชี้ให้เห็นคำถามของฉันไม่ได้ถูกสร้างขึ้นอย่างดี (คำพูดของฉัน) แต่ด้วยความคิดเห็นที่นี่ฉันเริ่มเข้าใจดีขึ้นว่ามันคืออะไรที่เกิดขึ้นในสมองของฉันเกี่ยวกับสิ่งเหล่านี้: เราทุกคนเห็นด้วย (ฉันคิดว่า) การทดสอบหน่วยสามารถเป็นฝันร้ายโดยไม่มี DI น่าเสียดายที่การใช้ DI โดยเฉพาะกับคอนเทนเนอร์ IoC สามารถสร้างรูปแบบใหม่ของการดีบักฝันร้ายด้วยความจริงที่ว่ามันสามารถแก้ไขการพึ่งพาที่ runtime คล้ายกับ DI, FP ทำให้การทดสอบหน่วยง่ายขึ้น แต่ไม่มีปัญหาการพึ่งพารันไทม์
Matt Cashatt

(ต่อจากด้านบน) . นี่คือความเข้าใจของฉันในปัจจุบัน โปรดแจ้งให้เราทราบหากฉันทำเครื่องหมายหาย ฉันไม่รังเกียจที่จะยอมรับว่าฉันเป็นเพียงมนุษย์ในหมู่มนุษย์ยักษ์ที่นี่!
Matt Cashatt

@MatthewPatrickCashatt - DI ไม่จำเป็นต้องบ่งบอกถึงปัญหาการพึ่งพารันไทม์ซึ่งอย่างที่คุณทราบมันแย่มาก
Telastyn

7

คำตอบที่รวดเร็วในการตอบคำถามของคุณคือ: ไม่มี

แต่ในขณะที่คนอื่น ๆ ยืนยันคำถามนั้นมีสองแนวคิดที่ไม่เกี่ยวข้องกัน

ลองทำทีละขั้นตอนกัน

DI แสดงผลลัพธ์ในลักษณะที่ไม่ใช้งานได้

ในแกนกลางของการเขียนโปรแกรมฟังก์ชั่นเป็นฟังก์ชั่นบริสุทธิ์ - ฟังก์ชั่นที่แมปอินพุตกับเอาต์พุตดังนั้นคุณจะได้รับเอาต์พุตเดียวกันสำหรับอินพุตที่กำหนด

โดยทั่วไป DI หมายถึงหน่วยของคุณไม่บริสุทธิ์อีกต่อไปเนื่องจากเอาต์พุตอาจแตกต่างกันไปขึ้นอยู่กับการฉีด ตัวอย่างเช่นในฟังก์ชั่นต่อไปนี้:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(ฟังก์ชั่น) อาจแตกต่างกันให้ผลลัพธ์ที่แตกต่างกันสำหรับการป้อนข้อมูลที่ได้รับเดียวกัน สิ่งนี้ทำให้bookSeatsไม่บริสุทธิ์เช่นกัน

มีข้อยกเว้นสำหรับเรื่องนี้ - คุณสามารถฉีดอัลกอริทึมการเรียงลำดับหนึ่งในสองแบบที่ใช้การแมปอินพุต - เอาท์พุตเดียวกันแม้ว่าจะใช้อัลกอริทึมที่แตกต่างกัน แต่สิ่งเหล่านี้เป็นข้อยกเว้น

ระบบไม่สามารถบริสุทธิ์ได้

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

ระบบต้องมีผลข้างเคียงโดยมีตัวอย่างที่ชัดเจนดังนี้:

  • UI
  • ฐานข้อมูล
  • API (ในสถาปัตยกรรมไคลเอนต์ - เซิร์ฟเวอร์)

ดังนั้นส่วนหนึ่งของระบบของคุณต้องเกี่ยวข้องกับผลข้างเคียงและส่วนนั้นอาจเกี่ยวข้องกับรูปแบบที่จำเป็นหรือรูปแบบ OO

กระบวนทัศน์เชลล์ - คอร์

การยืมคำศัพท์จากการพูดคุยที่ยอดเยี่ยมของ Gary Bernhardt ในเรื่องของสถาปัตยกรรมระบบที่ดี (หรือโมดูล) จะรวมสองชั้นนี้:

  • แกน
    • ฟังก์ชั่นที่บริสุทธิ์
    • การแตกแขนง
    • ไม่มีการพึ่งพา
  • เปลือก
    • ไม่บริสุทธิ์ (ผลข้างเคียง)
    • ไม่มีการแตกแขนง
    • การอ้างอิง
    • อาจมีความจำเป็นเกี่ยวข้องกับสไตล์ OO เป็นต้น

สิ่งสำคัญคือการ 'แยก' ระบบออกเป็นส่วนที่บริสุทธิ์ (แกนกลาง) และส่วนที่ไม่บริสุทธิ์ (เปลือก)

ถึงแม้ว่าจะมีวิธีแก้ปัญหาข้อบกพร่องเล็กน้อย (และข้อสรุป) บทความของ Mark Seemannเสนอแนวคิดที่เหมือนกันมาก การใช้งาน Haskell นั้นมีความลึกซึ้งเป็นพิเศษเพราะมันแสดงให้เห็นว่าสามารถทำได้โดยใช้ FP

DI และ FP

การจ้าง DI นั้นสมเหตุสมผลอย่างสมบูรณ์แม้ว่าใบสมัครของคุณจะบริสุทธิ์ กุญแจสำคัญคือการ จำกัด DI ภายในเปลือกที่ไม่บริสุทธิ์

ตัวอย่างจะเป็นสตับ API - คุณต้องการ API จริงในการผลิต แต่ใช้สตับในการทดสอบ การยึดโมเดล shell-core จะช่วยได้มาก

ข้อสรุป

ดังนั้น FP และ DI จึงไม่ใช่ทางเลือก คุณมีแนวโน้มที่จะมีทั้งในระบบของคุณและคำแนะนำคือเพื่อให้แน่ใจว่ามีการแยกระหว่างส่วนที่บริสุทธิ์และไม่บริสุทธิ์ของระบบที่ FP และ DI อาศัยอยู่ตามลำดับ


เมื่อคุณอ้างถึงกระบวนทัศน์เชลล์ - คอร์คุณจะประสบความสำเร็จได้อย่างไรในการแยกสาขาในเชลล์ ฉันสามารถคิดถึงตัวอย่างมากมายที่แอปพลิเคชันจะต้องทำสิ่งที่ไม่บริสุทธิ์อย่างใดอย่างหนึ่งหรืออื่นตามค่า กฎนี้ไม่มีการแตกสาขาในภาษาอย่าง Java หรือไม่?
jrahhali

@jrahhali โปรดดูการพูดคุยของ Gary Bernhardt สำหรับรายละเอียด (เชื่อมโยงในคำตอบ)
Izhaki

ซีรีส์ Seemann อีกชุดที่น่าสนใจblog.ploeh.dk/2017/01/27/…
jk

1

จากมุมมองของฟังก์ชั่น OOP ถือได้ว่าเป็นอินเทอร์เฟซแบบวิธีเดียว

อินเตอร์เฟสเป็นสัญญาที่แข็งแกร่งกว่าฟังก์ชั่น

หากคุณใช้วิธีการทำงานและทำ DI มากมายเมื่อเปรียบเทียบกับการใช้ OOP คุณจะได้ผู้สมัครมากขึ้นสำหรับการพึ่งพาแต่ละครั้ง

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

VS

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
สามารถห่อคลาสใดก็ได้เพื่อใช้งานอินเทอร์เฟซดังนั้น "สัญญาที่แข็งแกร่งกว่า" จึงไม่ค่อยแข็งแกร่งนัก ที่สำคัญกว่าการให้แต่ละฟังก์ชั่นเป็นประเภทที่แตกต่างกันทำให้ไม่สามารถกำหนดองค์ประกอบของฟังก์ชันได้
Doval

ฟังก์ชั่นการเขียนโปรแกรมไม่ได้หมายความว่า "การเขียนโปรแกรมด้วยฟังก์ชั่นการสั่งซื้อที่สูงขึ้น" มันหมายถึงแนวคิดที่กว้างกว่าฟังก์ชั่นการสั่งซื้อที่สูงขึ้นเป็นเพียงหนึ่งเทคนิคที่มีประโยชน์ในกระบวนทัศน์
Jimmy Hoffa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.