دليل البرمجة العملية في Elixir و OTP

نشرت: 2022-03-11

يحب الناس تصنيف لغات البرمجة إلى نماذج. هناك لغات موجهة للكائنات (OO) ، ولغات حتمية ، ولغات وظيفية ، وما إلى ذلك. يمكن أن يكون هذا مفيدًا في معرفة اللغات التي تحل مشكلات مماثلة ، وأنواع المشكلات التي تهدف اللغة إلى حلها.

في كل حالة ، يحتوي النموذج عمومًا على تركيز وتقنية "رئيسية" واحدة هي القوة الدافعة لعائلة اللغات هذه:

  • في لغات OO ، إنها الفئة أو الكائن كطريقة لتغليف الحالة (البيانات) بمعالجة تلك الحالة (الطرق).

  • في اللغات الوظيفية ، يمكن أن يكون التلاعب بالوظائف نفسها أو البيانات غير القابلة للتغيير التي يتم تمريرها من وظيفة إلى أخرى.

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

في هذا المنشور ، سوف نلتقط معنى البرمجة الموجهة نحو العمليات عند استخدام هذه اللغات ، ونستكشف الاختلافات وأوجه التشابه مع النماذج الأخرى ، ونرى الآثار المترتبة على كل من التدريب والاعتماد ، وننتهي بمثال قصير عن البرمجة الموجهة نحو العملية.

ما هي البرمجة الموجهة نحو العمليات؟

لنبدأ بتعريف: البرمجة الموجهة نحو العمليات هي نموذج يعتمد على التواصل المتسلسل ، في الأصل من ورقة كتبها توني هور في عام 1977. وهذا ما يسمى أيضًا نموذج الممثل للتزامن. تشمل اللغات الأخرى التي لها علاقة بهذا العمل الأصلي أوكام وليمبو وجو. الورقة الرسمية تتعامل فقط مع الاتصال المتزامن ؛ تستخدم معظم نماذج الجهات الفاعلة (بما في ذلك OTP) الاتصال غير المتزامن أيضًا. من الممكن دائمًا بناء اتصال متزامن فوق الاتصال غير المتزامن ، ويدعم OTP كلا النموذجين.

في هذا التاريخ ، أنشأ OTP نظامًا للحوسبة المتسامحة من خلال توصيل العمليات المتسلسلة. تأتي التسهيلات المتسامحة مع الخطأ من نهج "دعها تفشل" مع استعادة الأخطاء الصلبة في شكل المشرفين واستخدام المعالجة الموزعة التي تم تمكينها بواسطة نموذج الممثل. يمكن مقارنة "دعها تفشل" بـ "منعها من الفشل" ، حيث أن الأولى أسهل بكثير في استيعابها وقد ثبت في مكتب المدعي العام أنها أكثر موثوقية بكثير من الثانية. والسبب هو أن جهد البرمجة المطلوب لمنع الفشل (كما هو موضح في نموذج استثناء Java الذي تم فحصه) أكثر تعقيدًا وتطلبًا.

لذلك ، يمكن تعريف البرمجة الموجهة للعملية على أنها نموذج يكون فيه هيكل العملية والتواصل بين عمليات النظام هو الاهتمامات الأساسية .

البرمجة الشيئية مقابل البرمجة العملية

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

البرمجة الموجهة نحو العمليات: نموذج لمخطط فئة UML

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

تكمن قوة نهج التطوير الموجه للكائنات في أنه يمكن توسيع النظام لدعم أنواع جديدة من الكائنات ذات تأثير محدود على الكود الحالي ، طالما أن أنواع الكائنات الجديدة تتوافق مع توقعات الكود الحالي.

البرمجة الوظيفية مقابل البرمجة العملية

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

على سبيل المثال ، Scala هي لغة وظيفية مبنية على Java Virtual Machine. في حين أنه يمكنه الوصول إلى مرافق Java للاتصال ، إلا أنه ليس جزءًا متأصلًا من اللغة. في حين أنها لغة شائعة مستخدمة في برمجة Spark ، فهي مرة أخرى مكتبة تستخدم جنبًا إلى جنب مع اللغة.

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

الإكسير / OTP والبرمجة العملية

في Elixir / Erlang و OTP ، تعد أساسيات الاتصال جزءًا من الجهاز الظاهري الذي ينفذ اللغة. القدرة على التواصل بين العمليات وبين الآلات مبنية في نظام اللغة ومركزية. وهذا يؤكد أهمية الاتصال في هذا النموذج وفي أنظمة اللغة هذه.

في حين أن لغة الإكسير وظيفية في الغالب من حيث المنطق المعبر عنه في اللغة ، فإن استخدامها موجه نحو العملية .

ماذا يعني أن تكون عملية المنحى؟

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

على هذا النحو ، فإن التصميم الموجه نحو العملية هو تحديد مجموعة أنواع العمليات المطلوبة لحل مشكلة أو تلبية حاجة .

يدخل جانب الوقت بسرعة في جهود التصميم والمتطلبات. ما هي دورة حياة النظام؟ ما هي الاحتياجات الجمركية العرضية والتي هي ثابتة؟ أين الحمولة في النظام وما السرعة والحجم المتوقعان؟ فقط بعد فهم هذه الأنواع من الاعتبارات يبدأ التصميم الموجه نحو العملية في تحديد وظيفة كل عملية أو المنطق الذي سيتم تنفيذه.

آثار التدريب

دلالة هذا التصنيف على التدريب هو أن التدريب يجب أن لا يبدأ بأمثلة لغوية أو أمثلة "Hello World" ، ولكن بالتفكير في هندسة النظم وتركيز التصميم على تخصيص العملية .

تعتبر مخاوف الترميز ثانوية بالنسبة لتصميم العملية والتخصيص التي تتم معالجتها بشكل أفضل على مستوى أعلى ، وتتضمن تفكيرًا متعدد الوظائف حول دورة الحياة ، وضمان الجودة ، و DevOps ، ومتطلبات عمل العميل. يجب أن تشتمل أي دورة تدريبية في Elixir أو Erlang (وهي كذلك بشكل عام) على OTP ، ويجب أن يكون لها توجيه للعملية من البداية ، وليس مثل "الآن يمكنك البرمجة في Elixir ، لذلك دعونا نفعل التزامن".

آثار التبني

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

بالنسبة لأعمال التوثيق والتصميم ، قد يكون من المفيد جدًا استخدام تدوين رسومي (مثل الشكل 1 للغات OO). اقتراح الإكسير والبرمجة الموجهة من UML سيكون مخطط التسلسل (مثال في الشكل 2) لإظهار العلاقات الزمنية بين العمليات وتحديد العمليات التي تشارك في خدمة الطلب. لا يوجد نوع مخطط UML لالتقاط دورة الحياة وبنية العملية ، ولكن يمكن تمثيله بمربع بسيط ومخطط سهم لأنواع العمليات وعلاقاتها. على سبيل المثال ، الشكل 3:

عينة البرمجة الموجهة نحو العملية مخطط تسلسل UML

مخطط هيكل عملية البرمجة عينة عملية المنحى

مثال على اتجاه العملية

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

التصميم الأولي للعملية والتخصيص

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

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

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

في جميع أنواع العمليات الثلاثة ، يمكننا توسيع نطاق العمليات بشكل مستقل عن بعضها البعض ، وتوزيعها جغرافيًا ، وضمان عدم فقدان النتائج أبدًا من خلال الإقرار النشط بعمليات نقل البيانات بين العمليات.

كما تمت مناقشته ، بدأنا المثال بتصميم عملية مستقل عن منطق الأعمال في كل عملية. في الحالات التي يكون فيها لمنطق الأعمال متطلبات محددة لتجميع البيانات أو الجغرافيا التي يمكن أن تؤثر على تخصيص العملية بشكل متكرر. يظهر تصميم العملية لدينا حتى الآن في الشكل 4.

مثال على التطوير الموجه نحو العملية: التصميم الأولي للعملية

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

من خلال إجراء الحساب في مجموعة معزولة من العمليات ، يمكننا إدارة العبء على تلك العمليات وضمان استقرارها ومتطلبات الموارد.

من خلال وضع العرض التقديمي للنتيجة في مجموعة معزولة من العمليات ، فإننا نتحكم في الحمل على بقية النظام ونسمح لمجموعة العمليات بالتوسع ديناميكيًا للتحميل.

متطلبات إضافية

الآن ، دعنا نضيف بعض المتطلبات المعقدة. لنفترض أنه في كل ولاية قضائية (دولة أو ولاية) ، يمكن أن ينتج عن جدولة الأصوات نتيجة متناسبة ، أو نتيجة فوز الفائز بكل شيء ، أو عدم وجود نتيجة إذا تم الإدلاء بعدد غير كافٍ من الأصوات بالنسبة إلى عدد سكان تلك الولاية القضائية. كل ولاية قضائية لديها سيطرة على هذه الجوانب. مع هذا التغيير ، فإن نتائج البلدان ليست تجميعًا بسيطًا لنتائج التصويت الأولية ، ولكنها عبارة عن تجميع لنتائج الولاية / الإقليم. يؤدي هذا إلى تغيير تخصيص العملية من الأصل إلى المطالبة بأن النتائج من عمليات الولاية / المقاطعة تغذي عمليات الدولة. إذا كان البروتوكول المستخدم بين عمليات جمع الأصوات والولاية / المقاطعة والمقاطعة إلى الدولة هو نفسه ، فيمكن إعادة استخدام منطق التجميع ، ولكن هناك حاجة إلى عمليات متميزة تحمل النتائج وتكون مسارات الاتصال الخاصة بها مختلفة ، كما هو موضح في الشكل 5.

مثال على التطوير الموجه نحو العمليات: تصميم عملية معدل

الرمز

لإكمال المثال ، سنراجع تنفيذ المثال في Elixir OTP. لتبسيط الأمور ، يفترض هذا المثال استخدام خادم ويب مثل Phoenix لمعالجة طلبات الويب الفعلية ، وتقوم خدمات الويب هذه بتقديم طلبات إلى العملية المحددة أعلاه. هذا له ميزة تبسيط المثال والحفاظ على التركيز على Elixir / OTP. في نظام الإنتاج ، يكون لوجود هذه العمليات المنفصلة بعض المزايا بالإضافة إلى فصل الاهتمامات ، ويسمح بالنشر المرن ، ويوزع الحمل ، ويقلل من زمن الوصول. يمكن العثور على كود المصدر الكامل مع الاختبارات على https://github.com/technomage/voting. تم اختصار المصدر في هذا المنشور لسهولة القراءة. كل عملية أدناه تتناسب مع شجرة إشراف مكتب المدعي العام لضمان إعادة العمليات عند الفشل. انظر المصدر لمزيد من المعلومات حول هذا الجانب من المثال.

مسجل التصويت

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

 defmodule Voting.VoteRecorder do @moduledoc """ This module receives votes and sends them to the proper aggregator. This module uses supervised tasks to ensure that any failure is recovered from and the vote is not lost. """ @doc """ Start a task to track the submittal of a vote to an aggregator. This is a supervised task to ensure completion. """ def cast_vote where, who do Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor, fn -> Voting.Aggregator.submit_vote where, who end) |> Task.await end end

مجمع الأصوات

تقوم هذه العملية بتجميع الأصوات داخل الولاية القضائية ، وتحسب النتيجة لتلك الولاية القضائية ، وإعادة توجيه ملخصات التصويت إلى العملية الأعلى التالية (اختصاص أعلى مستوى ، أو مقدم نتيجة).

 defmodule Voting.Aggregator do use GenStage ... @doc """ Submit a single vote to an aggregator """ def submit_vote id, candidate do pid = __MODULE__.via_tuple(id) :ok = GenStage.call pid, {:submit_vote, candidate} end @doc """ Respond to requests """ def handle_call {:submit_vote, candidate}, _from, state do n = state.votes[candidate] || 0 state = %{state | votes: Map.put(state.votes, candidate, n+1)} {:reply, :ok, [%{state.id => state.votes}], state} end @doc """ Handle events from subordinate aggregators """ def handle_events events, _from, state do votes = Enum.reduce events, state.votes, fn e, votes -> Enum.reduce e, votes, fn {k,v}, votes -> Map.put(votes, k, v) # replace any entries for subordinates end end # Any jurisdiction specific policy would go here # Sum the votes by candidate for the published event merged = Enum.reduce votes, %{}, fn {j, jv}, votes -> # Each jourisdiction is summed for each candidate Enum.reduce jv, votes, fn {candidate, tot}, votes -> Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}" n = votes[candidate] || 0 Map.put(votes, candidate, n + tot) end end # Return the published event and the state which retains # Votes by jourisdiction {:noreply, [%{state.id => merged}], %{state | votes: votes}} end end

مقدم النتيجة

تتلقى هذه العملية أصواتًا من مُجمِّع وتخزين هذه النتائج مؤقتًا لطلبات الخدمة لعرض النتائج.

 defmodule Voting.ResultPresenter do use GenStage … @doc """ Handle requests for results """ def handle_call :get_votes, _from, state do {:reply, {:ok, state.votes}, [], state} end @doc """ Obtain the results from this presenter """ def get_votes id do pid = Voting.ResultPresenter.via_tuple(id) {:ok, votes} = GenStage.call pid, :get_votes votes end @doc """ Receive votes from aggregator """ def handle_events events, _from, state do Logger.debug "@@@@ Presenter received: #{inspect events}" votes = Enum.reduce events, state.votes, fn v, votes -> Enum.reduce v, votes, fn {k,v}, votes -> Map.put(votes, k, v) end end {:noreply, [], %{state | votes: votes}} end end

يبعد

استكشف هذا المنشور Elixir / OTP من إمكاناته كلغة عملية المنحى ، قارن هذا بالنماذج الوظيفية والموضوعية ، واستعرض آثار ذلك على التدريب والاعتماد.

يتضمن المنشور أيضًا مثالًا قصيرًا لتطبيق هذا التوجه على مشكلة عينة. إذا كنت ترغب في مراجعة جميع الكود ، فإليك رابطًا لمثالنا على GitHub مرة أخرى ، فقط حتى لا تضطر إلى التمرير للخلف للبحث عنه.

الوجبات الرئيسية هي النظر إلى الأنظمة على أنها مجموعة من عمليات الاتصال. خطط للنظام من وجهة نظر تصميم العملية أولاً ، ثم من وجهة نظر التشفير المنطقي ثانيًا.