Penafian: Karena belum ada jawaban yang bagus, saya memutuskan untuk memposting bagian dari posting blog hebat yang saya baca beberapa waktu yang lalu, disalin hampir kata demi kata. Anda dapat menemukan postingan blog lengkapnya di sini . Jadi begini:
Kita dapat mendefinisikan dua antarmuka berikut:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
The IQuery<TResult>Menentukan pesan yang mendefinisikan query tertentu dengan data itu kembali menggunakan TResultjenis generik. Dengan antarmuka yang ditentukan sebelumnya, kita dapat mendefinisikan pesan kueri seperti ini:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Kelas ini mendefinisikan operasi kueri dengan dua parameter, yang akan menghasilkan larik Userobjek. Kelas yang menangani pesan ini dapat didefinisikan sebagai berikut:
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
private readonly NorthwindUnitOfWork db;
public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
}
}
Sekarang kita dapat membiarkan konsumen bergantung pada IQueryHandlerantarmuka generik :
public class UserController : Controller
{
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;
public UserController(
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
{
this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.findUsersBySearchTextHandler.Handle(query);
return View(users);
}
}
Model ini segera memberi kami banyak fleksibilitas, karena sekarang kami dapat memutuskan apa yang akan dimasukkan ke dalam UserController. Kami dapat memasukkan implementasi yang sama sekali berbeda, atau implementasi yang membungkus implementasi nyata, tanpa harus melakukan perubahan pada UserController(dan semua konsumen lain dari antarmuka tersebut).
The IQuery<TResult>antarmuka memberi kita waktu kompilasi dukungan ketika menentukan atau suntik IQueryHandlersdalam kode kita. Ketika kita mengubah FindUsersBySearchTextQuerykembali UserInfo[]bukan (dengan menerapkan IQuery<UserInfo[]>), yang UserControllerakan gagal dikompilasi, karena jenis kendala generik pada IQueryHandler<TQuery, TResult>tidak akan mampu memetakan FindUsersBySearchTextQueryke User[].
Menyuntikkan IQueryHandlerantarmuka ke konsumen Namun, memiliki beberapa masalah kurang jelas yang masih perlu dibenahi. Jumlah ketergantungan konsumen kita mungkin menjadi terlalu besar dan dapat menyebabkan konstruktor kelebihan injeksi - ketika konstruktor mengambil terlalu banyak argumen. Jumlah kueri yang dijalankan kelas bisa sering berubah, yang akan membutuhkan perubahan konstan ke dalam jumlah argumen konstruktor.
Kami dapat memperbaiki masalah karena harus menyuntikkan terlalu banyak IQueryHandlersdengan lapisan abstraksi ekstra. Kami membuat mediator yang berada di antara konsumen dan penangan kueri:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Ini IQueryProcessoradalah antarmuka non-generik dengan satu metode umum. Seperti yang Anda lihat dalam definisi antarmuka, IQueryProcessortergantung pada IQuery<TResult>antarmuka. Hal ini memungkinkan kami untuk memiliki dukungan waktu kompilasi pada konsumen kami yang bergantung pada IQueryProcessor. Mari tulis ulang UserControlleruntuk menggunakan yang baru IQueryProcessor:
public class UserController : Controller
{
private IQueryProcessor queryProcessor;
public UserController(IQueryProcessor queryProcessor)
{
this.queryProcessor = queryProcessor;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
The UserControllersekarang tergantung pada IQueryProcessoryang dapat menangani semua pertanyaan kita. The UserController's SearchUsersmetode memanggil IQueryProcessor.Processmetode yang lewat di sebuah objek query diinisialisasi. Karena FindUsersBySearchTextQuerymengimplementasikan IQuery<User[]>antarmuka, kita dapat meneruskannya ke Execute<TResult>(IQuery<TResult> query)metode umum . Berkat inferensi tipe C #, kompilator dapat menentukan tipe generik dan ini membuat kita tidak perlu menyatakan tipe secara eksplisit. Jenis kembalian dari Processmetode ini juga dikenal.
Sekarang menjadi tanggung jawab implementasi IQueryProcessoruntuk menemukan hak IQueryHandler. Ini memerlukan beberapa pengetikan dinamis, dan secara opsional menggunakan kerangka kerja Dependency Injection, dan semuanya dapat dilakukan hanya dengan beberapa baris kode:
sealed class QueryProcessor : IQueryProcessor
{
private readonly Container container;
public QueryProcessor(Container container)
{
this.container = container;
}
[DebuggerStepThrough]
public TResult Process<TResult>(IQuery<TResult> query)
{
var handlerType = typeof(IQueryHandler<,>)
.MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
The QueryProcessorkelas membangun sebuah tertentu IQueryHandler<TQuery, TResult>jenis berdasarkan pada jenis contoh permintaan disediakan. Tipe ini digunakan untuk meminta kelas kontainer yang disediakan untuk mendapatkan sebuah instance dari tipe itu. Sayangnya kita perlu memanggil Handlemetode menggunakan refleksi (dengan menggunakan kata kunci dymamic C # 4.0 dalam kasus ini), karena pada titik ini tidak mungkin untuk mentransmisikan contoh handler, karena TQueryargumen generik tidak tersedia pada waktu kompilasi. Namun, kecuali Handlemetode tersebut diubah namanya atau mendapat argumen lain, panggilan ini tidak akan pernah gagal dan jika Anda mau, sangat mudah untuk menulis pengujian unit untuk kelas ini. Menggunakan refleksi akan memberikan sedikit penurunan, tetapi tidak ada yang perlu dikhawatirkan.
Untuk menjawab salah satu kekhawatiran Anda:
Jadi saya mencari alternatif yang merangkum seluruh kueri, tetapi masih cukup fleksibel sehingga Anda tidak hanya menukar spaghetti Repositories untuk ledakan kelas perintah.
Konsekuensi dari penggunaan desain ini adalah akan ada banyak kelas kecil dalam sistem, tetapi memiliki banyak kelas kecil / terfokus (dengan nama yang jelas) adalah hal yang baik. Pendekatan ini jelas jauh lebih baik daripada memiliki banyak kelebihan beban dengan parameter berbeda untuk metode yang sama dalam repositori, karena Anda dapat mengelompokkannya dalam satu kelas kueri. Jadi, Anda masih mendapatkan kelas kueri yang jauh lebih sedikit daripada metode dalam repositori.