Apakah Pemrograman Fungsional merupakan alternatif untuk pola injeksi ketergantungan?

21

Saya baru-baru ini membaca sebuah buku berjudul Pemrograman Fungsional dalam bahasa C # dan saya sadar bahwa sifat pemrograman fungsional yang tidak dapat diubah dan tanpa kewarganegaraan mencapai hasil yang serupa dengan pola injeksi ketergantungan dan mungkin bahkan merupakan pendekatan yang lebih baik, terutama dalam hal pengujian unit.

Saya akan menghargai jika ada orang yang memiliki pengalaman dengan kedua pendekatan dapat berbagi pemikiran dan pengalaman mereka untuk menjawab pertanyaan utama: apakah Pemrograman Fungsional merupakan alternatif untuk pola injeksi ketergantungan?

Matt Cashatt
sumber
10
Ini tidak masuk akal bagi saya, ketidakberdayaan tidak menghilangkan ketergantungan.
Telastyn
Saya setuju bahwa itu tidak menghapus dependensi. Mungkin pemahaman saya yang salah, tapi saya membuat kesimpulan itu karena jika saya tidak dapat mengubah objek asli, saya harus meneruskannya (menyuntikkannya) ke fungsi apa pun yang memanfaatkannya.
Matt Cashatt
5
Ada juga Cara Menipu Pemrogram OO Menjadi Pemrograman Fungsional yang Penuh Kasih , yang benar-benar merupakan analisis terperinci dari DI dari sudut pandang OO dan FP.
Robert Harvey
1
Pertanyaan ini, artikel yang ditautkan, dan jawaban yang diterima mungkin juga berguna: stackoverflow.com/questions/11276319/… Abaikan kata Monad yang menakutkan. Seperti yang ditunjukkan Runar dalam jawabannya, ini bukan konsep yang rumit dalam hal ini (hanya fungsi).
itsbruce

Jawaban:

27

Manajemen ketergantungan adalah masalah besar dalam OOP karena dua alasan berikut:

  • Kopling ketat antara data dan kode.
  • Penggunaan efek samping di mana-mana.

Sebagian besar programmer OO menganggap penggabungan ketat antara data dan kode sepenuhnya menguntungkan, tetapi ada biaya. Mengelola aliran data melalui lapisan adalah bagian yang tidak dapat dihindari dari pemrograman dalam paradigma apa pun. Menggabungkan data dan kode Anda menambah masalah tambahan bahwa jika Anda ingin menggunakan fungsi pada titik tertentu, Anda harus menemukan cara untuk mendapatkan objeknya ke titik itu.

Penggunaan efek samping menciptakan kesulitan yang serupa. Jika Anda menggunakan efek samping untuk beberapa fungsionalitas, tetapi ingin dapat menukar implementasinya, Anda tidak punya pilihan lain selain menyuntikkan ketergantungan itu.

Pertimbangkan sebagai contoh program spammer yang mengikis halaman web untuk alamat email lalu mengirimnya melalui email. Jika Anda memiliki pola pikir DI, saat ini Anda sedang memikirkan layanan yang akan Anda enkapsulasi di belakang antarmuka, dan layanan mana yang akan disuntikkan di mana. Saya akan meninggalkan desain itu sebagai latihan untuk pembaca. Jika Anda memiliki pola pikir FP, saat ini Anda sedang memikirkan input dan output untuk lapisan fungsi terendah, seperti:

  • Masukkan alamat halaman web, keluarkan teks dari halaman itu.
  • Masukkan teks halaman, tampilkan daftar tautan dari halaman itu.
  • Masukkan teks halaman, tampilkan daftar alamat email pada halaman itu.
  • Masukkan daftar alamat email, buat daftar alamat email dengan duplikat yang dihapus.
  • Masukkan alamat email, buat email spam untuk alamat itu.
  • Masukkan email spam, output perintah SMTP untuk mengirim email itu.

Ketika Anda berpikir dalam hal input dan output, tidak ada dependensi fungsi, hanya dependensi data. Itulah yang membuat mereka begitu mudah disatukan. Layer up berikutnya mengatur untuk output dari satu fungsi untuk dimasukkan ke input berikutnya, dan dapat dengan mudah menukar berbagai implementasi sesuai kebutuhan.

Dalam arti yang sangat nyata, pemrograman fungsional secara alami mendorong Anda untuk selalu membalikkan dependensi fungsi Anda, dan karena itu Anda biasanya tidak perlu mengambil tindakan khusus untuk melakukannya setelah fakta. Ketika Anda melakukannya, alat-alat seperti fungsi tingkat tinggi, penutup, dan aplikasi parsial membuatnya lebih mudah dicapai dengan pelat ketel yang lebih sedikit.

Perhatikan bahwa bukan dependensi itu sendiri yang bermasalah. Ketergantungan itu menunjuk ke arah yang salah. Lapisan berikutnya mungkin memiliki fungsi seperti:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Tidak apa-apa untuk lapisan ini untuk memiliki dependensi yang dikodekan secara keras seperti ini, karena tujuan utamanya adalah untuk merekatkan fungsi-fungsi lapisan bawah bersama-sama. Mengganti implementasi sama mudahnya dengan membuat komposisi yang berbeda:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Komposisi ulang yang mudah ini dimungkinkan oleh kurangnya efek samping. Fungsi lapisan bawah sepenuhnya independen satu sama lain. Lapisan berikutnya dapat memilih yang processTextsebenarnya digunakan berdasarkan beberapa konfigurasi pengguna:

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

Sekali lagi, bukan masalah karena semua dependensi menunjukkan satu arah. Kita tidak perlu membalikkan beberapa dependensi agar semuanya menunjuk dengan cara yang sama, karena fungsi murni sudah memaksa kita untuk melakukannya.

Perhatikan bahwa Anda dapat membuat ini lebih banyak digabungkan dengan melewati configlapisan paling bawah daripada memeriksanya di atas. FP tidak mencegah Anda dari melakukan ini, tetapi cenderung membuatnya lebih menyebalkan jika Anda mencoba.

Karl Bielefeldt
sumber
3
"Penggunaan efek samping menciptakan kesulitan yang serupa. Jika Anda menggunakan efek samping untuk beberapa fungsi, tetapi ingin dapat menukar implementasinya, Anda tidak punya pilihan lain selain menyuntikkan ketergantungan itu." Saya tidak berpikir efek samping ada hubungannya dengan ini. Jika Anda ingin menukar implementasi di Haskell, Anda masih harus melakukan injeksi ketergantungan . Desugar kelas tipe dan Anda membagikan antarmuka sebagai argumen pertama untuk setiap fungsi.
Doval
2
Inti masalahnya adalah bahwa hampir setiap bahasa memaksa Anda untuk referensi hard-code ke modul kode lainnya, jadi satu-satunya cara untuk bertukar implementasi adalah dengan menggunakan pengiriman dinamis di mana-mana, dan kemudian Anda terjebak menyelesaikan dependensi Anda pada saat run time. Sistem modul akan memungkinkan Anda mengekspresikan grafik dependensi pada waktu pengecekan tipe.
Doval
@ Doval - Terima kasih atas komentar Anda yang menarik dan menggugah pikiran. Saya mungkin salah paham dengan Anda, tetapi apakah saya benar dalam menyimpulkan dari komentar Anda bahwa jika saya menggunakan gaya pemrograman fungsional daripada gaya DI (dalam pengertian C # tradisional), maka saya akan menghindari kemungkinan frustrasi debugging yang terkait dengan run-time resolusi ketergantungan?
Matt Cashatt
@MatthewPatrickCashatt Ini bukan masalah gaya atau paradigma, tetapi fitur bahasa. Jika bahasa tidak mendukung modul sebagai hal-hal kelas satu, Anda harus melakukan beberapa bentuk pengiriman dinamis dan injeksi ketergantungan untuk bertukar implementasi, karena tidak ada cara untuk mengekspresikan dependensi secara statis. Untuk membuatnya sedikit berbeda, jika program C # Anda menggunakan string, ia memiliki ketergantungan pada kode keras System.String. Sebuah sistem modul akan membiarkan Anda mengganti System.Stringdengan variabel sehingga pilihan implementasi string tidak sulit dikodekan, tetapi masih diselesaikan pada waktu kompilasi.
Doval
8

Apakah Pemrograman Fungsional merupakan alternatif untuk pola injeksi ketergantungan?

Ini mengejutkan saya sebagai pertanyaan aneh. Pendekatan Pemrograman Fungsional sebagian besar bersinggungan dengan injeksi ketergantungan.

Tentu saja, memiliki keadaan tidak berubah dapat mendorong Anda untuk tidak "menipu" dengan memiliki efek samping atau menggunakan status kelas sebagai kontrak implisit antara fungsi. Itu membuat melewati data lebih eksplisit, yang saya kira adalah bentuk paling mendasar dari injeksi ketergantungan. Dan konsep pemrograman fungsional melewati fungsi membuat itu jauh lebih mudah.

Tapi itu tidak menghilangkan ketergantungan. Operasi Anda masih membutuhkan semua data / operasi yang mereka butuhkan ketika keadaan Anda bisa berubah. Dan Anda masih perlu mendapatkan dependensi itu di sana. Jadi saya tidak akan mengatakan bahwa pendekatan pemrograman fungsional menggantikan DI sama sekali, jadi tidak ada alternatif.

Jika ada, mereka baru saja menunjukkan kepada Anda betapa buruknya kode OO dapat membuat dependensi implisit daripada yang jarang dipikirkan oleh programmer.

Telastyn
sumber
Sekali lagi terima kasih telah berkontribusi pada percakapan, Telastyn. Seperti yang telah Anda tunjukkan, pertanyaan saya tidak dibangun dengan baik (kata-kata saya), tetapi berkat umpan balik di sini saya mulai memahami sedikit lebih baik apa yang memicu dalam pikiran saya tentang semua ini: Kita semua setuju (Saya pikir) bahwa pengujian unit bisa menjadi mimpi buruk tanpa DI. Sayangnya, penggunaan DI, terutama dengan wadah IoC dapat membuat bentuk baru dari mimpi buruk debugging berkat fakta bahwa ia menyelesaikan dependensi saat runtime. Mirip dengan DI, FP membuat pengujian unit lebih mudah, tetapi tanpa masalah ketergantungan runtime.
Matt Cashatt
(lanjutan dari atas). . Ini adalah pemahaman saya sekarang. Tolong beri tahu saya jika saya kehilangan tanda. Saya tidak keberatan mengakui bahwa saya hanyalah manusia biasa di antara para raksasa di sini!
Matt Cashatt
@MatthewPatrickCashatt - DI tidak selalu menyiratkan masalah ketergantungan runtime, yang seperti yang Anda perhatikan, mengerikan.
Telastyn
7

Jawaban cepat untuk pertanyaan Anda adalah: Tidak .

Tetapi seperti yang telah dinyatakan oleh orang lain, pertanyaan itu menikahi dua, konsep yang agak tidak terkait.

Mari kita lakukan langkah demi langkah.

DI menghasilkan gaya non-fungsional

Pada intinya pemrograman fungsi adalah fungsi murni - fungsi yang memetakan input ke output, sehingga Anda selalu mendapatkan output yang sama untuk input yang diberikan.

DI biasanya berarti unit Anda tidak lagi murni karena output dapat bervariasi tergantung pada injeksi. Misalnya, dalam fungsi berikut:

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

getBookedSeatCount(fungsi) dapat bervariasi menghasilkan hasil yang berbeda untuk input yang diberikan sama. Ini membuat bookSeatsnajis juga.

Ada pengecualian untuk ini - Anda dapat menyuntikkan salah satu dari dua algoritma pengurutan yang menerapkan pemetaan input-output yang sama, meskipun menggunakan algoritma yang berbeda. Tapi ini pengecualian.

Suatu sistem tidak dapat murni

Fakta bahwa suatu sistem tidak dapat murni sama-sama diabaikan seperti yang dinyatakan dalam sumber pemrograman fungsional.

Suatu sistem harus memiliki efek samping dengan contoh-contoh nyata adalah:

  • UI
  • Basis data
  • API (dalam arsitektur client-server)

Jadi bagian dari sistem Anda harus melibatkan efek samping dan bagian itu mungkin juga melibatkan gaya imperatif, atau gaya OO.

Paradigma shell-core

Meminjam istilah-istilah dari perbincangan hebat Gary Bernhardt tentang batasan , arsitektur sistem (atau modul) yang baik akan mencakup dua lapisan ini:

  • Inti
    • Fungsi murni
    • Percabangan
    • Tidak ada ketergantungan
  • Kulit
    • Najis (efek samping)
    • Tidak bercabang
    • Ketergantungan
    • Mungkin penting, melibatkan gaya OO, dll.

Kuncinya adalah untuk 'membagi' sistem menjadi bagian murni (inti) dan bagian tidak murni (shell).

Meskipun menawarkan solusi yang sedikit cacat (dan kesimpulan), artikel Mark Seemann ini mengusulkan konsep yang sama. Implementasi Haskell sangat berwawasan karena ini menunjukkan bahwa semuanya dapat dilakukan dengan menggunakan FP.

DI dan FP

Mempekerjakan DI sangat masuk akal bahkan jika sebagian besar aplikasi Anda murni. Kuncinya adalah membatasi DI di dalam shell yang tidak murni.

Contohnya adalah bertopik API - Anda ingin API yang sebenarnya dalam produksi, tetapi gunakan bertopik dalam pengujian. Mengikuti model shell-core akan sangat membantu di sini.

Kesimpulan

Jadi FP dan DI bukan alternatif. Anda mungkin memiliki keduanya di sistem Anda, dan sarannya adalah untuk memastikan pemisahan antara bagian yang murni dan tidak murni dari sistem, di mana FP dan DI berada masing-masing.

Izhaki
sumber
Ketika Anda merujuk pada paradigma shell-core, bagaimana seseorang mencapai tidak bercabang di shell? Saya dapat memikirkan banyak contoh di mana aplikasi perlu melakukan satu hal yang tidak murni berdasarkan nilai. Apakah aturan tanpa percabangan ini berlaku dalam bahasa seperti Java?
jrahhali
@jrahhali Silakan lihat Bincang-bincang Gary Bernhardt untuk detailnya (ditautkan dalam jawabannya).
Izhaki
seri Seemann relavent lainnya blog.ploeh.dk/2017/01/27/…
jk.
1

Dari sudut pandang OOP, fungsi dapat dianggap sebagai antarmuka metode tunggal.

Antarmuka adalah kontrak yang lebih kuat daripada fungsi.

Jika Anda menggunakan pendekatan fungsional dan melakukan banyak DI maka dibandingkan dengan menggunakan pendekatan OOP Anda akan mendapatkan lebih banyak kandidat untuk setiap ketergantungan.

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.
Sarang
sumber
3
Setiap kelas dapat dibungkus untuk mengimplementasikan antarmuka sehingga "kontrak yang lebih kuat" tidak jauh lebih kuat. Lebih penting lagi memberikan fungsi masing-masing jenis yang berbeda membuat batas tidak mungkin untuk melakukan komposisi fungsi.
Doval
Pemrograman fungsional tidak berarti "Pemrograman dengan fungsi urutan yang lebih tinggi", itu mengacu pada konsep yang jauh lebih luas, fungsi urutan yang lebih tinggi hanyalah salah satu teknik yang berguna dalam paradigma.
Jimmy Hoffa