Bagaimana inversi ketergantungan terkait dengan fungsi tingkat tinggi?

41

Hari ini saya baru saja melihat artikel ini yang menggambarkan relevansi prinsip SOLID dalam pengembangan F #

F # dan prinsip-prinsip Desain - SOLID

Dan sementara membahas yang terakhir - "Prinsip inversi ketergantungan", penulis berkata:

Dari sudut pandang fungsional, wadah dan konsep injeksi ini dapat diselesaikan dengan fungsi urutan tinggi yang sederhana, atau pola tipe hole-in-the-middle yang dibangun langsung ke dalam bahasa.

Tapi dia tidak menjelaskan lebih lanjut. Jadi, pertanyaan saya adalah, bagaimana inversi dependensi terkait dengan fungsi tingkat tinggi?

Gulshan
sumber

Jawaban:

38

Dependency Inversion dalam OOP berarti Anda mengkodekan terhadap antarmuka yang kemudian disediakan oleh implementasi dalam suatu objek.

Bahasa yang mendukung fungsi bahasa yang lebih tinggi sering dapat memecahkan masalah inversi ketergantungan sederhana dengan meneruskan perilaku sebagai fungsi alih-alih objek yang mengimplementasikan antarmuka dalam OO-sense.

Dalam bahasa seperti itu, tanda tangan fungsi dapat menjadi antarmuka dan fungsi dilewatkan sebagai ganti objek tradisional untuk memberikan perilaku yang diinginkan. Lubang di pola tengah adalah contoh yang baik untuk ini.

Ini memungkinkan Anda mencapai hasil yang sama dengan kode lebih sedikit dan lebih ekspresif, karena Anda tidak perlu mengimplementasikan seluruh kelas yang sesuai dengan antarmuka (OOP) untuk memberikan perilaku yang diinginkan untuk penelepon. Sebagai gantinya, Anda hanya bisa melewatkan definisi fungsi sederhana. Singkatnya: Kode seringkali lebih mudah dipelihara, lebih ekspresif, dan lebih fleksibel ketika seseorang menggunakan fungsi urutan yang lebih tinggi.

Contoh dalam C #

Pendekatan tradisional:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

Dengan fungsi urutan yang lebih tinggi:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

Sekarang implementasi dan doa menjadi kurang rumit. Kami tidak perlu menyediakan implementasi IFilter lagi. Kami tidak perlu menerapkan kelas untuk filter lagi.

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

Tentu saja, ini sudah bisa dilakukan oleh LinQ di C #. Saya hanya menggunakan contoh ini untuk menggambarkan bahwa lebih mudah dan lebih fleksibel untuk menggunakan fungsi urutan yang lebih tinggi daripada objek yang mengimplementasikan antarmuka.

Elang
sumber
3
Contoh yang bagus. Namun, seperti Gulshan saya mencoba mencari tahu lebih banyak tentang pemrograman fungsional dan saya bertanya-tanya apakah "fungsional DI" semacam ini tidak mengorbankan kekakuan dan signifikansi dibandingkan dengan "DI berorientasi objek". Tanda tangan pesanan yang lebih tinggi hanya menyatakan bahwa fungsi yang diteruskan harus mengambil Pelanggan sebagai parameter dan mengembalikan bool sedangkan versi OO memberlakukan fakta bahwa objek yang dilewati adalah filter (mengimplementasikan IFilter <Customer>). Itu juga membuat gagasan tentang filter eksplisit, yang bisa menjadi hal yang baik jika itu adalah konsep inti dari Domain (lihat DDD). Apa yang kamu pikirkan ?
guillaume31
2
@ ian31: Ini memang topik yang menarik! Apa pun yang diteruskan ke FilterCustomer akan berperilaku sebagai semacam filter secara implisit. Ketika konsep filter adalah bagian penting dari domain dan Anda memerlukan aturan filter kompleks yang digunakan beberapa kali di seluruh sistem, lebih baik merangkumnya. Jika tidak atau hanya pada tingkat yang sangat rendah, maka saya akan bertujuan untuk kesederhanaan teknis dan pragmatisme.
Falcon
5
@ ian31: Saya sama sekali tidak setuju. Melaksanakan IFilter<Customer>tidak ada penegakan sama sekali. Fungsi tingkat tinggi jauh lebih fleksibel, yang merupakan manfaat besar, dan mampu menuliskannya sebaris adalah manfaat besar lainnya. Lambdas juga lebih mudah menangkap variabel lokal.
DeadMG
3
@ ian31: Fungsi ini juga dapat diverifikasi pada waktu kompilasi. Anda juga dapat menulis fungsi, menamainya, dan kemudian meneruskannya sebagai argumen selama memenuhi kontrak yang jelas (mengambil pelanggan, mengembalikan bool). Anda tidak perlu menyampaikan ekspresi lambda. Jadi Anda dapat menutupi kurangnya ekspresif pada tingkat tertentu. Namun, kontrak dan maksudnya tidak dinyatakan dengan jelas. Itu terkadang kerugian besar. Semua itu adalah masalah ekspresi, bahasa, dan enkapsulasi. Saya pikir Anda harus menilai setiap kasus dengan sendirinya.
Falcon
2
jika Anda merasa kuat tentang mengklarifikasi makna semantik dari fungsi disuntikkan, Anda dapat di C tanda tangan fungsi # nama menggunakan delegasi: public delegate bool CustomerFilter(Customer customer). dalam bahasa fungsional murni seperti haskell, jenis aliasing sepele:type customerFilter = Customer -> Bool
sara
8

Jika Anda ingin mengubah perilaku suatu fungsi

doThis(Foo)

Anda bisa melewati fungsi lain

doThisWith(Foo, anotherFunction)

yang mengimplementasikan perilaku yang Anda ingin berbeda.

"doThisWith" adalah fungsi tingkat tinggi karena mengambil fungsi lain sebagai argumen.

Misalnya Anda bisa

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
Program Lenny
sumber
5

Jawaban singkat:

Injeksi Ketergantungan Klasik / Inversi Kontrol menggunakan antarmuka kelas sebagai pengganti untuk fungsionalitas dependen. Antarmuka ini diimplementasikan oleh kelas.

Alih-alih Interface / ClassImplementation banyak dependensi dapat lebih mudah diimplementasikan dengan fungsi delegasi.

Anda menemukan contoh untuk keduanya di c # di ioc-factory-pro-dan-contras-untuk-antarmuka-versus-delegasi .

k3b
sumber
0

Bandingkan ini:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

dengan:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

Versi kedua adalah cara Java 8 untuk mengurangi kode boilerplate (perulangan dll) dengan menyediakan fungsi tingkat tinggi seperti filteryang memungkinkan Anda untuk melewati batas minimum (yaitu ketergantungan yang akan disuntikkan - ekspresi lambda).

Sridhar Sarnobat
sumber
0

Piggy-backing off contoh LennyProgrammers ...

Salah satu hal yang dilewatkan oleh contoh lain adalah Anda dapat menggunakan fungsi urutan lebih tinggi bersama dengan aplikasi fungsi parsial (PFA) untuk mengikat (atau "menyuntikkan") dependensi ke dalam suatu fungsi (melalui daftar argumennya) untuk membuat fungsi baru.

Jika bukannya:

doThisWith(Foo, anotherFunction)

kami (untuk menjadi konvensional dalam cara PFA biasanya dilakukan) memiliki fungsi pekerja tingkat rendah sebagai (swapping arg order):

doThisWith( anotherFunction, Foo )

Kami kemudian dapat menerapkan doThisWith sebagian seperti:

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

Yang memungkinkan kita nanti untuk menggunakan fungsi baru seperti:

doThis(Foo)

Atau bahkan:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

Lihat juga: https://ramdajs.com/docs/#partial

... dan, ya, adders / multipliers adalah contoh yang tidak imajinatif. Contoh yang lebih baik adalah fungsi yang mengambil pesan, dan mencatatnya atau mengirim surel tergantung pada apa yang dilewati oleh fungsi "konsumen" sebagai ketergantungan.

Memperluas gagasan ini, daftar argumen yang lebih panjang dapat semakin dipersempit ke fungsi-fungsi yang semakin khusus dengan daftar argumen yang lebih pendek dan lebih pendek, dan tentu saja salah satu dari fungsi-fungsi tersebut dapat diteruskan ke fungsi-fungsi lain sebagai dependensi untuk diterapkan sebagian.

OOP bagus jika Anda membutuhkan sekumpulan hal dengan beberapa operasi yang terkait erat, tetapi itu berubah menjadi pekerjaan untuk membuat sekelompok kelas masing-masing dengan metode "melakukannya" publik tunggal, ala "The Kingdom of Nouns".

Roboprog
sumber