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 IContext
untuk 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.
sumber
public SomeClass(Context c)
. Kode ini cukup jelas, bukan? Ini menyatakan,that SomeClass
tergantung pada aContext
. Err, tapi tunggu, tidak! Itu hanya bergantung pada ketergantunganX
yang didapat dari Konteks. Itu berarti, setiap kali Anda membuat perubahan,Context
itu bisa pecahSomeObject
, meskipun Anda hanya berubahContext
sY
. Tapi ya, Anda tahu Anda hanya berubah , jadiY
tidakX
apaSomeClass
-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.Jawaban:
Memiliki satu alih-alih banyak parameter dalam konstruktor bukan bagian bermasalah dari desain ini . Selama
IContext
kelas Anda tidak lain adalah fasad layanan , khusus untuk menyediakan dependensi yang digunakanMyControllerBase
, dan bukan pelacak layanan umum yang digunakan di seluruh kode Anda, bagian dari kode Anda adalah IMHO ok.Contoh pertama Anda mungkin diubah menjadi
itu tidak akan menjadi perubahan desain yang substansial
MyControllerBase
. Jika desain ini baik atau buruk hanya bergantung pada kenyataan jika Anda mauTheContext
,SomeService
danAnotherService
selalu semua diinisialisasi dengan benda tiruan, atau semuanya dengan benda nyataJadi hanya menggunakan satu parameter, bukan 3 dalam konstruktor bisa sepenuhnya masuk akal.
Hal yang bermasalah adalah
IContext
mengeksposGetService
metode ini di depan umum. IMHO Anda harus menghindari ini, alih-alih jaga "metode pabrik" eksplisit. Jadi apakah boleh menerapkanGetSomeService
danGetAnotherService
metode dari contoh saya menggunakan pencari lokasi? IMHO itu tergantung. SelamaIContext
kelas 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 sepertiGetSomeService
, 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
IContext
implementasinya hanyalah pembungkus di sekitar metode umum, umumGetService
, yang memungkinkan untuk menyelesaikan setiap dependensi sewenang-wenang oleh kelas arbitrer, maka semuanya berlaku apa yang ditulis @BenjaminHodgson dalam jawabannya.sumber
GetService
metode generik . Refactoring ke metode yang secara eksplisit dinamai dan diketik lebih baik. Lebih baik lagi adalah menjabarkan ketergantunganIContext
implementasi secara eksplisit dalam konstruktor.IValidatableObject
?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 AndaIContext
itu 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 membuatMyControllerBase
pekerjaan. 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
GetService
dengan parameter tipe buruk Anda akan mendapatkan pengecualian. Dengan kata lain,GetService
fungsi 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
IContext
untuk 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) .sumber
MyControllerBase
tidak digabungkan ke wadah DI tertentu, juga bukan "benar-benar" contoh untuk anti-pola Penentu Lokasi Layanan.GetService<T>
metode umum . Menyelesaikan ketergantungan yang sewenang-wenang adalah aroma yang sebenarnya, yang ada dan benar dalam contoh OP.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%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 :
Ada pertanyaan yang membahas masalah ini di StackOverflow . Seperti disebutkan dalam salah satu komentar di sana:
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()
danGetCurrentUserId()
. 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.
sumber
protected
delegasi satu-liner ke komponen baru. Kemudian Anda bisa kehilangan BaseController dan menggantiprotected
metode - 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!)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:
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.Ini tentu tidak "langka". Karena saya menggunakan pengontrol dasar , setiap kali saya perlu mengubah konstruktornya, saya mungkin harus mengubah 10, 100, 1000 pengontrol lainnya.
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 berubahGetService<T>()
untukGetService<T>(string extraInfo = null)
mengatasi "masalah potensial" ini.Bagaimanapun, terima kasih sekali lagi untuk semua orang. Sudah sangat berguna. Bersulang.
sumber
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 setGetServiceX()
/GetServiceY()
metode kecil, seperti yang disarankan oleh @DocBrown, maka itu akan menjadi Facade.IContext
dan menyuntikkan itu, dan yang terpenting: jika Anda menambahkan dependensi baru, kode masih akan dikompilasi - meskipun tes akan gagal saat runtime