يمكن اعتبار الخدمة عملية موزعة بمرور الوقت ، والتي لها عدة نقاط يمكن من خلالها التأثير على نتيجتها (الإلغاء من خلال البوابة ، الرفض في القسم ، إرسال معلومات حول التغيير في حالة الخدمة إلى البوابة ، وكذلك إرسال نتيجة توفيرها). في هذا الصدد ، تمر كل خدمة بدورة حياتها الخاصة من خلال هذه العملية ، وتراكم البيانات حول طلب المستخدم ، والأخطاء المستلمة ، ونتائج الخدمة ، وما إلى ذلك. يسمح هذا في أي وقت بالقدرة على التحكم واتخاذ قرار بشأن الإجراءات الإضافية لمعالجة الخدمة.
سوف نتحدث أكثر عن كيف وبمساعدة ما يمكنك تنظيم هذه المعالجة.
اختيار محرك أتمتة العمليات التجارية
لتنظيم معالجة البيانات ، توجد مكتبات وأنظمة لأتمتة العمليات التجارية على نطاق واسعفي السوق: من الحلول المدمجة إلى الأنظمة كاملة الميزات التي توفر إطارًا للتحكم في العملية. اخترنا Workflow Core كأداة لأتمتة العمليات التجارية. تم إجراء هذا الاختيار لعدة أسباب: أولاً ، تمت كتابة المحرك بلغة C # لمنصة .NET Core (هذه هي منصة التطوير الرئيسية لدينا) ، لذلك من الأسهل إدراجه في مخطط المنتج العام ، على عكس Camunda BPM على سبيل المثال. بالإضافة إلى ذلك ، فهو محرك مضمن يوفر فرصًا كبيرة لإدارة حالات العمليات التجارية. ثانيًا ، من بين العديد من خيارات التخزين المدعومة ، هناك أيضًا PostgreSQL مستخدمة في حلولنا. ثالثًا ، يوفر المحرك بناء جملة بسيطًا لوصف عملية في شكل واجهة برمجة تطبيقات بطلاقة (هناك أيضًا متغير لوصف عملية في ملف JSON ، ومع ذلك ،بدا أقل ملاءمة للاستخدام نظرًا لحقيقة أنه يصبح من الصعب اكتشاف خطأ في وصف العملية حتى لحظة تنفيذها الفعلي).
العمليات التجارية
من بين الأدوات المقبولة عمومًا لوصف العمليات التجارية ، يجب ملاحظة تدوين BPMN . على سبيل المثال ، قد يبدو حل مشكلة FizzBuzz في تدوين BPMN كما يلي:

يحتوي محرك Workflow Core على معظم الكتل الأساسية والعبارات المقدمة في التدوين ، كما هو مذكور أعلاه ، يسمح لك باستخدام بيانات API أو JSON بطلاقة لوصف عمليات محددة. يمكن أن يتخذ تنفيذ هذه العملية عن طريق محرك Workflow Core الشكل التالي :
// .
public class FizzBuzzWfData
{
public int Counter { get; set; } = 1;
public StringBuilder Output { get; set; } = new StringBuilder();
}
// .
public class FizzBuzzWorkflow : IWorkflow<FizzBuzzWfData>
{
public string Id => "FizzBuzz";
public int Version => 1;
public void Build(IWorkflowBuilder<FizzBuzzWfData> builder)
{
builder
.StartWith(context => ExecutionResult.Next())
.While(data => data.Counter <= 100)
.Do(a => a
.StartWith(context => ExecutionResult.Next())
.Output((step, data) => data.Output.Append(data.Counter))
.If(data => data.Counter % 3 == 0 || data.Counter % 5 == 0)
.Do(b => b
.StartWith(context => ExecutionResult.Next())
.Output((step, data) => data.Output.Clear())
.If(data => data.Counter % 3 == 0)
.Do(c => c
.StartWith(context => ExecutionResult.Next())
.Output((step, data) =>
data.Output.Append("Fizz")))
.If(data => data.Counter % 5 == 0)
.Do(c => c
.StartWith(context => ExecutionResult.Next())
.Output((step, data) =>
data.Output.Append("Buzz"))))
.Then(context => ExecutionResult.Next())
.Output((step, data) =>
{
Console.WriteLine(data.Output.ToString());
data.Output.Clear();
data.Counter++;
}));
}
}
}
بالطبع ، يمكن وصف العملية بشكل أكثر بساطة عن طريق إضافة مخرجات القيم المرغوبة في الخطوات التالية للتحقق من العلاقة الأساسية. ومع ذلك ، مع التنفيذ الحالي ، يمكنك أن ترى أن كل خطوة قادرة على إجراء بعض التغييرات على "حصالة البيانات" العامة لبيانات العملية ، ويمكنها أيضًا الاستفادة من نتائج الخطوات السابقة. في هذه الحالة ، يتم تخزين بيانات العملية في مثيل
FizzBuzzWfData، حيث يتم توفير الوصول إلى كل خطوة في وقت تنفيذها.
طريقة
Buildيأخذ كائن منشئ العمليات كوسيطة ، والذي يعمل كنقطة بداية لاستدعاء سلسلة من طرق الامتداد التي تصف بالتسلسل خطوات عملية الأعمال. يمكن أن تحتوي طرق الامتداد ، بدورها ، على وصف للإجراءات مباشرة في الكود الحالي في شكل تعبيرات lambda التي تم تمريرها كوسيطات ، أو يمكن تحديد معلمات لها. في الحالة الأولى ، التي يتم تقديمها في القائمة ، يتم ترجمة خوارزمية بسيطة إلى مجموعة معقدة من التعليمات. في الثانية ، يتم إخفاء منطق الخطوات في فئات منفصلة ترث من النوع Step(أو AsyncStepللمتغيرات غير المتزامنة) ، مما يسمح لك بملاءمة العمليات المعقدة في وصف أكثر إيجازًا. من الناحية العملية ، يبدو أن النهج الثاني أكثر ملاءمة ، في حين أن الطريقة الأولى كافية لأمثلة بسيطة أو عمليات تجارية بسيطة للغاية.
تقوم فئة وصف العملية الفعلية بتنفيذ الواجهة ذات المعلمات
IWorkflow، وعند تنفيذ العقد ، تحتوي على معرف العملية ورقم الإصدار. بفضل هذه المعلومات ، يكون المحرك قادرًا على إنتاج مثيلات المعالجة في الذاكرة ، وملءها بالبيانات وتحديد حالتها في التخزين. يسمح لك دعم الإصدار بإنشاء متغيرات عملية جديدة دون المخاطرة بالتأثير على الطبعات الموجودة في المستودع. لإنشاء إصدار جديد ، يكفي إنشاء نسخة من الوصف الحالي ، وتعيين رقم تالي للخاصية Versionوتغيير سلوك هذه العملية حسب الحاجة (يجب ترك المعرف بدون تغيير).
أمثلة على العمليات التجارية في سياق مهمتنا هي:
- – .
- – , , .
- – .
- – , .
كما ترى من الأمثلة ، يتم تقسيم جميع العمليات بشكل مشروط إلى "دورية" ، يتضمن تنفيذها تكرارًا دوريًا ، و "خطيًا" ، يتم إجراؤها في سياق عبارات محددة ، ومع ذلك ، لا تستبعد وجود بعض الهياكل الدورية داخلها.
دعنا نفكر في مثال لإحدى العمليات التي تعمل في حلنا لاستطلاع قائمة انتظار الطلبات الواردة:
public class LoadRequestWf : IWorkflow<LoadRequestWfData>
{
public const string DefinitionId = "LoadRequest";
public string Id => DefinitionId;
public int Version => 1;
public void Build(IWorkflowBuilder<LoadRequestWfData> builder)
{
builder
.StartWith(then => ExecutionResult.Next())
.While(d => !d.Quit)
.Do(x => x
.StartWith<LoadRequestStep>() // *
.Output(d => d.LoadRequest_Output, s => s.Output)
.If(d => d.LoadRequest_Output.Exception != null)
.Do(then => then
.StartWith(ctx => ExecutionResult.Next()) // *
.Output((s, d) => d.Quit = true))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.Unknown)
.Do(then => then
.StartWith<LogInfoAboutFaultResponseStep>() // *
.Input((s, d) =>
{ s.Input = d.LoadRequest_Output?.Result?.Fault; })
.Output((s, d) => d.Quit = false))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.DataRequest)
.Do(then => then
.StartWith<StartWorkflowStep>() // *
.Input(s => s.Input, d => BuildEpguNewApplicationWfData(d))
.Output((s, d) => d.Quit = false))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType == ReqType.Empty)
.Do(then => then
.StartWith(ctx => ExecutionResult.Next()) // *
.Output((s, d) => d.Quit = true))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.CancellationRequest)
.Do(then => then
.StartWith<StartWorkflowStep>() // *
.Input(s => s.Input, d => BuildCancelRequestWfData(d))
.Output((s, d) => d.Quit = false)));
}
}
في الأسطر المميزة بعلامة * ، يمكنك رؤية استخدام طرق الامتداد ذات المعلمات ، والتي توجه المحرك لاستخدام فئات الخطوة (المزيد حول ذلك لاحقًا) المقابلة لمعلمات النوع. مع مساعدة من طرق الإرشاد
Inputو Outputلدينا الفرصة لتعيين البيانات الأولية التي تم تمريرها إلى الخطوة قبل بدء التنفيذ، وبناء عليه، تغيير البيانات العملية (ويتم تمثيل من قبل مثيل من الدرجة LoadRequestWfData) في اتصال مع الإجراءات التي يقوم بها الخطوة. وهذه هي الطريقة التي تبدو بها العملية على مخطط BPMN:

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

ترتبط الواجهة والفئة
IWorkflowالمجردة ارتباطًا مباشرًا بالمحرك StepBodyAsync(ومع ذلك ، يمكنك استخدام StepBody التناظري المتزامن). يوضح الرسم البياني أدناه تنفيذ "اللبنات الأساسية" - فئات محددة مع أوصاف عمليات أعمال سير العمل والخطوات المستخدمة فيها ( Step). في المستوى الأدنى ، يتم تقديم الخدمات ، والتي ، في جوهرها ، خاصة بالفعل بهذا التنفيذ المعين للحل ، وعلى عكس العمليات والخطوات ، فهي ليست إلزامية.
يجب تسجيل الخدمات ، مثل الخطوات ، في حاوية التبعية حتى تتمكن الخطوات التي تستخدم خدماتها من الحصول على المثيلات اللازمة لها عن طريق الحقن عبر المُنشئ.
تضمين المحرك في المحلول
في وقت بداية إنشاء نظام التكامل مع البوابة ، كان الإصدار 2.1.2 من المحرك متاحًا في مستودع Nuget. إنه مدمج في حاوية التبعية بالطريقة القياسية في طريقة
ConfigureServicesالصنف Startup:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddWorkflow(opts =>
opts.UsePostgreSQL(connectionString, false, false, schemaName));
// ...
}
يمكن تكوين المحرك لأحد مستودعات البيانات المدعومة (هناك مستودعات أخرى من بينها : MySQL و MS SQL و SQLite و MongoDB). في حالة PostgreSQL ، يستخدم المحرك Entity Framework Core في متغير Code First للعمل مع العمليات. وفقًا لذلك ، إذا كانت هناك قاعدة بيانات فارغة ، فمن الممكن تطبيق الترحيل والحصول على بنية الجدول المطلوبة. يعد استخدام الترحيل أمرًا اختياريًا ، ويمكن التحكم في ذلك باستخدام وسيطات الطريقة
UsePostgreSQL: تسمح وسيطات النوع المنطقي الثانية ( canCreateDB) والثالثة ( canMigrateDB) بإخبار المحرك بما إذا كان يمكنه إنشاء قاعدة بيانات إذا لم تكن موجودة وتطبيق عمليات الترحيل.
نظرًا لأنه مع التحديث التالي للمحرك ، هناك احتمال غير صفري لتغيير نموذج البيانات الخاص به ، وقد يؤدي الاستخدام المقابل للترحيل التالي إلى إتلاف البيانات المتراكمة بالفعل ، فقد قررنا التخلي عن هذا الخيار والحفاظ على بنية قاعدة البيانات بمفردنا ، بناءً على آلية مكونات قاعدة البيانات المستخدمة في مشاريعنا الأخرى.
لذلك ، تم حل مشكلة تخزين البيانات وتسجيل المحرك في حاوية التبعية ، دعنا ننتقل إلى بدء تشغيل المحرك. بالنسبة لهذه المهمة ، ظهر خيار الخدمة المستضافة وهناانظر مثالاً على فئة أساسية لإنشاء مثل هذه الخدمة). تم تعديل الرمز المأخوذ كأساس بشكل طفيف للحفاظ على نمطية ، مما يعني تقسيم حل التكامل (يسمى "Onyx") إلى جزء مشترك يوفر تهيئة المحرك وتنفيذ بعض إجراءات الخدمة ، وجزء خاص بكل عميل محدد (وحدات التكامل) ...
تحتوي كل وحدة على أوصاف العملية ، والبنية التحتية لتنفيذ منطق الأعمال ، بالإضافة إلى بعض التعليمات البرمجية الموحدة لتمكين نظام التكامل المطور من التعرف على أوصاف العملية وتحميلها ديناميكيًا في مثيل لمحرك Workflow Core:

التسجيل وبدء العمليات التجارية
الآن بعد أن أصبح لدينا أوصاف جاهزة للعمليات التجارية والمحرك المتصل بالحل ، حان الوقت لإخبار المحرك بالعمليات التي سيعمل معها.
يتم ذلك باستخدام الكود التالي ، والذي يمكن أن يكون موجودًا في الخدمة المستضافة المذكورة سابقًا (يمكن أيضًا وضع الكود الذي يبدأ تسجيل العمليات في الوحدات المتصلة هنا):
public async Task RunWorkflowsAsync(IWorkflowHost host,
CancellationToken token)
{
host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();
// ...
await host.StartAsync(token);
token.WaitHandle.WaitOne();
host.Stop();
}
خاتمة
بشكل عام ، قمنا بتغطية الخطوات التي تحتاج إلى اتخاذها لاستخدام Workflow Core في حل تكامل. يتيح لك المحرك وصف العمليات التجارية بطريقة مرنة ومريحة. مع الأخذ في الاعتبار حقيقة أننا نتعامل مع مهمة التكامل مع بوابة "Gosuslug" من خلال SMEV ، فمن المتوقع أن تغطي العمليات التجارية المتوقعة مجموعة من المهام المتنوعة إلى حد ما (استطلاع قائمة الانتظار ، وتحميل / تنزيل الملفات ، وضمان الامتثال لبروتوكول التبادل وضمان تأكيد استلام البيانات ومعالجة الأخطاء في مراحل مختلفة وما إلى ذلك). ومن ثم ، سيكون من الطبيعي أن نتوقع حدوث بعض اللحظات غير الواضحة من التنفيذ للوهلة الأولى ، وسوف نخصص لهم المقالة النهائية التالية من الدورة.