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 TResult
jenis 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 User
objek. 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 IQueryHandler
antarmuka 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 IQueryHandlers
dalam kode kita. Ketika kita mengubah FindUsersBySearchTextQuery
kembali UserInfo[]
bukan (dengan menerapkan IQuery<UserInfo[]>
), yang UserController
akan gagal dikompilasi, karena jenis kendala generik pada IQueryHandler<TQuery, TResult>
tidak akan mampu memetakan FindUsersBySearchTextQuery
ke User[]
.
Menyuntikkan IQueryHandler
antarmuka 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 IQueryHandlers
dengan lapisan abstraksi ekstra. Kami membuat mediator yang berada di antara konsumen dan penangan kueri:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Ini IQueryProcessor
adalah antarmuka non-generik dengan satu metode umum. Seperti yang Anda lihat dalam definisi antarmuka, IQueryProcessor
tergantung pada IQuery<TResult>
antarmuka. Hal ini memungkinkan kami untuk memiliki dukungan waktu kompilasi pada konsumen kami yang bergantung pada IQueryProcessor
. Mari tulis ulang UserController
untuk 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 UserController
sekarang tergantung pada IQueryProcessor
yang dapat menangani semua pertanyaan kita. The UserController
's SearchUsers
metode memanggil IQueryProcessor.Process
metode yang lewat di sebuah objek query diinisialisasi. Karena FindUsersBySearchTextQuery
mengimplementasikan 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 Process
metode ini juga dikenal.
Sekarang menjadi tanggung jawab implementasi IQueryProcessor
untuk 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 QueryProcessor
kelas 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 Handle
metode menggunakan refleksi (dengan menggunakan kata kunci dymamic C # 4.0 dalam kasus ini), karena pada titik ini tidak mungkin untuk mentransmisikan contoh handler, karena TQuery
argumen generik tidak tersedia pada waktu kompilasi. Namun, kecuali Handle
metode 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.