
Dalam artikel saya sebelumnya, saya menjelaskan cara menerapkan pola Maybe Monad menggunakan pernyataan async / menunggu . Kali ini saya akan memberi tahu Anda cara menerapkan templat desain populer lainnya, "Monad Reader", menggunakan teknik yang sama.
Templat ini memungkinkan Anda untuk secara implisit mentransfer konteks tertentu ke hierarki pemanggilan fungsi tanpa menggunakan parameter atau bidang kelas, dan itu dapat dianggap sebagai cara lain untuk menerapkan Injeksi Ketergantungan. Sebagai contoh:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
"Pembaca" klasik
Pertama, mari kita lihat bagaimana Anda dapat menerapkan pola ini tanpa pernyataan async / menunggu :
public class Config { public string Template; } public static class ClassicReader { public static void Main() { var greeter = GreetGuys(); Console.WriteLine(greeter.Apply(new Config{Template = "Hello, {0}"}));
(Pembaca)
public class Reader<T, TCtx> { private readonly Func<TCtx, T> _exec; public Reader(Func<TCtx, T> exec) => this._exec = exec; public T Apply(TCtx ctx) => this._exec(ctx); } public static class Reader { public static Reader<TJoin, TCtx> SelectMany<TIn, TOut, TCtx, TJoin>( this Reader<TIn, TCtx> source, Func<TIn, Reader<TOut, TCtx>> bind, Func<TIn, TOut, TJoin> join) => new Reader<TJoin, TCtx>(ctx => { var inValue = source.Apply(ctx); var outValue = bind(inValue).Apply(ctx); return join(inValue, outValue); }); }
Kode ini berfungsi, tetapi tanpa menggunakan sintaks kueri (yang tidak selalu nyaman dengan sendirinya), keterbacaannya menurun secara dramatis dan ini tidak mengejutkan karena monad berasal dari bahasa fungsional di mana kode tersebut terlihat alami dan terbaca dengan baik (meskipun bahkan di Haskelel muncul dengan "lakukan" notasi untuk meningkatkan keterbacaan). Namun, implementasi klasik membantu untuk memahami esensi dari pola - alih-alih segera mengeksekusi beberapa kode, ia ditempatkan dalam fungsi yang hanya akan dipanggil ketika ia mendapatkan konteksnya.
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany dapat menggabungkan beberapa fungsi ini menjadi satu, sehingga Anda dapat membuat seluruh rutin yang akan ditunda hingga konteksnya diterapkan. Di sisi lain, pendekatan ini menyerupai penulisan kode asinkron, di mana eksekusi program dihentikan jika hasil dari beberapa operasi asinkron diperlukan. Ketika hasil operasi siap, program akan melanjutkan. Ada asumsi bahwa infrastruktur C #, yang dirancang untuk bekerja dengan operasi asinkron ( async / menunggu ), dapat digunakan dalam beberapa cara ketika mengimplementasikan monad "Pembaca" dan ... asumsi ini benar! Jika fungsi memerlukan akses ke konteks, maka pelaksanaannya dapat "dijeda" sampai konteks ini ditentukan secara eksternal.
Pembaca Asinkron
Dalam artikel saya sebelumnya, saya menunjukkan cara untuk mendapatkan kontrol atas pernyataan async / menunggu menggunakan tipe pengembalian asinkron Generik . Pendekatan yang sama akan digunakan saat ini. Mari kita mulai dengan kelas Reader , yang akan digunakan sebagai jenis hasil operasi asinkron:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
Kelas ini memiliki dua tugas (secara teoritis, kita dapat membuat dua kelas yang berbeda):
- Mengambil nilai dari konteks.
- Buat daftar tautan instance dari kelas Reader yang akan digunakan untuk mendistribusikan konteks di seluruh hierarki panggilan.
Untuk masing-masing tugas ini, kami akan membuat konstruktor terpisah:
private readonly Func<object, T> _extractor;
Ketika instance kelas Reader digunakan sebagai argumen kepada operator yang menunggu , instance ini mendapatkan referensi ke delegasi yang akan melanjutkan program. Delegasi ini harus dipanggil setelah menerima konteks dari ekstraksi data yang diperlukan untuk melanjutkan program.

Untuk menautkan instance kelas Reader , mari kita buat metode SetChild :
private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } }
yang akan dipanggil di dalam ReaderTaskMethodBuilder :
public class ReaderTaskMethodBuilder<T> { ... public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IReader reader) { this.Task.SetChild(reader); } awaiter.OnCompleted(stateMachine.MoveNext); } public Reader<T> Task { get; } }
Di dalam metode SetChild, kita memanggil fungsi SetCtx untuk menyebarkan konteks melintasi hirarki panggilan. Jika, saat memanggil SetCtx pada level hirarki ini, fungsi _ extractor (konstruktor pertama dari kelas Reader ) ditentukan yang secara langsung mengekstraksi data dari konteks, sekarang Anda dapat memanggilnya, mendapatkan data yang diperlukan dan menyelesaikan operasi asinkron saat ini melalui panggilan ke SetResult :
public void SetCtx(object ctx) { this._ctx = ctx; if (this._ctx != null) { this._child?.SetCtx(this._ctx); if (this._extractor != null) { this.SetResult(this._extractor(this._ctx)); } } }
SetResult menyimpan nilai yang diambil dari konteks dan memanggil delegasi untuk melanjutkan program:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
Dalam kasus ketika instance kelas Reader tidak memiliki fungsi _ extractor dimulai (konstruktor kedua dari kelas Reader ), maka SetResult harus dipanggil oleh ReaderTaskMethodBuilder ketika mesin state yang dihasilkan masuk ke status akhir.
SetCtx juga disebut dalam metode Terapkan untuk mengatur konteks di simpul akar hierarki:
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
Kode GitHub Lengkap
Sekarang, Anda dapat melihat contoh yang lebih realistis menggunakan Asynchronous Reader -a:
Klik untuk membuka contoh. public static class ReaderTest { public class Configuration { public readonly int DataBaseId; public readonly string GreetingTemplate; public readonly string NameFormat; public Configuration(int dataBaseId, string greetingTemplate, string nameFormat) { this.DataBaseId = dataBaseId; this.GreetingTemplate = greetingTemplate; this.NameFormat = nameFormat; } } public static async Task Main() { int[] ids = { 1, 2, 3 }; Configuration[] configurations = { new Configuration(100, "Congratulations, {0}! You won {1}$!", "{0} {1}"), new Configuration(100, "¡Felicidades, {0}! Ganaste {1} $", "{0}"), }; foreach (var configuration in configurations) { foreach (var userId in ids) {
Program ini menunjukkan salam untuk beberapa pengguna, tetapi kami tidak tahu nama mereka sebelumnya karena kami hanya memiliki pengidentifikasi mereka, jadi kami perlu membaca nama mereka dari "database". Untuk terhubung ke "database" ini, Anda perlu mengetahui parameter koneksi, dan untuk menghasilkan salam, kami juga membutuhkan template untuk salam ini. Semua informasi ini diteruskan secara implisit melalui Pembaca yang tidak sinkron.
Ketergantungan injeksi melalui "Pustaka" asinkron
Dibandingkan dengan implementasi klasik, Reader asinkron memiliki satu kelemahan - kami tidak dapat menentukan jenis konteks yang akan dikirim. Keterbatasan ini berasal dari kenyataan bahwa kompiler C # hanya memungkinkan satu tipe data parameter (tipe generik) di kelas ReaderTaskMethodBuilder (ini mungkin diperbaiki di versi mendatang).
Di sisi lain, saya tidak berpikir ini penting, karena dalam kehidupan nyata sebuah wadah injeksi ketergantungan kemungkinan besar akan dilewatkan sebagai sebuah konteks:
public static class Reader { public static Reader<TService> GetService<TService>() => Reader<TService>.Read<IServiceProvider>(serviceProvider => (TService)serviceProvider .GetService(typeof(TService))); }
... private static async Reader<string> Greet(string userName) { var service = await Reader.GetService<IGreater>(); return service.GreetUser(userName); } ...
( Di sini Anda dapat menemukan versi lengkap ... )
Tidak seperti asynchronous “Maybe” , yang saya tidak rekomendasikan untuk digunakan dalam kode industri apa pun, saya akan mempertimbangkan untuk menggunakan Asynchronous Reader -a dalam beberapa proyek kehidupan nyata sebagai pengganti (atau penambahan) ke mekanisme injeksi ketergantungan tradisional (ketika semua dependensi dilewatkan sebagai parameter konstruktor) karena Reader -a memiliki sejumlah keunggulan:
- Tidak perlu untuk bidang yang kelasnya menyimpan tautan ke sumber daya yang disematkan. Bahkan, tidak akan ada kebutuhan untuk kelas nyata sama sekali, karena semua logika dapat diimplementasikan dalam metode statis.
- Menggunakan Reader -a akan mengarah pada penulisan kode non-blocking karena semua metode akan asinkron dan tidak ada yang akan mengganggu penggunaan versi asinkron fungsi perpustakaan.
- Kode akan sedikit lebih mudah dibaca, karena setiap kali kita melihat Reader sebagai jenis pengembalian beberapa metode, kita akan tahu bahwa itu memerlukan akses ke beberapa konteks implisit
- Asynchronous Reader tidak menggunakan refleksi.
Tentu saja, mungkin ada keberatan untuk menggunakan Pembaca ini -a, tetapi dalam kasus apa pun, tugas utama dari artikel ini adalah untuk menunjukkan bagaimana templat yang awalnya dibuat untuk bahasa fungsional dapat diadaptasi untuk gaya pemrograman imperatif, yang dianggap oleh kebanyakan orang lebih mudah untuk dipahami .