Menggunakan Func, bukan antarmuka untuk IoC

15

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?

TheCatWhisperer
sumber
2
Apa yang Anda gambarkan adalah fungsi tingkat tinggi, aspek penting dari pemrograman fungsional.
Robert Harvey
2
Saya akan menggunakan delegasi alih-alih Funckarena Anda dapat memberi nama parameter, tempat Anda dapat menyatakan maksudnya.
Stefan Hanke
1
terkait: stackoverflow: ioc-factory-pro-and-contras-for-interface-versus-delegate . @TheCatWhisperer pertanyaan ini lebih umum sedangkan pertanyaan stackoverflow menyempit ke kasus khusus "pabrik"
k3b

Jawaban:

11

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), Funcsebaliknya , 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 dengan Funckelas di Java atau C ++ (saya bahkan tidak yakin apakah Functersedia pada saat itu di C # ). Jadi banyak tutorial, contoh atau buku teks masih lebih suka interfacebentuknya, bahkan jika menggunakan Funcakan lebih elegan.

Anda mungkin melihat jawaban saya sebelumnya tentang prinsip pemisahan antarmuka dan pendekatan Flow Design Ralf Westphal. Paradigma ini mengimplementasikan DI dengan Funcparameter 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.

Doc Brown
sumber
Program ini bahkan tidak dirancang dengan prinsip pemisahan antarmuka dalam pikiran, mereka pada dasarnya hanya digunakan sebagai kelas abstrak. Ini adalah alasan lebih lanjut mengapa saya menggunakan pendekatan ini, antarmuka tidak dipikirkan dengan baik dan sebagian besar tidak berguna karena hanya satu kelas yang mengimplementasikannya. Prinsip pemisahan antarmuka tidak harus dibawa ke ekstrem, terutama sekarang kita memiliki Func, tetapi dalam pikiran saya, itu masih sangat penting.
TheCatWhisperer
1
Program ini bahkan tidak dirancang dengan prinsip pemisahan antarmuka dalam pikiran - yah, menurut uraian Anda, Anda mungkin tidak menyadari bahwa istilah itu dapat diterapkan pada jenis desain Anda.
Doc Brown
Dok, saya merujuk pada kode yang dibuat oleh pendahulu saya, yang saya refactoring, dan yang darinya kelas baru saya agak bergantung. Bagian dari alasan saya menggunakan pendekatan ini adalah karena saya berencana membuat perubahan besar pada bagian lain dari program ini di masa depan, termasuk dependensi kelas ini
TheCatWhisperer
1
"... Pada saat injeksi ketergantungan dan IoC mulai populer, tidak ada yang nyata setara dengan kelas Func" Doc itulah yang hampir saya jawab tetapi tidak benar-benar merasa cukup percaya diri! Terima kasih telah memverifikasi kecurigaan saya pada yang itu.
Graham
9

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.

Erik
sumber
Saya harus men-debug kode seseorang, ketika pelaksana asli tidak ada lagi di sana, dan saya dapat memberi tahu Anda dari pengalaman pertama, bahwa mengetahui lambda mana yang dijalankan di mana tumpukan sangat banyak pekerjaan dan proses yang benar-benar membuat frustrasi. Seandainya implementator asli menggunakan antarmuka, akan sangat mudah untuk menemukan bug yang saya cari.
cwap
cwap, aku merasakan sakitmu. Namun, dalam implementasi khusus saya, semua lambda adalah satu garis yang menunjuk ke satu fungsi. Mereka juga dihasilkan dalam satu tempat.
TheCatWhisperer
Dalam pengalaman saya, ini hanya masalah perkakas. ReSharpers Inspect-> Incoming Calls melakukan pekerjaan yang baik dalam mengikuti jalan di atas funcs / lambdas.
Christian
3

Apakah ada masalah dengan menggunakan delegasi umum / fungsi urutan yang lebih tinggi seperti Func for IoC, yang bertentangan dengan antarmuka yang lebih spesifik?

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:

  • Kontainer IoC sering melakukan sihir untuk menyambungkan dependensi dengan cara berjenjang, dan mungkin tidak akan bermain bagus ketika ada beberapa hal Tdan beberapa hal Func<T>.
  • Fungsi memiliki beberapa tipuan, sehingga bisa sedikit lebih sulit untuk dipikirkan dan didebug.
  • Berfungsi menunda instantiation, yang berarti kesalahan runtime dapat muncul pada waktu yang aneh atau tidak sama sekali selama pengujian. Itu juga dapat meningkatkan kemungkinan urutan masalah operasi dan tergantung pada penggunaan Anda, kebuntuan dalam pemesanan inisialisasi.
  • Apa yang Anda lewati ke dalam Func kemungkinan merupakan penutupan, dengan sedikit overhead dan komplikasi yang ditimbulkannya.
  • Memanggil Fungsi sedikit lebih lambat daripada mengakses objek secara langsung. (Tidak cukup bahwa Anda akan melihat dalam program non-sepele, tetapi ada di sana)
Telastyn
sumber
1
Saya menggunakan Kontainer IoC untuk membuat pabrik dengan cara tradisional, kemudian pabrik mengemas metode antarmuka menjadi lambdas, menyelesaikan masalah yang Anda sebutkan di poin pertama Anda. Poin bagus.
TheCatWhisperer
1

Mari kita ambil contoh sederhana - mungkin Anda menyuntikkan alat logging.

Menyuntikkan kelas

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

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:

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

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:

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

Menyuntikkan suatu metode

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

Pertama, perhatikan bahwa parameter konstruktor memiliki nama yang lebih panjang sekarang methodThatLogs,. Ini perlu karena Anda tidak tahu apa yang Action<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:

  1. Lebih banyak pekerjaan untuk instantiate a Worker
  2. Pengembang yang mencoba men-debug Workerakan 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:

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

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:

  1. Menyuntikkan hanya metode membuat lebih sulit untuk menyimpulkan tujuan, sedangkan antarmuka dengan jelas mengomunikasikan tujuan.
  2. Menyuntikkan hanya metode memperlihatkan fungsi yang kurang ke kelas yang menerima injeksi. Bahkan jika Anda tidak membutuhkannya hari ini, Anda mungkin membutuhkannya besok.
  3. Anda tidak dapat secara otomatis menyuntikkan hanya metode menggunakan wadah IoC.
  4. Anda tidak dapat mengetahui dari akar komposisi kelas beton mana yang bekerja dalam contoh tertentu.
  5. Merupakan masalah untuk menguji unit ekspresi lambda itu sendiri.

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.

John Wu
sumber
Saya seharusnya menentukan, kelas yang saya buat adalah kelas logika proses. Hal ini bergantung pada kelas eksternal untuk mendapatkan data yang dibutuhkan untuk membuat keputusan berdasarkan informasi, efek samping yang menyebabkan kelas seperti pembalak tidak digunakan. Masalah dengan contoh Anda adalah bahwa itu adalah OO yang buruk, terutama dalam konteks menggunakan wadah IoC. Alih-alih menggunakan pernyataan if, yang menambahkan kerumitan tambahan, ke kelas, itu hanya harus dilewatkan logger inert. Juga, memperkenalkan logika dalam lambda memang akan membuat pengujian lebih sulit ... mengalahkan tujuan penggunaannya, itulah mengapa itu tidak dilakukan.
TheCatWhisperer
Titik lambdas ke metode antarmuka dalam aplikasi, dan membangun struktur data sewenang-wenang dalam unit test.
TheCatWhisperer
Mengapa orang selalu fokus pada hal-hal kecil yang jelas merupakan contoh yang sewenang-wenang? Nah jika Anda bersikeras berbicara tentang logging yang tepat, logger inert akan menjadi ide yang buruk dalam beberapa kasus, misalnya ketika string yang akan dicatat mahal untuk dihitung.
John Wu
Saya tidak yakin apa yang Anda maksud dengan "titik lambdas ke metode Antarmuka." Saya pikir Anda harus menyuntikkan implementasi metode yang cocok dengan tanda tangan delegasi. Jika metode kebetulan milik antarmuka, itu insidental; tidak ada pemeriksaan kompilasi atau run-time untuk memastikannya. Apakah saya salah paham? Mungkin Anda bisa memasukkan contoh kode dalam posting Anda?
John Wu
Saya tidak bisa mengatakan saya setuju dengan kesimpulan Anda di sini. Untuk satu hal, Anda harus menyatukan kelas Anda dengan jumlah minimal dependensi, jadi menggunakan lambdas memastikan bahwa setiap item yang diinjeksi persis satu ketergantungan dan bukan penggabungan beberapa hal dalam sebuah antarmuka. Anda juga dapat mendukung pola membangun Workerketergantungan 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.
Aaron M. Eshbach
0

Pertimbangkan kode berikut yang saya tulis sejak lama:

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Dibutuhkan lokasi relatif ke file templat, memuatnya ke dalam memori, merender isi pesan, dan merakit objek email.

Anda mungkin melihat IPhysicalPathMapperdan berpikir, "Hanya ada satu fungsi. Itu bisa menjadi Func." Tetapi sebenarnya, masalah di sini adalah bahwa IPhysicalPathMapperseharusnya tidak ada. Solusi yang jauh lebih baik adalah dengan hanya parameterisasi jalan :

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

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.

jpmc26
sumber