"قارئ" أحادي من خلال المزامنة / في انتظار C #


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


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


class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"})); //(Hi, John!, Hi, Jose!) Console.WriteLine(await GreetGuys().Apply(new Config {Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, Jose!) } //These functions do not have any link to any instance of the Config class. public static async Reader<(string gJohn, string gJose)> GreetGuys() => (await Greet("John"), await Greet("Jose")); 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); //Without using the query notation the code would look like this: //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); }); } 

تعمل التعليمات البرمجية ولكنها لا تبدو طبيعية للمطورين C #. لا عجب ، لأن الأحاديات جاءت من لغات وظيفية حيث يمكن كتابة رمز مشابه بطريقة أكثر إيجازًا. ومع ذلك ، فإن التطبيق الكلاسيكي يساعد في التقليل من جوهر النموذج - بدلاً من التنفيذ الفوري لبعض الأكواد ، يتم وضعه في وظيفة سيتم استدعاؤها عندما يكون سياقها جاهزًا.


 public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); //That is how the code would look like with explicit passing of context: //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. Used to extract some value from a context 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. Used by ReaderTaskMethodBuilder in a compiler generated code internal Reader() { } 

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



لإنشاء اتصالات بين "القراء" الأصل والطفل ، فلننشئ الطريقة:


 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 الذي ينشر سياقًا إلى تسلسل هرمي ويستدعي مستخرجًا إذا تم تعريفه:


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

في حالة عدم وجود مثيل " قارئ " تمت تهيئته لـ " قارئ" ، فمن المفترض أن يتم استدعاء SetResult بواسطة ReaderTaskMethodBuilder عندما ينتقل جهاز الحالة الذي تم إنشاؤه إلى حالته النهائية.


تطبيق الأسلوب فقط يدعو SetCtx


 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) { //The logic receives only a single explicit parameter - userId var logic = GetGreeting(userId); //The rest of parameters (database Id, templates) can be passed implicitly 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; } } 

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


حقن التبعية مع Async "Reader"


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


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


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

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


على عكس المزامنة "ربما" ، والتي لم أوصي باستخدامها في أي كود إنتاج (بسبب المشكلة مع الكتل أخيرًا ) ، سأفكر في استخدام قارئ المتزامن في مشاريع حقيقية كبديل عن (أو "بالإضافة إلى") طريقة حقن التبعية التقليدية (عندما يتم تمرير جميع التبعيات إلى مُنشئ فئة) نظرًا لأن للقارئ بعض المزايا:


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

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

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


All Articles