Dengan cara ini saya menulis kode ini dapat diuji, tetapi apakah ada yang salah dengan itu saya hilang?

13

Saya memiliki antarmuka yang disebut IContext. Untuk keperluan ini, tidak masalah apa yang dilakukannya kecuali yang berikut:

T GetService<T>();

Apa yang dilakukan metode ini adalah melihat wadah DI aplikasi saat ini dan mencoba untuk menyelesaikan ketergantungan. Cukup standar menurut saya.

Dalam aplikasi ASP.NET MVC saya, konstruktor saya terlihat seperti ini.

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

Jadi daripada menambahkan beberapa parameter dalam konstruktor untuk setiap layanan (karena ini akan sangat mengganggu dan memakan waktu bagi pengembang untuk memperluas aplikasi) Saya menggunakan metode ini untuk mendapatkan layanan.

Sekarang, rasanya salah . Namun, cara saya saat ini membenarkannya di kepala saya adalah ini - saya bisa mengejeknya .

Saya bisa. Tidak akan sulit untuk mengejek IContextuntuk menguji Controller. Saya harus tetap:

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

Tapi seperti yang saya katakan, rasanya salah. Selamat datang di pikiran / penyalahgunaan.

LiverpoolsNumber9
sumber
8
Ini disebut Service Locator dan saya tidak menyukainya. Ada banyak tulisan tentang hal ini - lihat martinfowler.com/articles/injection.html dan blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern untuk pemula.
Benjamin Hodgson
Dari artikel Martin Fowler: "Saya sering mendengar keluhan bahwa jenis pelacak layanan ini adalah hal yang buruk karena mereka tidak dapat diuji karena Anda tidak dapat menggantikan implementasi untuk mereka. Tentu saja Anda dapat merancang mereka dengan buruk untuk masuk ke ini semacam masalah, tetapi Anda tidak harus melakukannya. Dalam hal ini instance pelacak layanan hanyalah pemegang data sederhana. Saya dapat dengan mudah membuat pelacak dengan implementasi uji layanan saya. " Bisakah Anda menjelaskan mengapa Anda tidak menyukainya? Mungkin dalam jawaban?
LiverpoolsNumber9
8
Dia benar, ini desain yang buruk. Sangat mudah: public SomeClass(Context c). Kode ini cukup jelas, bukan? Ini menyatakan, that SomeClasstergantung pada a Context. Err, tapi tunggu, tidak! Itu hanya bergantung pada ketergantungan Xyang didapat dari Konteks. Itu berarti, setiap kali Anda membuat perubahan, Contextitu bisa pecah SomeObject, meskipun Anda hanya berubah Contexts Y. Tapi ya, Anda tahu Anda hanya berubah , jadi Ytidak Xapa SomeClass-apa. Tetapi menulis kode yang baik bukan tentang apa yang Anda ketahui tetapi apa yang diketahui karyawan baru ketika dia melihat kode Anda pertama kali.
valenterri
@DocBrown Bagi saya itulah yang saya katakan - saya tidak melihat perbedaannya di sini. Bisakah Anda jelaskan lebih lanjut?
valenterri
1
@DocBrown Saya mengerti maksud Anda sekarang. Ya, jika Konteksnya hanya bundel dari semua dependensi, maka ini bukan desain yang buruk. Namun mungkin penamaan yang buruk, tetapi itu juga hanya asumsi. OP harus mengklarifikasi jika ada lebih banyak metode (objek dalam) dari konteks. Juga, membahas kode baik-baik saja, tetapi ini adalah programer.stackexchange jadi bagi saya kita juga harus mencoba melihat "di belakang" hal-hal untuk membuat OP membaik.
valenterri

Jawaban:

5

Memiliki satu alih-alih banyak parameter dalam konstruktor bukan bagian bermasalah dari desain ini . Selama IContextkelas Anda tidak lain adalah fasad layanan , khusus untuk menyediakan dependensi yang digunakan MyControllerBase, dan bukan pelacak layanan umum yang digunakan di seluruh kode Anda, bagian dari kode Anda adalah IMHO ok.

Contoh pertama Anda mungkin diubah menjadi

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

itu tidak akan menjadi perubahan desain yang substansial MyControllerBase. Jika desain ini baik atau buruk hanya bergantung pada kenyataan jika Anda mau

  • pastikan TheContext, SomeServicedan AnotherServiceselalu semua diinisialisasi dengan benda tiruan, atau semuanya dengan benda nyata
  • atau, untuk memungkinkan inisialisasi mereka dengan kombinasi yang berbeda dari 3 objek (yang berarti, untuk kasus ini Anda harus melewati parameter satu per satu)

Jadi hanya menggunakan satu parameter, bukan 3 dalam konstruktor bisa sepenuhnya masuk akal.

Hal yang bermasalah adalah IContextmengekspos GetServicemetode ini di depan umum. IMHO Anda harus menghindari ini, alih-alih jaga "metode pabrik" eksplisit. Jadi apakah boleh menerapkan GetSomeServicedan GetAnotherServicemetode dari contoh saya menggunakan pencari lokasi? IMHO itu tergantung. Selama IContextkelas tetap menggunakan pabrik abstrak sederhana untuk tujuan khusus menyediakan daftar objek layanan yang eksplisit, yang dapat diterima oleh IMHO. Pabrik abstrak biasanya hanya kode "lem", yang tidak harus diuji sendiri. Namun demikian, Anda harus bertanya pada diri sendiri apakah dalam konteks metode seperti GetSomeService, apakah Anda benar-benar membutuhkan pelacak layanan, atau apakah panggilan konstruktor eksplisit tidak akan lebih sederhana.

Jadi berhati-hatilah, ketika Anda tetap pada desain di mana IContextimplementasinya hanyalah pembungkus di sekitar metode umum, umum GetService, yang memungkinkan untuk menyelesaikan setiap dependensi sewenang-wenang oleh kelas arbitrer, maka semuanya berlaku apa yang ditulis @BenjaminHodgson dalam jawabannya.

Doc Brown
sumber
Saya setuju dengan ini. Masalah dengan contohnya adalah GetServicemetode generik . Refactoring ke metode yang secara eksplisit dinamai dan diketik lebih baik. Lebih baik lagi adalah menjabarkan ketergantungan IContextimplementasi secara eksplisit dalam konstruktor.
Benjamin Hodgson
1
@BenjaminHodgson: mungkin terkadang lebih baik, tetapi tidak selalu lebih baik. Anda perhatikan bau kode ketika daftar parameter ctor semakin lama. Lihat jawaban saya sebelumnya di sini: programmers.stackexchange.com/questions/190120/…
Doc Brown
@DocBrown "kode bau" dari over-injeksi konstruktor merupakan indikasi pelanggaran SRP, yang merupakan masalah nyata. Cukup dengan membungkus beberapa layanan ke dalam kelas Facade, hanya untuk mengekspos mereka sebagai properti, tidak melakukan apa pun untuk mengatasi sumber masalah. Jadi Facade seharusnya tidak menjadi pembungkus sederhana di sekitar komponen lain, tetapi idealnya harus menawarkan API yang disederhanakan untuk mengkonsumsinya (atau harus menyederhanakannya dengan cara lain) ...
AlexFoxGill
... Ingatlah bahwa "bau kode" seperti over-injeksi constructor bukanlah masalah itu sendiri. Ini hanya petunjuk bahwa ada masalah yang lebih dalam dalam kode yang biasanya dapat diselesaikan dengan refactoring dengan masuk akal begitu masalah telah diidentifikasi
AlexFoxGill
Di mana Anda saat Microsoft membuat ini IValidatableObject?
RubberDuck
15

Desain ini dikenal sebagai Service Locator * dan saya tidak menyukainya. Ada banyak argumen yang menentangnya:

Service Locator memasangkan Anda ke wadah Anda . Menggunakan injeksi dependensi reguler (di mana konstruktor menjabarkan dependensi secara eksplisit), Anda dapat langsung mengganti wadah Anda dengan yang berbeda, atau kembali ke new-ekspresi. Dengan Anda IContextitu tidak benar-benar mungkin.

Service Locator menyembunyikan ketergantungan . Sebagai klien, sangat sulit untuk mengatakan apa yang Anda butuhkan untuk membuat instance kelas. Anda perlu semacam IContext, tetapi Anda juga perlu mengatur konteks untuk mengembalikan objek yang benar untuk membuat MyControllerBasepekerjaan. Ini sama sekali tidak jelas dari tanda tangan konstruktor. Dengan DI biasa kompiler memberi tahu Anda apa yang Anda butuhkan. Jika kelas Anda memiliki banyak ketergantungan, Anda harus merasa sakit karena itu akan memacu Anda untuk refactor. Service Locator menyembunyikan masalah dengan desain yang buruk.

Service Locator menyebabkan kesalahan run-time . Jika Anda menelepon GetServicedengan parameter tipe buruk Anda akan mendapatkan pengecualian. Dengan kata lain, GetServicefungsi Anda bukan fungsi total. (Total fungsi adalah ide dari dunia FP, tetapi pada dasarnya berarti bahwa fungsi harus selalu mengembalikan nilai.) Lebih baik membiarkan kompiler membantu dan memberi tahu Anda ketika Anda memiliki dependensi yang salah.

Service Locator melanggar Prinsip Pergantian Liskov . Karena perilakunya bervariasi berdasarkan pada argumen tipe, Service Locator dapat dilihat seolah-olah secara efektif memiliki jumlah metode yang tak terbatas pada antarmuka! Argumen ini dijabarkan secara rinci di sini .

Pencari Layanan sulit untuk diuji . Anda telah memberikan contoh palsu IContextuntuk tes, yang baik-baik saja, tetapi tentunya lebih baik tidak harus menulis kode itu sejak awal. Suntikkan dependensi palsu Anda secara langsung, tanpa harus melalui pencari lokasi layanan Anda.

Singkatnya, jangan lakukan itu . Sepertinya ini adalah solusi yang menggoda untuk masalah kelas dengan banyak ketergantungan, tetapi dalam jangka panjang Anda hanya akan membuat hidup Anda sengsara.

* Saya mendefinisikan Service Locator sebagai objek dengan Resolve<T>metode generik yang mampu menyelesaikan dependensi sewenang-wenang dan digunakan di seluruh basis kode (bukan hanya di Root Komposisi). Ini tidak sama dengan Service Facade (objek yang menggabungkan beberapa set dependensi yang diketahui kecil) atau Abstract Factory (objek yang membuat instance dari satu tipe - tipe Pabrik Abstrak mungkin generik tetapi metodenya tidak) .

Benjamin Hodgson
sumber
1
Anda setuju tentang pola Penentu Lokasi Layanan (yang saya setujui). Tetapi sebenarnya, dalam contoh OP, MyControllerBasetidak digabungkan ke wadah DI tertentu, juga bukan "benar-benar" contoh untuk anti-pola Penentu Lokasi Layanan.
Doc Brown
@DocBrown saya setuju. Bukan karena itu membuat hidup saya lebih mudah, tetapi karena sebagian besar contoh yang diberikan di atas tidak relevan dengan kode saya.
LiverpoolsNumber9
2
Bagi saya, ciri khas anti-pola Layanan Locator adalah GetService<T>metode umum . Menyelesaikan ketergantungan yang sewenang-wenang adalah aroma yang sebenarnya, yang ada dan benar dalam contoh OP.
Benjamin Hodgson
1
Masalah lain dengan menggunakan Service Locator adalah ia mengurangi fleksibilitas: Anda hanya dapat memiliki satu implementasi dari setiap antarmuka layanan. Jika Anda membangun dua kelas yang mengandalkan IFrobnicator, tetapi kemudian memutuskan bahwa satu harus menggunakan implementasi DefaultFrobnicator asli Anda, tetapi yang lain benar-benar harus menggunakan dekorator CacheingFrobnicator di sekitarnya, Anda harus mengubah kode yang ada, sedangkan jika Anda menyuntikkan dependensi secara langsung yang harus Anda lakukan adalah mengubah kode pengaturan Anda (atau file konfigurasi jika Anda menggunakan kerangka kerja DI). Jadi ini merupakan pelanggaran OCP.
Jules
1
@DocBrown GetService<T>()Metode ini memungkinkan kelas yang sewenang-wenang untuk diminta: "Apa yang dilakukan metode ini adalah melihat wadah DI saat ini dari aplikasi dan mencoba untuk menyelesaikan ketergantungan. Standar yang adil, saya pikir." . Saya merespons komentar Anda di bagian atas jawaban ini. Ini adalah 100%
Pencari
5

Argumen terbaik terhadap anti-pola Layanan Locator secara jelas dinyatakan oleh Mark Seemann jadi saya tidak akan membahas terlalu banyak mengapa ini adalah ide yang buruk - ini adalah perjalanan belajar yang harus Anda luangkan waktu untuk mengerti sendiri (saya juga merekomendasikan buku Markus ).

OK jadi untuk menjawab pertanyaan - mari kita nyatakan kembali masalah Anda yang sebenarnya :

Jadi daripada menambahkan beberapa parameter dalam konstruktor untuk setiap layanan (karena ini akan sangat mengganggu dan memakan waktu bagi pengembang untuk memperluas aplikasi) Saya menggunakan metode ini untuk mendapatkan layanan.

Ada pertanyaan yang membahas masalah ini di StackOverflow . Seperti disebutkan dalam salah satu komentar di sana:

Komentar terbaik: "Salah satu manfaat luar biasa dari Injeksi Konstruktor adalah bahwa itu membuat pelanggaran Prinsip Tanggung Jawab Tunggal sangat jelas."

Anda mencari di tempat yang salah untuk solusi untuk masalah Anda. Penting untuk mengetahui kapan kelas melakukan terlalu banyak. Dalam kasus Anda, saya sangat curiga bahwa tidak perlu "Pengontrol Pangkalan". Bahkan, dalam OOP hampir selalu tidak ada kebutuhan untuk warisan sama sekali . Variasi dalam perilaku dan fungsionalitas bersama dapat dicapai sepenuhnya melalui penggunaan antarmuka yang tepat, yang biasanya menghasilkan kode yang difaktorkan dan dienkapsulasi lebih baik - dan tidak perlu meneruskan dependensi ke konstruktor superclass.

Di semua proyek yang saya kerjakan di mana ada Base Controller, itu dilakukan murni untuk keperluan berbagi properti dan metode yang mudah digunakan, seperti IsUserLoggedIn()dan GetCurrentUserId(). BERHENTI . Ini adalah penyalahgunaan warisan yang mengerikan. Alih-alih, buat komponen yang memaparkan metode ini dan gunakan dependensi di tempat yang Anda perlukan. Dengan cara ini, komponen Anda akan tetap dapat diuji dan ketergantungannya akan terlihat jelas.

Selain dari hal lain, ketika menggunakan pola MVC saya selalu merekomendasikan pengontrol kurus . Anda dapat membaca lebih lanjut tentang ini di sini tetapi esensi dari pola ini sederhana, bahwa pengontrol di MVC harus melakukan satu hal saja: menangani argumen yang dilewatkan oleh kerangka kerja MVC, mendelegasikan masalah lain ke beberapa komponen lainnya. Sekali lagi ini adalah Prinsip Tanggung Jawab Tunggal di tempat kerja.

Ini benar-benar akan membantu untuk mengetahui kasus penggunaan Anda untuk membuat penilaian yang lebih akurat, tetapi jujur ​​saya tidak bisa memikirkan skenario mana pun di mana kelas dasar lebih disukai daripada dependensi yang diperhitungkan dengan baik.

AlexFoxGill
sumber
+1 - ini adalah pandangan baru terhadap pertanyaan yang tidak pernah dijawab oleh jawaban lain
Benjamin Hodgson
1
"Salah satu manfaat luar biasa dari Injeksi Konstruktor adalah bahwa hal itu membuat pelanggaran terhadap Prinsip Tanggung Jawab Tunggal sangat jelas" Sukai itu. Jawaban yang sangat bagus. Jangan setuju dengan semua itu. Kasus penggunaan saya, cukup lucu, tidak harus menduplikasi kode dalam sistem yang akan memiliki lebih dari 100 pengendali (setidaknya). Namun ulang SRP - setiap layanan yang disuntikkan memiliki tanggung jawab tunggal, seperti halnya pengontrol yang menggunakan semuanya - bagaimana orang akan membiakkannya ??!
LiverpoolsNumber9
1
@ LiverpoolsNumber9 Kuncinya adalah memindahkan fungsionalitas dari BaseController ke dependensi, hingga Anda tidak memiliki apa pun lagi di BaseController kecuali protecteddelegasi satu-liner ke komponen baru. Kemudian Anda bisa kehilangan BaseController dan mengganti protectedmetode - metode itu dengan panggilan langsung ke dependensi - ini akan menyederhanakan model Anda dan menjadikan semua dependensi Anda eksplisit (yang merupakan hal yang sangat baik dalam proyek dengan 100-an pengontrol!)
AlexFoxGill
@ LiverpoolsNumber9 - jika Anda bisa menyalin sebagian BaseController Anda ke pastebin, saya bisa membuat beberapa saran nyata
AlexFoxGill
-2

Saya menambahkan jawaban untuk ini berdasarkan kontribusi orang lain. Terima kasih banyak. Pertama, inilah jawaban saya: "Tidak, tidak ada yang salah dengan itu".

Jawaban "Servis Fasad" Doc Brown Saya menerima jawaban ini karena apa yang saya cari (jika jawabannya "tidak") adalah beberapa contoh atau perluasan atas apa yang saya lakukan. Dia memberikan ini dengan menyarankan bahwa A) itu punya nama, dan B) mungkin ada cara yang lebih baik untuk melakukan ini.

Jawaban "Service Locator" Benjamin Hodgson Seperti saya menghargai pengetahuan yang saya peroleh di sini, apa yang saya miliki bukanlah "Service Locator". Ini adalah "Fasad Layanan". Segala sesuatu dalam jawaban ini benar tetapi tidak untuk keadaan saya.

Jawaban USR

Saya akan menangani ini secara lebih rinci:

Anda memberikan banyak informasi statis dengan cara ini. Anda menunda keputusan untuk runtime seperti halnya banyak bahasa dinamis. Dengan begitu Anda kehilangan verifikasi statis (keamanan), dokumentasi dan dukungan perkakas (pelengkapan otomatis, refactoring, temukan penggunaan, aliran data).

Saya tidak kehilangan perkakas apa pun dan saya tidak kehilangan ketikan "statis". Bagian depan layanan akan mengembalikan apa yang telah saya konfigurasikan dalam wadah DI saya, atau default(T). Dan apa yang dikembalikan adalah "diketik". Refleksi dienkapsulasi.

Saya tidak melihat mengapa menambahkan layanan tambahan sebagai argumen konstruktor adalah beban besar.

Ini tentu tidak "langka". Karena saya menggunakan pengontrol dasar , setiap kali saya perlu mengubah konstruktornya, saya mungkin harus mengubah 10, 100, 1000 pengontrol lainnya.

Jika Anda menggunakan kerangka kerja injeksi dependensi, Anda bahkan tidak perlu meneruskan nilai parameter secara manual. Sekali lagi Anda kehilangan beberapa keuntungan statis tetapi tidak banyak.

Saya menggunakan injeksi ketergantungan. Itulah intinya.

Dan akhirnya, komentar Jules tentang jawaban Benjamin saya tidak kehilangan fleksibilitas. Ini bagian depan layanan saya . Saya dapat menambahkan sebanyak mungkin parameter yang GetService<T>ingin saya bedakan antara implementasi yang berbeda, seperti yang akan dilakukan ketika mengkonfigurasi wadah DI. Jadi misalnya, saya bisa berubah GetService<T>()untuk GetService<T>(string extraInfo = null)mengatasi "masalah potensial" ini.


Bagaimanapun, terima kasih sekali lagi untuk semua orang. Sudah sangat berguna. Bersulang.

LiverpoolsNumber9
sumber
4
Saya tidak setuju bahwa Anda memiliki Fasad Layanan di sini. The GetService<T>()Metode mampu (upaya untuk) tekad sewenang-wenang dependensi . Ini menjadikannya Pelok Layanan, bukan Fasad Layanan, seperti yang saya jelaskan di catatan kaki jawaban saya. Jika Anda menggantinya dengan set GetServiceX()/ GetServiceY()metode kecil, seperti yang disarankan oleh @DocBrown, maka itu akan menjadi Facade.
Benjamin Hodgson
2
Anda harus membaca, dan memperhatikan, bagian terakhir dari artikel ini tentang Pelacak Layanan Abstrak , yang pada dasarnya adalah apa yang Anda lakukan. Saya telah melihat anti-pola ini merusak seluruh proyek - perhatikan dengan cermat kutipan "itu akan membuat hidup Anda sebagai pengembang pemeliharaan lebih buruk karena Anda akan perlu menggunakan kekuatan otak dalam jumlah yang cukup besar untuk memahami implikasi dari setiap perubahan yang Anda buat "
AlexFoxGill
3
Pada pengetikan statis - Anda tidak mengerti intinya. Jika saya mencoba menulis kode yang tidak menyediakan kelas menggunakan DI dengan parameter konstruktor yang diperlukan, itu tidak akan dikompilasi. Itu statis, keamanan waktu kompilasi terhadap masalah. Jika Anda menulis kode, memberikan konstruktor dengan IContext yang belum dikonfigurasikan dengan benar untuk memberikan argumen yang sebenarnya dibutuhkan, maka itu akan dikompilasi dan gagal pada saat dijalankan
Ben Aaronson
3
@ LiverpoolsNumber9 Kalau begitu mengapa memiliki bahasa yang sangat diketik sama sekali? Kesalahan kompiler adalah garis pertahanan pertama terhadap bug. Tes unit adalah yang kedua. Hormat Anda tidak akan diambil oleh unit test karena ini merupakan interaksi antara kelas dan ketergantungannya, sehingga Anda akan masuk ke garis pertahanan ketiga: tes integrasi. Saya tidak tahu seberapa sering Anda menjalankannya, tetapi Anda sekarang berbicara tentang umpan balik dalam urutan menit atau lebih, daripada milidetik yang diperlukan IDE Anda untuk menggarisbawahi kesalahan kompiler.
Ben Aaronson
2
@ LiverpoolsNumber9 salah: tes Anda sekarang harus mengisi IContextdan menyuntikkan itu, dan yang terpenting: jika Anda menambahkan dependensi baru, kode masih akan dikompilasi - meskipun tes akan gagal saat runtime
AlexFoxGill