بالطبع ، من الأفضل عدم كتابة الكثير من التعليمات البرمجية على الإطلاق . وإذا كتبت ، فعليك ، كما تعلم ، أن تفكر جيدًا نظام العظام بنية النظام وتنفيذها نظام اللحوم منطق النظام. في هذه المذكرة ، سنقدم وصفات من أجل التنفيذ المريح للأخير.
سنقدم أمثلة للغة كلوجور ، ومع ذلك ، يمكن تطبيق المبدأ نفسه في لغات البرمجة الوظيفية الأخرى (على سبيل المثال ، نستخدم نفس الفكرة بالضبط في إرلانج).
فكرة
الفكرة نفسها بسيطة وتعتمد على العبارات التالية:
- يتكون أي منطق دائمًا من خطوات أولية ؛
- لكل خطوة ، هناك حاجة إلى بيانات معينة ، والتي تطبق منطقها وتنتج إما نتيجة ناجحة أو غير ناجحة.
على مستوى الكود الزائف ، يمكن تمثيل ذلك على النحو التالي:
do-something-elementary(context) -> [:ok updated_context] | [:error reason]
أين:
do-something-elementary
- اسم الوظيفة ؛context
- حجة الدالة ، بنية البيانات مع السياق الأولي الذي تستمد منه الوظيفة جميع البيانات اللازمة ؛updated_context
- بنية البيانات مع السياق المحدث ، إذا نجحت ، حيث تضيف الوظيفة نتيجة تنفيذها ؛reason
- بنية البيانات ، سبب الفشل ، مع الفشل.
هذه هي الفكرة كلها. وبعد ذلك - مسألة تقنية. مع 100،500 مليون جزء.
مثال: مستخدم يقوم بعملية شراء
سنكتب التفاصيل على مثال بسيط ملموس ، متاح على GitHub هنا .
لنفترض أن لدينا مستخدمين بالأموال والكثير الذي يكلفهم المال ويمكن للمستخدمين شراؤه. نريد أن نكتب رمزًا ينفذ شراء القطعة:
buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]
من أجل البساطة ، سنقوم بتخزين كمية المال والكثير من المستخدمين في هيكل المستخدم نفسه.
من أجل التنفيذ ، نحتاج إلى العديد من الوظائف المساعدة.
وظيفة until-first-error
في الغالبية العظمى من الحالات ، يمكن تمثيل منطق الأعمال كتسلسل من الخطوات التي يجب اتخاذها حتى يحدث خطأ. للقيام بذلك ، سنقوم بإنشاء دالة:
until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]
أين:
fs
هو سلسلة من الدوال (الإجراءات الأولية) ؛init_context
هو السياق الأولي.
يمكن الاطلاع على تنفيذ هذه الميزة على GitHub هنا .
الدالة with-result-or-error
في كثير من الأحيان ، يكون الإجراء الأساسي هو أنك تحتاج فقط إلى تنفيذ بعض الوظائف ، وإذا نجحت ، فأضف نتيجتها إلى السياق. للقيام بذلك ، لدينا وظيفة:
with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]
بشكل عام ، الغرض الوحيد من هذه الوظيفة هو تقليل حجم الكود.
وأخيرًا ، "جمالنا" ...
الوظيفة التي تنفذ الشراء
1. (defn buy-lot [user_id lot_id] 2. (let [with-lot-fn (partial 3. util/with-result-or-error 4. #(lot-db/find-by-id lot_id) 5. :lot) 6. 7. buy-lot-fn (fn [{:keys [lot] :as ctx}] 8. (util/with-result-or-error 9. #(user-db/update-by-id! 10. user_id 11. (fn [user] 12. (let [wallet_v (get-in user [:wallet :value]) 13. price_v (get-in lot [:price :value])] 14. (if (>= wallet_v price_v) 15. (let [updated_user (-> user 16. (update-in [:wallet :value] 17. - 18. price_v) 19. (update-in [:lots] 20. conj 21. {:lot_id lot_id 22. :price price_v}))] 23. [:ok updated_user]) 24. [:error {:type :invalid_wallet_value 25. :details {:code :not_enough 26. :provided wallet_v 27. :required price_v}}])))) 28. :user 29. ctx)) 30. 31. fs [with-lot-fn 32. buy-lot-fn]] 33. 34. (match (util/until-first-error fs {}) 35. 36. [:ok {:user updated_user}] 37. [:ok updated_user] 38. 39. [:error reason] 40. [:error reason])))
دعنا نذهب من خلال الرمز:
- ص 34:
match
هو ماكرو لمطابقة القيم من قالب من مكتبة clojure.core.match
؛ - ص.34-40: نطبق دالة
until-first-error
على الخطوات الأولية fs
، ونأخذ البيانات التي نحتاجها من السياق ونعيدها ، أو نرفع الخطأ ؛ - ص 2-5: نحن نبني أول إجراء أولي (سيتم تطبيق السياق الحالي عليه فقط) ، والذي يضيف ببساطة البيانات على المفتاح
:lot
إلى السياق الحالي ؛ - ص.7-29: هنا نستخدم الوظيفة المألوفة
with-result-or-error
، لكن الإجراء الذي يتم الالتفاف عليه أصبح أكثر صعوبة: في معاملة واحدة ، نتحقق من أن المستخدم لديه ما يكفي من المال ، وإذا نجح ، فإننا نجري عملية شراء (لأن ، افتراضيًا ، يكون تطبيقنا متعدد مؤشرات الترابط (ومن شاهد في آخر مرة تطبيقًا واحد الخيوط؟) ويجب أن نكون مستعدين لهذا).
وبضع كلمات عن الوظائف الأخرى التي استخدمناها:
lot-db/find-by-id(id)
- تُرجع الكثير ، id
؛user-db/update-by-id!(user_id, update-user-fn)
- يطبق وظيفة update-user-fn
على user_id
المستخدم (في قاعدة بيانات وهمية).
وللاختبار؟ ...
اختبار هذا التطبيق عينة من clojure REPL. نبدأ REPL من وحدة التحكم من جذر المشروع:
lein repl
ما مستخدمينا بالأموال:
context-aware-app.core=> (context-aware-app.user.db/enumerate) [:ok ({:id "1", :name "Vasya", :wallet {:value 100}, :lots []} {:id "2", :name "Petya", :wallet {:value 100}, :lots []})]
ما لدينا الكثير (السلع):
context-aware-app.core=> (context-aware-app.lot.db/enumerate) [:ok ({:id "1", :name "Apple", :price {:value 10}} {:id "2", :name "Banana", :price {:value 20}} {:id "3", :name "Nuts", :price {:value 80}})]
تشتري Vasya تفاحة:
context-aware-app.core=>(context-aware-app.processing/buy-lot "1" "1") [:ok {:id "1", :name "Vasya", :wallet {:value 90}, :lots [{:lot_id "1", :price 10}]}]
والموز:
context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "2") [:ok {:id "1", :name "Vasya", :wallet {:value 70}, :lots [{:lot_id "1", :price 10} {:lot_id "2", :price 20}]}]
و "المكسرات":
context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "3") [:error {:type :invalid_wallet_value, :details {:code :not_enough, :provided 70, :required 80}}]
لم يكن هناك ما يكفي من المال للمكسرات.
المجموع
ونتيجة لذلك ، باستخدام البرمجة السياقية ، لن تكون هناك بعد الآن أجزاء ضخمة من التعليمات البرمجية (لا تتناسب مع شاشة واحدة) ، بالإضافة إلى "الأساليب الطويلة" و "الفئات الكبيرة" و "قوائم المعلمات الطويلة". ويعطي:
- توفير الوقت لقراءة وفهم الكود ؛
- اختبار كود مبسط
- القدرة على إعادة استخدام الكود (بما في ذلك استخدام النسخ واللصق + ملف dopilivanie) ؛
- تبسيط إعادة هيكلة الكود.
على سبيل المثال كل ما نحب ونمارسه.