Konteks: Saya menggunakan C #
Saya merancang sebuah kelas, dan untuk mengisolasinya, dan membuat pengujian unit lebih mudah, saya memberikan semua dependensinya; tidak ada instantiasi objek secara internal. Namun, alih-alih referensi antarmuka untuk mendapatkan data yang dibutuhkan, saya memilikinya referensi tujuan umum. Fungsi mengembalikan data / perilaku yang diperlukan. Ketika saya menyuntikkan dependensinya, saya bisa melakukannya dengan ekspresi lambda.
Bagi saya, ini sepertinya pendekatan yang lebih baik karena saya tidak perlu melakukan ejekan yang membosankan selama pengujian unit. Juga, jika implementasi di sekitarnya memiliki perubahan mendasar, saya hanya perlu mengubah kelas pabrik; tidak diperlukan perubahan di kelas yang berisi logika.
Namun, saya belum pernah melihat IoC melakukan hal ini sebelumnya, yang membuat saya berpikir ada beberapa jebakan potensial yang mungkin saya lewatkan. Satu-satunya yang dapat saya pikirkan adalah ketidakcocokan kecil dengan versi C # sebelumnya yang tidak mendefinisikan Func, dan ini bukan masalah dalam kasus saya.
Apakah ada masalah dengan menggunakan delegasi umum / fungsi urutan yang lebih tinggi seperti Func for IoC, yang bertentangan dengan antarmuka yang lebih spesifik?
sumber
Func
karena Anda dapat memberi nama parameter, tempat Anda dapat menyatakan maksudnya.Jawaban:
Jika suatu antarmuka memang hanya berisi satu fungsi, tidak lebih, dan tidak ada alasan kuat untuk memperkenalkan dua nama (nama antarmuka dan nama fungsi di dalam antarmuka),
Func
sebaliknya , menggunakan kode boilerplate yang tidak perlu dan dalam banyak kasus lebih disukai - sama seperti ketika Anda mulai merancang DTO dan mengenalinya hanya membutuhkan satu atribut anggota.Saya kira banyak orang lebih terbiasa menggunakan
interfaces
, karena pada saat injeksi dependensi dan IoC semakin populer, tidak ada yang setara denganFunc
kelas di Java atau C ++ (saya bahkan tidak yakin apakahFunc
tersedia pada saat itu di C # ). Jadi banyak tutorial, contoh atau buku teks masih lebih sukainterface
bentuknya, bahkan jika menggunakanFunc
akan lebih elegan.Anda mungkin melihat jawaban saya sebelumnya tentang prinsip pemisahan antarmuka dan pendekatan Flow Design Ralf Westphal. Paradigma ini mengimplementasikan DI dengan
Func
parameter secara eksklusif , untuk alasan yang persis sama seperti yang Anda sebutkan sendiri (dan beberapa lagi). Jadi seperti yang Anda lihat, ide Anda bukan yang benar-benar baru, justru sebaliknya.Dan ya, saya menggunakan pendekatan ini sendiri, untuk kode produksi, untuk program yang diperlukan untuk memproses data dalam bentuk pipa dengan beberapa langkah menengah, termasuk pengujian unit untuk setiap langkah. Maka dari itu saya bisa memberi Anda pengalaman langsung bahwa itu bisa bekerja dengan sangat baik.
sumber
Salah satu keuntungan utama yang saya temukan dengan IoC adalah ia akan memungkinkan saya untuk memberi nama semua dependensi saya melalui penamaan antarmuka mereka, dan wadah akan tahu mana yang akan disuplai ke konstruktor dengan mencocokkan nama jenis. Ini nyaman dan memungkinkan lebih banyak nama deskriptif dependensi daripada
Func<string, string>
.Saya juga sering menemukan bahwa bahkan dengan ketergantungan tunggal, sederhana, kadang-kadang perlu memiliki lebih dari satu fungsi - antarmuka memungkinkan Anda untuk mengelompokkan fungsi-fungsi ini bersama-sama dengan cara mendokumentasikan diri sendiri, vs memiliki beberapa parameter yang semuanya dibaca seperti
Func<string, string>
,Func<string, int>
.Pasti ada saat-saat ketika berguna untuk hanya memiliki delegasi yang disahkan sebagai ketergantungan. Ini adalah panggilan penilaian ketika Anda menggunakan delegasi vs memiliki antarmuka dengan anggota yang sangat sedikit. Kecuali benar-benar jelas apa tujuan dari argumen tersebut, saya biasanya akan melakukan kesalahan dalam pembuatan kode yang mendokumentasikan diri; yaitu. menulis sebuah antarmuka.
sumber
Tidak juga. Func adalah jenis antarmuka sendiri (dalam bahasa Inggris, bukan C # artinya). "Parameter ini adalah sesuatu yang memasok X ketika ditanya." Fungsi bahkan memiliki manfaat memasok informasi secara malas sesuai kebutuhan. Saya melakukan ini sedikit dan merekomendasikannya dalam jumlah sedang.
Adapun kerugian:
T
dan beberapa halFunc<T>
.sumber
Mari kita ambil contoh sederhana - mungkin Anda menyuntikkan alat logging.
Menyuntikkan kelas
Saya pikir itu cukup jelas apa yang sedang terjadi. Terlebih lagi, jika Anda menggunakan wadah IoC, Anda bahkan tidak perlu menyuntikkan sesuatu secara eksplisit, Anda cukup menambahkan ke root komposisi Anda:
Saat debugging
Worker
, pengembang hanya perlu berkonsultasi dengan root komposisi untuk menentukan kelas konkret yang digunakan.Jika pengembang membutuhkan logika yang lebih rumit, ia memiliki seluruh antarmuka untuk bekerja dengan:
Menyuntikkan suatu metode
Pertama, perhatikan bahwa parameter konstruktor memiliki nama yang lebih panjang sekarang
methodThatLogs
,. Ini perlu karena Anda tidak tahu apa yangAction<string>
seharusnya dilakukan. Dengan antarmuka, itu benar-benar jelas, tapi di sini kita harus mengandalkan penamaan parameter. Ini tampaknya secara inheren kurang dapat diandalkan dan lebih sulit untuk ditegakkan selama membangun.Sekarang, bagaimana kita menyuntikkan metode ini? Nah, wadah IoC tidak akan melakukannya untuk Anda. Jadi, Anda menyuntikkannya secara eksplisit saat Anda membuat instantiate
Worker
. Ini menimbulkan beberapa masalah:Worker
Worker
akan merasa lebih sulit untuk mencari tahu apa contoh konkret dipanggil. Mereka tidak bisa hanya berkonsultasi dengan komposisi root; mereka harus menelusuri kode.Bagaimana kalau kita membutuhkan logika yang lebih rumit? Teknik Anda hanya memaparkan satu metode. Sekarang saya kira Anda bisa memanggang hal-hal rumit ke dalam lambda:
tetapi ketika Anda menulis unit test Anda, bagaimana Anda menguji ekspresi lambda itu? Ini anonim, jadi kerangka kerja unit test Anda tidak dapat membuat instantiate secara langsung. Mungkin Anda bisa mencari cara cerdas untuk melakukannya, tetapi itu mungkin akan menjadi PITA lebih besar daripada menggunakan antarmuka.
Ringkasan perbedaan:
Jika Anda baik-baik saja dengan semua hal di atas, maka tidak apa-apa untuk menyuntikkan metode saja. Kalau tidak, saya sarankan Anda tetap dengan tradisi dan menyuntikkan antarmuka.
sumber
Worker
ketergantungan satu demi satu, yang memungkinkan setiap lambda disuntikkan secara independen sampai semua dependensi dipenuhi. Ini mirip dengan aplikasi parsial dalam FP. Selain itu, menggunakan lambdas membantu menghilangkan status implisit dan sambungan tersembunyi antara dependensi.Pertimbangkan kode berikut yang saya tulis sejak lama:
Dibutuhkan lokasi relatif ke file templat, memuatnya ke dalam memori, merender isi pesan, dan merakit objek email.
Anda mungkin melihat
IPhysicalPathMapper
dan berpikir, "Hanya ada satu fungsi. Itu bisa menjadiFunc
." Tetapi sebenarnya, masalah di sini adalah bahwaIPhysicalPathMapper
seharusnya tidak ada. Solusi yang jauh lebih baik adalah dengan hanya parameterisasi jalan :Ini menimbulkan sejumlah pertanyaan lain untuk meningkatkan kode ini. Misalnya, mungkin seharusnya hanya menerima
EmailTemplate
, dan kemudian mungkin harus menerima templat yang telah dirender, dan kemudian mungkin seharusnya hanya di-lined.Inilah sebabnya saya tidak suka inversi kontrol sebagai pola meresap. Ini umumnya dianggap sebagai solusi seperti dewa untuk menyusun semua kode Anda. Namun pada kenyataannya, jika Anda menggunakannya secara luas (tidak seperti hemat), itu membuat kode Anda jauh lebih buruk dengan mendorong Anda untuk memperkenalkan banyak antarmuka yang tidak perlu yang digunakan sepenuhnya mundur. (Mundur dalam arti bahwa penelepon harus benar-benar bertanggung jawab untuk mengevaluasi dependensi-dependensi itu dan meneruskan hasilnya daripada kelas itu sendiri yang memanggil panggilan itu.)
Antarmuka harus digunakan hemat, dan inversi kontrol dan injeksi ketergantungan juga harus digunakan hemat. Jika Anda memiliki jumlah yang besar, kode Anda menjadi lebih sulit untuk diuraikan.
sumber