Dalam artikel saya sebelumnya saya menjelaskan cara mencapai perilaku monad "Maybe" menggunakan operator async / wait . Kali ini saya akan menunjukkan bagaimana menerapkan pola desain populer lainnya "Reader Monad" menggunakan teknik yang sama.
Pola itu memungkinkan secara implisit melewati beberapa konteks ke beberapa fungsi tanpa menggunakan parameter fungsi atau objek global bersama dan itu dapat dianggap sebagai cara lain untuk mengimplementasikan 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 monad dapat diimplementasikan tanpa operator 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 berfungsi tetapi tidak terlihat alami untuk pengembang C #. Tidak heran, karena monad berasal dari bahasa fungsional di mana kode yang sama dapat ditulis dengan cara yang lebih ringkas. Namun, implementasi klasik membantu mengecilkan esensi dari pola - alih-alih eksekusi segera dari beberapa kode itu dimasukkan ke dalam fungsi yang akan dipanggil ketika konteksnya siap.
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany dapat menggabungkan beberapa fungsi tersebut menjadi satu, sehingga Anda dapat membuat sub-rutin yang pelaksanaannya akan ditunda sampai konteksnya diterapkan. Di sisi lain, pendekatan itu menyerupai penulisan kode asinkron di mana eksekusi program dihentikan jika beberapa operasi async berjalan. Ketika hasil operasi siap, eksekusi program akan dilanjutkan. Suatu asumsi muncul bahwa infrastruktur C # yang dirancang untuk bekerja dengan operasi asinkron ( async / menunggu ) entah bagaimana dapat digunakan dalam implementasi monad "Pembaca" dan ... asumsi itu benar!
"Pembaca" Async
Dalam artikel saya sebelumnya, saya mendemonstrasikan cara mendapatkan kontrol atas operator async / menunggu menggunakan tipe pengembalian async Generalized . Teknik yang sama akan digunakan saat ini. Mari kita mulai dengan kelas "Pembaca" yang akan digunakan sebagai jenis hasil operasi asinkron:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
Kelas memiliki dua tanggung jawab yang berbeda (secara teoritis kita dapat membuat 2 kelas):
- Mengekstraksi beberapa nilai membentuk konteks ketika konteks diterapkan
- Pembuatan daftar instance Reader yang ditautkan yang akan digunakan untuk mendistribusikan konteks melalui hierarki panggilan.
Untuk setiap tanggung jawab, kami akan menggunakan konstruktor terpisah:
private readonly Func<object, T> _extractor;
Ketika instance kelas Reader digunakan sebagai argumen dari operator yang menunggu instance akan menerima tautan ke delegasi lanjutan yang harus dipanggil hanya ketika konteks eksekusi diselesaikan dan kami dapat mengekstrak (dari konteks) beberapa data yang akan digunakan dalam kelanjutan.

Untuk membuat koneksi antara "pembaca" induk dan anak, mari kita buat metode ini:
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 SetCtx yang menyebarkan konteks ke hierarki dan memanggil extractor jika didefinisikan:
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 diekstraksi dari konteks dan memanggil kelanjutan:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
Dalam kasus ketika instance Reader tidak memiliki "extractor" yang diinisialisasi, maka SetResult seharusnya dipanggil oleh ReaderTaskMethodBuilder ketika mesin state yang dihasilkan masuk ke status akhir.
Terapkan metode panggil saja SetCtx
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
Anda dapat menemukan semua kode di github ( jika masih belum diblokir )
Sekarang, saya ingin menunjukkan contoh yang lebih realistis tentang bagaimana Async Reader dapat digunakan:
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 id mereka, jadi kami perlu membaca informasi itu dari "database". Untuk terhubung ke basis data, kita perlu mengetahui beberapa pengenal koneksi dan untuk membuat salam, kita memerlukan templatnya dan ... semua informasi dilewatkan secara implisit melalui Pembaca async.
Ketergantungan Injeksi dengan Async "Reader"
Dibandingkan dengan implementasi klasik, pembaca async memiliki kekurangan - kita tidak dapat menentukan jenis konteks yang diteruskan. Keterbatasan ini berasal dari fakta bahwa C # compiler mengharapkan hanya satu tipe parameter generik dalam kelas metode async builder (mungkin akan diperbaiki di masa depan).
Di sisi lain, saya tidak berpikir itu penting karena dalam kehidupan nyata kemungkinan besar beberapa wadah injeksi ketergantungan akan diberikan sebagai 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 contoh lengkap ... )
Tidak seperti async "Maybe" , yang saya tidak rekomendasikan untuk digunakan dalam kode produksi apa pun (karena masalah dengan akhirnya blok), saya akan mempertimbangkan untuk menggunakan Async Reader dalam proyek nyata sebagai pengganti (atau "selain") pendekatan injeksi ketergantungan tradisional (ketika semua dependensi dilewatkan ke konstruktor kelas) karena Pembaca memiliki beberapa keuntungan:
- Tidak perlu di properti kelas yang menyimpan tautan ke sumber daya yang disuntikkan. Sebenarnya tidak perlu di kelas sama sekali - semua logika dapat diimplementasikan dalam metode statis.
- Menggunakan Pembaca akan mendorong pembuatan kode non-pemblokiran karena semua metode akan tidak sinkron dan tidak ada yang akan mencegah pengembang menggunakan versi operasi input / output yang tidak menghalangi.
- Kode akan sedikit lebih "dapat dibaca" - setiap kali kita melihat Pembaca sebagai jenis hasil dari beberapa metode kita akan tahu bahwa itu memerlukan akses ke beberapa konteks implisit.
- Pembaca async tidak menggunakan refleksi.
Tentu saja mungkin ada beberapa argumen yang menentang penggunaan Pembaca tetapi bagaimanapun, tujuan utama dari artikel ini adalah untuk menunjukkan bagaimana pola, yang awalnya dirancang untuk bahasa fungsional, dapat diadopsi ke gaya imperatif pengkodean yang diyakini lebih mudah untuk dimengerti oleh kebanyakan orang.