Monad "Reader" عبر المزامنة / في انتظار C #


في مقالتي السابقة ، وصفت كيفية تنفيذ نمط ربما مناد باستخدام عبارات غير متزامنة / تنتظر . هذه المرة سوف أخبرك بكيفية تطبيق قالب تصميم شائع آخر ، "Monad Reader" ، باستخدام نفس التقنيات.


يسمح لك هذا القالب بنقل سياق معين ضمنيًا إلى التسلسل الهرمي لاستدعاءات الوظائف دون استخدام معلمات أو حقول فئة ، ويمكن اعتباره طريقة أخرى لتطبيق Dependency Injection. على سبيل المثال:


class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"})); //(Hi, John!, Hi, José!) Console.WriteLine(await GreetGuys().Apply(new Config {Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, José!) } //       -   “Config". public static async Reader<(string gJohn, string gJose)> GreetGuys() => (await Greet("John"), await Greet("José")); static async Reader<string> Greet(string name) => string.Format(await ExtractTemplate(), name); static async Reader<string> ExtractTemplate() => await Reader<string>.Read<Config>(c => c.Template); 

"القارئ" الكلاسيكي


أولاً ، دعنا نرى كيف يمكنك تطبيق هذا النمط دون عبارات التزامن / الانتظار :


 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}"})); //(Hello, John, Hello, Jose) Console.WriteLine(greeter.Apply(new Config{Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, Jose!) } public static Reader<(string gJohn, string gJose), Config> GreetGuys() => from toJohn in Greet("John") from toJose in Greet("Jose") select (toJohn, toJose); //  "query syntax"      : //Greet("John") // .SelectMany( // toJohn => Greet("Jose"), // (toJohn, toJose) => (toJohn, toJose)) public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); } 

(القارئ)


 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); }); } 

تعمل هذه التعليمة البرمجية ، لكن بدون استخدام بناء جملة الاستعلام (والذي لا يناسب دائمًا حد ذاته) ، تنخفض قابلية قراءته بشكل كبير وهذا ليس مفاجئًا لأن الأحاديات جاءت من لغات وظيفية حيث تبدو هذه الشفرة طبيعية وتقرأ جيدًا (على الرغم من أنه حتى في Haskelel ظهرت "do") تدوين لتحسين قابلية القراءة). ومع ذلك ، فإن التطبيق الكلاسيكي يساعد على فهم جوهر النموذج - بدلاً من تنفيذ بعض التعليمات البرمجية على الفور ، يتم وضعه في وظيفة لن يتم استدعاؤها إلا عندما يحصل على سياقها.


 public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); //      ,     : //public static string Greet(string name, Config cfg) // => string.Format(cfg.Template, name); 

يمكن لـ SelectMany تجميع العديد من هذه الوظائف في واحدة ، حتى تتمكن من إنشاء روتين كامل سيتم تأجيله حتى يتم تطبيق السياق الخاص به. من ناحية أخرى ، يشبه هذا الأسلوب كتابة التعليمات البرمجية غير المتزامنة ، حيث يتم إيقاف تنفيذ البرنامج إذا كانت نتيجة بعض العمليات غير المتزامنة مطلوبة. عندما تكون نتيجة العملية جاهزة ، سيستمر البرنامج. هناك افتراض أن البنية الأساسية C # ، المصممة للعمل مع العمليات غير المتزامنة (غير المتزامن / تنتظر ) ، يمكن استخدامها بطريقة أو بأخرى عند تنفيذ monad "Reader" و ... هذا الافتراض صحيح! إذا كانت الوظائف تتطلب الوصول إلى السياق ، فيمكن عندئذٍ تعليقه مؤقتًا حتى يتم تحديد هذا السياق خارجيًا.


قارئ غير متزامن


في مقالتي السابقة ، أوضحت كيفية التحكم في عبارات المزامنة / الانتظار باستخدام أنواع الإرجاع غير المتزامنة العامة . وسيتم استخدام نفس النهج هذه المرة. لنبدأ بفئة Reader ، والتي سيتم استخدامها كنوع من نتائج العمليات غير المتزامنة:


 [AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ... 

هذه الفئة لها مهمتان (نظريا ، يمكننا إنشاء فئتين مختلفتين):


  1. استرداد القيم من السياق.
  2. قم بإنشاء قائمة مرتبط بمثيلات فئة Reader التي سيتم استخدامها لتوزيع السياق عبر التسلسل الهرمي للمكالمات.

لكل مهمة من هذه المهام ، سنقوم بإنشاء مُنشئ منفصل:


 private readonly Func<object, T> _extractor; //1.       public static Reader<T> Read<TCtx>(Func<TCtx, T> extractor) => new Reader<T>(ctx => extractor((TCtx)ctx)); private Reader(Func<object, T> exec) => this._extractor = exec; //2.   ReaderTaskMethodBuilder     C# internal Reader() { } 

عند استخدام مثيل لفئة Reader كوسيطة لمشغل انتظار ، يحصل هذا المثيل على إشارة إلى المفوض الذي سيواصل البرنامج. يجب استدعاء هذا المفوض بعد تلقي السياق من استخراج البيانات اللازمة للمتابعة.



لربط مثيلات فئة Reader ، دعنا ننشئ طريقة SetChild :


 private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } } 

والتي سوف تسمى داخل 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; } } 

داخل طريقة SetChild ، ندعو وظيفة SetCtx لنشر السياق عبر التسلسل الهرمي للمكالمات. إذا ، عند استدعاء SetCtx على هذا المستوى من التسلسل الهرمي ، تم تحديد وظيفة _ extractor (المُنشئ الأول لفئة Reader ) التي تقوم باستخراج البيانات مباشرة من السياق ، يمكنك الآن الاتصال بها ، والحصول على البيانات اللازمة وإكمال العملية الحالية غير المتزامنة من خلال مكالمة إلى 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 بتخزين القيمة التي يتم استردادها من السياق وتدعو المفوض لمتابعة البرنامج:


 internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); } 

في حالة عدم وجود مثيل لفئة Reader ، بدأت الدالة _ extractor (المُنشئ الثاني لفئة Reader ) ، ثم يجب استدعاء SetResult بواسطة ReaderTaskMethodBuilder عندما ينتقل جهاز الحالة المُنشأ إلى الحالة النهائية.


يُسمى SetCtx أيضًا في الأسلوب Apply لتعيين السياق في عقدة الجذر للتسلسل الهرمي:


 public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; } 

رمز جيثب الكامل


الآن ، يمكنك إلقاء نظرة على مثال أكثر واقعية لاستخدام قارئ غير متزامن - أ:


انقر لتوسيع المثال.
 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) { //""      - userId var logic = GetGreeting(userId); //  (database Id, templates)     var greeting = await logic.Apply(configuration); Console.WriteLine(greeting) } } //Congratulations, John Smith! You won 110$! //Congratulations, Mary Louie! You won 30$! //Congratulations, Louis Slaughter! You won 47$! //¡Felicidades, John! Ganaste 110 $ //¡Felicidades, Mary! Ganaste 30 $ //¡Felicidades, Louis! Ganaste 47 $ } private static async Reader<string> GetGreeting(int userId) { var template = await Reader<string>.Read<Configuration>(cfg => cfg.GreetingTemplate); var fullName = await GetFullName(userId); var win = await GetWin(userId); return string.Format(template, fullName, win); } private static async Reader<string> GetFullName(int userId) { var template = await Reader<string>.Read<Configuration>(cfg => cfg.NameFormat); var firstName = await GetFirstName(userId); var lastName = await GetLastName(userId); return string.Format(template, firstName, lastName); } private static async Reader<string> GetFirstName(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetFirstName(userId); } private static async Reader<string> GetLastName(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetLastName(userId); } private static async Reader<int> GetWin(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetWin(userId); } private static async Reader<Database> GetDataBase() { var dataBaseId = await Reader<int>.Read<Configuration>(cfg => cfg.DataBaseId); return Database.ConnectTo(dataBaseId); } } public class Database { public static Database ConnectTo(int id) { if (id == 100) { return new Database(); } throw new Exception("Wrong database"); } private Database() { } private static readonly (int Id, string FirstName, string LastName, int Win)[] Data = { (1, "John","Smith", 110), (2, "Mary","Louie", 30), (3, "Louis","Slaughter", 47), }; public async Task<string> GetFirstName(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).FirstName; } public async Task<string> GetLastName(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).LastName; } public async Task<int> GetWin(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).Win; } } 

يعرض البرنامج تحيات بعض المستخدمين ، لكننا لا نعرف أسماءهم مقدمًا لأن لدينا معرفاتهم فقط ، لذلك نحتاج إلى قراءة أسمائهم من "قاعدة البيانات". للاتصال بـ "قاعدة البيانات" هذه ، تحتاج إلى معرفة معلمات الاتصال ، ولإنشاء تحيات ، نحتاج أيضًا إلى قوالب لهذه التحيات. يتم تمرير كل هذه المعلومات ضمنيًا من خلال قارئ غير متزامن.


حقن التبعية عبر "قارئ" غير متزامن


مقارنةً بالتطبيق الكلاسيكي ، يكون للقارئ غير المتزامن عيب واحد - لا يمكننا تحديد نوع السياق المطلوب إرساله. يأتي هذا القيد من حقيقة أن برنامج التحويل البرمجي C # يسمح بنوع واحد فقط من البيانات ذات المعلمات (النوع العام) في فئة ReaderTaskMethodBuilder (قد يتم إصلاح هذا في الإصدارات المستقبلية).


من ناحية أخرى ، لا أعتقد أن هذا أمر بالغ الأهمية ، لأنه في الحياة الواقعية على الأرجح سيتم تمرير حاوية حقن التبعية كسياق:


 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); } ... 

( هنا يمكنك العثور على النسخة الكاملة ... )


على عكس "ربما" غير المتزامن ، والذي لم أوصي باستخدامه في أي كود صناعي ، سأفكر في استخدام القارئ غير المتزامن - في بعض مشاريع الحياة الحقيقية كبديل (أو إضافة) لآليات حقن التبعية التقليدية (عندما يتم تمرير جميع التبعيات كمعلمات مُنشئ) منذ Reader -a لديه عدد من المزايا:


  1. ليست هناك حاجة للحقول التي تخزن فئاتها الموارد المضمنة. في الواقع ، لن تكون هناك حاجة لفئات حقيقية على الإطلاق ، حيث يمكن تنفيذ كل المنطق بطرق ثابتة.
  2. سيؤدي استخدام Reader -a إلى كتابة تعليمات برمجية غير محظورة لأن جميع الطرق ستكون غير متزامنة ولن يتداخل أي شيء مع استخدام الإصدارات غير المتزامنة لوظائف المكتبة.
  3. ستكون الشفرة أكثر قابلية للقراءة قليلاً ، لأنه في كل مرة نرى فيها القارئ هو نوع الإرجاع لبعض الطرق ، سنعرف أنه يتطلب الوصول إلى بعض السياق الضمني
  4. القارئ غير المتزامن لا يستخدم الانعكاس.

بالطبع ، قد تكون هناك اعتراضات على استخدام هذا Reader -a ، ولكن على أي حال ، فإن المهمة الرئيسية لهذه المقالات هي إظهار كيف يمكن تكييف القوالب التي تم إنشاؤها أصلاً للغات الوظيفية للحصول على أسلوب برمجة ضروري ، والذي يعتبره معظم الناس أسهل في الفهم .

Source: https://habr.com/ru/post/ar470501/


All Articles