قم ببناء مكونات القضبان الأنيقة باستخدام كائنات الياقوت القديمة البسيطة
نشرت: 2022-03-11يكتسب موقع الويب الخاص بك زخمًا ، وأنت تنمو بسرعة. روبي / ريلز هي لغة البرمجة المفضلة لديك. فريقك أكبر وقد تخلت عن "النماذج السمينة ، أجهزة التحكم النحيفة" كنمط تصميم لتطبيقات Rails الخاصة بك. ومع ذلك ، ما زلت لا تريد التخلي عن استخدام ريلز.
لا مشكلة. اليوم ، سنناقش كيفية استخدام أفضل ممارسات OOP لجعل الكود الخاص بك أكثر وضوحًا ، وأكثر عزلًا ، وأكثر انفصالًا.
هل التطبيق الخاص بك يستحق إعادة بيع ديون؟
لنبدأ بالنظر في كيفية تحديد ما إذا كان تطبيقك مرشحًا جيدًا لإعادة البناء.
فيما يلي قائمة بالمقاييس والأسئلة التي عادةً ما أسألها لنفسي لتحديد ما إذا كان الرمز الخاص بي يحتاج إلى إعادة بناء أم لا.
- اختبارات الوحدة البطيئة. عادةً ما تعمل اختبارات وحدة PORO بسرعة مع رمز معزول جيدًا ، لذلك يمكن أن تكون اختبارات التشغيل البطيء في كثير من الأحيان مؤشرًا على التصميم السيئ والمسؤوليات المقترنة بشكل مفرط.
- نماذج أو وحدات تحكم الدهون. يعد النموذج أو وحدة التحكم التي تحتوي على أكثر من 200 سطر من التعليمات البرمجية (LOC) بشكل عام مرشحًا جيدًا لإعادة البناء.
- قاعدة رمز كبيرة للغاية. إذا كان لديك ERB / HTML / HAML مع أكثر من 30000 LOC أو كود مصدر Ruby (بدون GEMs) مع أكثر من 50000 LOC ، فهناك فرصة جيدة لإعادة البناء.
جرب استخدام شيء مثل هذا لمعرفة عدد سطور شفرة مصدر روبي لديك:
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
سيبحث هذا الأمر في جميع الملفات بامتداد .rb (ملفات روبي) في مجلد / التطبيق ، ويطبع عدد الأسطر. يرجى ملاحظة أن هذا الرقم تقريبي فقط حيث سيتم تضمين أسطر التعليق في هذه الإجماليات.
هناك خيار آخر أكثر دقة وأكثر إفادة وهو استخدام stats
مهمة Rails التي تنتج ملخصًا سريعًا لأسطر الكود وعدد الفئات وعدد الأساليب ونسبة الأساليب إلى الفئات ونسبة سطور التعليمات البرمجية لكل طريقة:
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- هل يمكنني استخراج الأنماط المتكررة في قاعدة التعليمات البرمجية الخاصة بي؟
الفصل في العمل
لنبدأ بمثال من العالم الحقيقي.
نتظاهر بأننا نريد كتابة تطبيق يتتبع وقت الركض. في الصفحة الرئيسية ، يمكن للمستخدم رؤية الأوقات التي دخلوا فيها.
يحتوي كل إدخال للوقت على التاريخ والمسافة والمدة ومعلومات "الحالة" الإضافية ذات الصلة (مثل الطقس ونوع التضاريس وما إلى ذلك) ومتوسط السرعة التي يمكن حسابها عند الحاجة.
نحتاج إلى صفحة تقرير تعرض متوسط السرعة والمسافة في الأسبوع.
إذا كان متوسط سرعة الإدخال أعلى من متوسط السرعة الإجمالية ، فسنقوم بإخطار المستخدم برسالة SMS (على سبيل المثال ، سنستخدم Nexmo RESTful API لإرسال الرسائل القصيرة).
ستسمح لك الصفحة الرئيسية بتحديد المسافة والتاريخ والوقت الذي تقضيه في الركض لإنشاء إدخال مشابه لهذا:
لدينا أيضًا صفحة statistics
وهي في الأساس تقرير أسبوعي يتضمن متوسط السرعة والمسافة المقطوعة في الأسبوع.
- يمكنك التحقق من العينة عبر الإنترنت هنا.
الرمز
تبدو بنية دليل app
كما يلي:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
لن أناقش نموذج User
لأنه ليس شيئًا مميزًا لأننا نستخدمه مع Devise لتنفيذ المصادقة.
أما بالنسبة لنموذج Entry
، فهو يحتوي على منطق الأعمال لتطبيقنا.
كل Entry
ينتمي إلى User
.
نتحقق من وجود سمات distance
، time_period
، date_time
status
لكل إدخال.
في كل مرة نقوم فيها بإنشاء إدخال ، نقارن متوسط سرعة المستخدم بمتوسط جميع المستخدمين الآخرين في النظام ، ونبلغ المستخدم عن طريق الرسائل القصيرة باستخدام Nexmo (لن نناقش كيفية استخدام مكتبة Nexmo ، على الرغم من أنني أردت لشرح حالة نستخدم فيها مكتبة خارجية).
- عينة من الجوهر
لاحظ أن نموذج Entry
يحتوي على أكثر من منطق الأعمال وحده. كما أنه يتعامل مع بعض عمليات التحقق من الصحة وعمليات الاسترجاعات.
يحتوي entries_controller.rb
على إجراءات CRUD الرئيسية (لا يوجد تحديث على الرغم من ذلك). يحصل EntriesController#index
على إدخالات المستخدم الحالي ويطلب السجلات حسب تاريخ الإنشاء ، بينما ينشئ EntriesController#create
إدخال جديد. لا حاجة لمناقشة ما هو واضح ومسؤوليات EntriesController#destroy
:
- عينة من الجوهر
في حين أن statistics_controller.rb
هي المسؤولة عن حساب التقرير الأسبوعي ، فإن StatisticsController#index
يحصل على إدخالات المستخدم الذي قام بتسجيل الدخول ويقوم بتجميعها حسب الأسبوع ، باستخدام طريقة #group_by
الموجودة في فئة Enumerable في Rails. ثم يحاول تزيين النتائج ببعض الطرق الخاصة.
- عينة من الجوهر
نحن لا نناقش وجهات النظر كثيرًا هنا ، لأن شفرة المصدر تشرح نفسها بنفسها.
يوجد أدناه عرض لسرد إدخالات المستخدم الذي قام بتسجيل الدخول ( index.html.erb
). هذا هو القالب الذي سيتم استخدامه لعرض نتائج إجراء الفهرس (الطريقة) في وحدة تحكم الإدخالات:
- عينة من الجوهر
لاحظ أننا نستخدم render @entries
، لسحب الكود المشترك إلى قالب جزئي _entry.html.erb
حتى نتمكن من الاحتفاظ بكودنا جافًا وقابل لإعادة الاستخدام:
- عينة من الجوهر
الشيء نفسه ينطبق على _form
الجزئية. بدلاً من استخدام نفس الكود مع إجراءات (جديدة وتحرير) ، نقوم بإنشاء نموذج جزئي قابل لإعادة الاستخدام:
- عينة من الجوهر
بالنسبة لعرض صفحة التقرير الأسبوعي ، فإن statistics/index.html.erb
تظهر بعض الإحصائيات وتقارير الأداء الأسبوعي للمستخدم من خلال تجميع بعض الإدخالات:
- عينة من الجوهر
وأخيرًا ، يشتمل المساعد الخاص بالإدخالات ، entries_helper.rb
، على مساعدين قابلين للقراءة_ time_period و readable_time_period
مما يجعل السمات أكثر قابلية readable_speed
بطريقة إنسانية:
- عينة من الجوهر
لا شيء يتوهم حتى الآن.
سوف يجادل معظمكم في إعادة هيكلة هذا الأمر ضد مبدأ KISS وسيجعل النظام أكثر تعقيدًا.
هل يحتاج هذا التطبيق حقًا إلى إعادة بناء ديون؟
بالتأكيد لا ، لكننا سننظر في الأمر لأغراض توضيحية فقط.
بعد كل شيء ، إذا قمت بمراجعة القسم السابق ، والخصائص التي تشير إلى أن التطبيق يحتاج إلى إعادة بناء ، يصبح من الواضح أن التطبيق في مثالنا ليس مرشحًا صالحًا لإعادة البناء.
دورة الحياة
لنبدأ بشرح هيكل نمط Rails MVC.
عادة ، يبدأ من خلال قيام المتصفح بتقديم طلب ، مثل https://www.toptal.com/jogging/show/1
.
يتلقى خادم الويب الطلب ويستخدم routes
لمعرفة controller
التي يجب استخدامها.
تقوم وحدات التحكم بعمل تحليل طلبات المستخدم ، وعمليات إرسال البيانات ، وملفات تعريف الارتباط ، والجلسات ، وما إلى ذلك ، ثم تطلب من model
الحصول على البيانات.
models
عبارة عن فئات Ruby التي تتحدث إلى قاعدة البيانات ، وتخزن البيانات وتتحقق من صحتها ، وتنفذ منطق الأعمال ، وبخلاف ذلك تقوم بالأعباء الثقيلة. المشاهدات هي ما يراه المستخدم: HTML و CSS و XML و Javascript و JSON.
إذا أردنا إظهار تسلسل دورة حياة طلب ريلز ، فسيبدو مثل هذا:
ما أريد تحقيقه هو إضافة المزيد من التجريد باستخدام كائنات الياقوت القديمة العادية (POROs) وجعل النمط مشابهًا لما يلي لإجراءات create/update
:
وشيء من هذا القبيل بالنسبة لإجراءات list/show
:
من خلال إضافة تجريدات POROs ، سنضمن الفصل الكامل بين المسؤوليات SRP ، وهو أمر لا يجيده ريلز.
القواعد الارشادية
لتحقيق التصميم الجديد ، سأستخدم الإرشادات المذكورة أدناه ، ولكن يرجى ملاحظة أن هذه ليست قواعد يجب عليك اتباعها إلى T. فكر فيها على أنها إرشادات مرنة تجعل إعادة البناء أسهل.
- يمكن أن تحتوي نماذج ActiveRecord على ارتباطات وثوابت ، ولكن لا شيء آخر. يعني ذلك عدم وجود عمليات رد نداء (استخدم كائنات الخدمة وأضف عمليات الاسترجاعات هناك) ولا عمليات تحقق من الصحة (استخدم كائنات النموذج لتضمين التسمية والتحقق من صحة النموذج).
- احتفظ بوحدات التحكم على شكل طبقات رفيعة واستدعِ دائمًا كائنات الخدمة. قد يتساءل البعض منكم عن سبب استخدام وحدات التحكم على الإطلاق لأننا نريد الاستمرار في استدعاء كائنات الخدمة لاحتواء المنطق؟ حسنًا ، تعد وحدات التحكم مكانًا جيدًا لتوجيه HTTP ، وتحليل المعلمات ، والمصادقة ، والتفاوض على المحتوى ، واستدعاء الخدمة المناسبة أو كائن المحرر ، والتقاط الاستثناءات ، وتنسيق الاستجابة ، وإرجاع رمز حالة HTTP الصحيح.
- يجب أن تستدعي الخدمات كائنات الاستعلام ، ويجب ألا تخزن الحالة. استخدم طرق المثيل ، وليس طرق الفصل. يجب أن يكون هناك عدد قليل جدًا من الأساليب العامة التي تتماشى مع SRP.
- يجب أن تتم الاستعلامات في كائنات الاستعلام. يجب أن تُرجع أساليب كائن الاستعلام كائنًا ، أو تجزئة أو مصفوفة ، وليس اقتران ActiveRecord.
- تجنب استخدام المساعدين واستخدم الزينة بدلاً من ذلك. لماذا ا؟ من الأخطاء الشائعة مع مساعدي ريلز أنه يمكنهم التحول إلى كومة كبيرة من الوظائف غير المرتبطة بالوظائف ، وكلها تتشارك في مساحة اسم وتتقدم على بعضها البعض. لكن الأسوأ من ذلك أنه لا توجد طريقة رائعة لاستخدام أي نوع من تعدد الأشكال مع مساعدي ريلز - توفير تطبيقات مختلفة لسياقات أو أنواع مختلفة ، أو مساعدين من الدرجة الأولى أو مفرطة. أعتقد أنه يجب استخدام فئات مساعد ريلز بشكل عام لطرق المنفعة ، وليس لحالات استخدام محددة ، مثل سمات نموذج التنسيق لأي نوع من منطق العرض. اجعلها خفيفة ومنسم.
- تجنب استخدام المخاوف واستخدم الديكور / المندوبين بدلاً من ذلك. لماذا ا؟ بعد كل شيء ، يبدو أن المخاوف هي جزء أساسي من ريلز ويمكنها تجفيف الكود عند مشاركتها بين نماذج متعددة. ومع ذلك ، فإن القضية الرئيسية هي أن المخاوف لا تجعل كائن النموذج أكثر تماسكًا. الكود منظم بشكل أفضل. بمعنى آخر ، لا يوجد تغيير حقيقي في واجهة برمجة التطبيقات الخاصة بالنموذج.
- حاول استخراج كائنات القيمة من النماذج للحفاظ على نظافة التعليمات البرمجية الخاصة بك ولتجميع السمات ذات الصلة.
- قم دائمًا بتمرير متغير مثيل واحد لكل عرض.
إعادة بناء التعليمات البرمجية
قبل أن نبدأ ، أود مناقشة أمر آخر. عندما تبدأ في إعادة الهيكلة ، ينتهي بك الأمر عادةً إلى أن تسأل نفسك: "هل هذه إعادة بناء ديون جيدة حقًا؟"
إذا كنت تشعر أنك تقوم بمزيد من الفصل أو العزل بين المسؤوليات (حتى لو كان ذلك يعني إضافة المزيد من التعليمات البرمجية والملفات الجديدة) ، فهذا أمر جيد عادةً. بعد كل شيء ، يعد فصل التطبيق ممارسة جيدة جدًا ويسهل علينا إجراء اختبار الوحدة المناسب.
لن أناقش الأشياء ، مثل نقل المنطق من وحدات التحكم إلى النماذج ، حيث أفترض أنك تقوم بذلك بالفعل ، وأنت مرتاح لاستخدام Rails (عادةً ما يكون Skinny Controller و FAT model).
من أجل الحفاظ على هذه المقالة ضيقة ، لن أناقش الاختبار هنا ، لكن هذا لا يعني أنه لا يجب عليك الاختبار.
على العكس من ذلك ، يجب أن تبدأ دائمًا باختبار للتأكد من أن الأمور على ما يرام قبل المضي قدمًا. هذا أمر لا بد منه ، خاصة عند إعادة بناء ديون.
ثم يمكننا تنفيذ التغييرات والتأكد من اجتياز جميع الاختبارات للأجزاء ذات الصلة من الكود.
استخراج كائنات القيمة
أولا ، ما هو كائن القيمة؟
يوضح مارتن فاولر:
كائن القيمة هو كائن صغير ، مثل المال أو كائن نطاق التاريخ. الخاصية الأساسية الخاصة بهم هي أنهم يتبعون دلالات القيمة بدلاً من الدلالات المرجعية.
في بعض الأحيان قد تواجه موقفًا يستحق فيه المفهوم تجريدًا خاصًا به ولا تستند مساواته إلى القيمة ، بل على الهوية. من الأمثلة على ذلك Ruby's Date و URI و Pathname. يعد الاستخراج إلى كائن ذي قيمة (أو نموذج مجال) وسيلة راحة كبيرة.
لماذا تهتم؟
واحدة من أكبر مزايا كائن القيمة هي التعبير الذي يساعد في تحقيقه في التعليمات البرمجية الخاصة بك. ستكون شفرتك أكثر وضوحًا ، أو على الأقل يمكن أن تكون إذا كانت لديك ممارسات تسمية جيدة. نظرًا لأن كائن القيمة هو تجريد ، فإنه يؤدي إلى رمز أوضح وأخطاء أقل.
فوز كبير آخر هو الثبات. ثبات الأشياء مهم جدا. عندما نقوم بتخزين مجموعات معينة من البيانات ، والتي يمكن استخدامها في كائن قيم ، فأنا عادة لا أرغب في معالجة هذه البيانات.
متى يكون هذا مفيدا؟
لا توجد إجابة واحدة تناسب الجميع. افعل ما هو أفضل بالنسبة لك وما هو منطقي في أي موقف معين.
بعد ذلك ، هناك بعض الإرشادات التي أستخدمها لمساعدتي في اتخاذ هذا القرار.
إذا كنت تعتقد أن مجموعة من الأساليب مرتبطة ، فستكون أكثر تعبيرًا مع كائنات القيمة. يعني هذا التعبير أن كائن القيمة يجب أن يمثل مجموعة مميزة من البيانات ، والتي يمكن لمطورك العادي أن يستنتجها ببساطة من خلال النظر إلى اسم الكائن.
كيف يتم ذلك؟
يجب أن تتبع كائنات القيمة بعض القواعد الأساسية:
- يجب أن تحتوي كائنات القيمة على سمات متعددة.
- يجب أن تكون السمات ثابتة طوال دورة حياة الكائن.
- يتم تحديد المساواة من خلال سمات الكائن.
في مثالنا ، سوف أقوم بإنشاء كائن قيمة EntryStatus
لاستخراج سمات Entry#status_weather
و Entry#status_landform
الخاصة ، والتي تبدو كالتالي:
- عينة من الجوهر
ملاحظة: هذا مجرد كائن روبي قديم عادي (PORO) لا يرث من ActiveRecord::Base
. لقد حددنا أساليب القارئ لسماتنا وقمنا بتعيينها عند التهيئة. استخدمنا أيضًا مزيجًا مشابهًا لمقارنة الكائنات باستخدام طريقة (<=>).
يمكننا تعديل نموذج Entry
لاستخدام كائن القيمة الذي أنشأناه:
- عينة من الجوهر
يمكننا أيضًا تعديل طريقة EntryController#create
لاستخدام كائن القيمة الجديد وفقًا لذلك:
- عينة من الجوهر
استخراج كائنات الخدمة
إذن ما هو كائن الخدمة؟
تتمثل مهمة كائن الخدمة في الاحتفاظ بالكود لجزء معين من منطق الأعمال. على عكس نمط "نموذج الدهون" ، حيث يحتوي عدد قليل من الكائنات على العديد والعديد من الطرق لجميع المنطق الضروري ، فإن استخدام كائنات الخدمة ينتج عنه العديد من الفئات ، كل منها يخدم غرضًا واحدًا.

لماذا ا؟ ما هي المنافع؟
- فصل. تساعدك كائنات الخدمة في تحقيق مزيد من العزلة بين الكائنات.
- الرؤية. تُظهر كائنات الخدمة (إذا سميت جيدًا) ما يفعله التطبيق. يمكنني فقط إلقاء نظرة على دليل الخدمات لمعرفة الإمكانات التي يوفرها التطبيق.
- نماذج التنظيف وأجهزة التحكم. يقوم المتحكمون بتحويل الطلب (المعلمات ، الجلسة ، ملفات تعريف الارتباط) إلى وسيطات ، ويمررها إلى الخدمة ويعيد التوجيه أو العرض وفقًا لاستجابة الخدمة. بينما تتعامل النماذج فقط مع الجمعيات ، والمثابرة. من شأن استخراج الكود من وحدات التحكم / النماذج إلى كائنات الخدمة أن يدعم SRP ويجعل الكود أكثر فصلًا. عندئذٍ ستكون مسؤولية النموذج هي فقط التعامل مع الجمعيات وحفظ / حذف السجلات ، في حين أن كائن الخدمة سيكون له مسؤولية واحدة (SRP). هذا يؤدي إلى تصميم أفضل واختبارات وحدة أفضل.
- جاف وتقبل التغيير. أبقي كائنات الخدمة بسيطة وصغيرة قدر الإمكان. أقوم بتكوين كائنات خدمة مع كائنات خدمة أخرى ، وأعيد استخدامها.
- تنظيف وتسريع مجموعة الاختبار الخاصة بك. الخدمات سهلة وسريعة للاختبار لأنها كائنات صغيرة من Ruby لها نقطة دخول واحدة (طريقة الاستدعاء). تتكون الخدمات المعقدة من خدمات أخرى ، بحيث يمكنك تقسيم اختباراتك بسهولة. كذلك ، فإن استخدام كائنات الخدمة يجعل من السهل محاكاة / كعب الكائنات ذات الصلة دون الحاجة إلى تحميل بيئة القضبان بأكملها.
- يمكن الاستدعاء من أي مكان. من المحتمل أن يتم استدعاء كائنات الخدمة من وحدات التحكم بالإضافة إلى كائنات الخدمة الأخرى ، DelayedJob / Rescue / Sidekiq Jobs ، مهام Rake ، وحدة التحكم ، إلخ.
من ناحية أخرى ، لا يوجد شيء مثالي على الإطلاق. من عيوب كائنات الخدمة أنها يمكن أن تكون مبالغة في القيام بعمل بسيط للغاية. في مثل هذه الحالات ، قد ينتهي بك الأمر إلى تعقيد شفرتك بدلاً من تبسيطها.
متى يجب استخراج كائنات الخدمة؟
لا توجد قاعدة صارمة وسريعة هنا أيضًا.
عادةً ما تكون كائنات الخدمة أفضل للأنظمة المتوسطة إلى الكبيرة ؛ أولئك الذين لديهم قدر معقول من المنطق يتجاوز عمليات CRUD القياسية.
لذلك عندما تعتقد أن مقتطف الشفرة قد لا ينتمي إلى الدليل الذي ستضيفه إليه ، فمن الأفضل إعادة النظر فيه ومعرفة ما إذا كان يجب أن ينتقل إلى كائن خدمة بدلاً من ذلك.
فيما يلي بعض المؤشرات حول وقت استخدام كائنات الخدمة:
- العمل معقد.
- يصل الإجراء عبر نماذج متعددة.
- يتفاعل الإجراء مع خدمة خارجية.
- الإجراء ليس مصدر قلق أساسي للنموذج الأساسي.
- هناك طرق متعددة لأداء العمل.
كيف يجب أن تصمم كائنات الخدمة؟
يعد تصميم الفصل لعنصر الخدمة أمرًا سهلاً نسبيًا ، نظرًا لأنك لا تحتاج إلى جواهر خاصة ، ولا يتعين عليك تعلم DSL جديد ، ويمكنك الاعتماد بشكل أو بآخر على مهارات تصميم البرامج التي تمتلكها بالفعل.
عادةً ما أستخدم الإرشادات والاصطلاحات التالية لتصميم كائن الخدمة:
- لا تخزن حالة الكائن.
- استخدم طرق المثيل ، وليس طرق الفصل.
- يجب أن يكون هناك عدد قليل جدًا من الطرق العامة (يفضل واحدة لدعم SRP.
- يجب أن تعرض الطرق كائنات النتائج المنسقة وليس القيم المنطقية.
- تندرج الخدمات ضمن دليل
app/services
. أنا أشجعك على استخدام الدلائل الفرعية لمجالات ثقيلة منطق الأعمال. على سبيل المثال ، سيقوم ملفapp/services/report/generate_weekly.rb
/ report / create_weekly.rb بتعريفReport::GenerateWeekly
بينماapp/services/report/publish_monthly.rb
Report::PublishMonthly
. - تبدأ الخدمات بفعل (ولا تنتهي بالخدمة):
ApproveTransaction
وSendTestNewsletter
وImportUsersFromCsv
. - تستجيب الخدمات لطريقة الاتصال. اكتشفت أن استخدام فعل آخر يجعله زائدًا بعض الشيء: () ApproveTransaction.approve لا يقرأ جيدًا. أيضًا ، طريقة الاستدعاء هي طريقة فعلية لكائنات lambda و procs والأسلوب.
إذا ألقيت نظرة على StatisticsController#index
، ستلاحظ مجموعة من الأساليب (من الأسابيع إلى weeks_to_date_from
، weeks_to_date_to
، ومتوسط avg_distance
، وما إلى ذلك) مقترنة بوحدة التحكم. هذا ليس جيدًا حقًا. ضع في اعتبارك العواقب إذا كنت ترغب في إنشاء التقرير الأسبوعي خارج statistics_controller
.
في حالتنا ، لنقم بإنشاء Report::GenerateWeekly
واستخراج منطق التقرير من StatisticsController
:
- عينة من الجوهر
لذا يبدو StatisticsController#index
أكثر نظافة:
- عينة من الجوهر
من خلال تطبيق نمط كائن الخدمة ، نجمع الكود حول إجراء محدد ومعقد ونشجع على إنشاء طرق أصغر وأكثر وضوحًا.
الواجب المنزلي: ضع في اعتبارك استخدام كائن القيمة WeeklyReport
بدلاً من Struct
.
استخراج كائنات الاستعلام من وحدات التحكم
ما هو كائن الاستعلام؟
كائن الاستعلام هو PORO الذي يمثل استعلام قاعدة البيانات. يمكن إعادة استخدامه عبر أماكن مختلفة في التطبيق مع إخفاء منطق الاستعلام في نفس الوقت. كما أنه يوفر وحدة معزولة جيدة للاختبار.
يجب عليك استخراج استعلامات SQL / NoSQL المعقدة في فئتها الخاصة.
يكون كل كائن استعلام مسؤولاً عن إرجاع مجموعة نتائج بناءً على المعايير / قواعد العمل.
في هذا المثال ، ليس لدينا أي استعلامات معقدة ، لذا فإن استخدام كائن الاستعلام لن يكون فعالاً. ومع ذلك ، لغرض العرض التوضيحي ، دعنا نستخرج الاستعلام في Report::GenerateWeekly#call
وأنشئ generate_entries_query.rb
:
- عينة من الجوهر
وفي Report::GenerateWeekly#call
، فلنستبدل:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
مع:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
يساعد نمط كائن الاستعلام في الحفاظ على منطق النموذج الخاص بك مرتبطًا بشكل صارم بسلوك الفصل ، مع الحفاظ أيضًا على وحدات التحكم الخاصة بك ضئيلة. نظرًا لأنها ليست أكثر من فئات Ruby القديمة العادية ، فإن كائنات الاستعلام لا تحتاج إلى أن ترث من ActiveRecord::Base
، ويجب ألا تكون مسؤولة عن أكثر من تنفيذ الاستعلامات.
استخراج إنشاء إدخال إلى كائن الخدمة
الآن ، دعنا نستخرج منطق إنشاء إدخال جديد إلى كائن خدمة جديد. دعنا نستخدم الاصطلاح CreateEntry
:
- عينة من الجوهر
والآن لدينا EntriesController#create
على النحو التالي:
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
نقل عمليات التحقق من الصحة إلى كائن النموذج
الآن ، هنا تبدأ الأمور في أن تصبح أكثر إثارة للاهتمام.
تذكر في إرشاداتنا ، أننا اتفقنا على أننا أردنا أن تحتوي النماذج على ارتباطات وثوابت ، ولكن لا شيء آخر (لا عمليات التحقق من الصحة ولا عمليات الاسترجاعات). لذلك لنبدأ بإزالة عمليات الاسترجاعات ، واستخدام كائن Form بدلاً من ذلك.
كائن النموذج هو كائن روبي قديم عادي (PORO). يتولى من وحدة التحكم / كائن الخدمة أينما يحتاج إلى التحدث إلى قاعدة البيانات.
لماذا استخدام كائنات النموذج؟
عند البحث عن إعادة تشكيل تطبيقك ، من الأفضل دائمًا مراعاة مبدأ المسؤولية الفردية (SRP).
يساعدك SRP على اتخاذ قرارات تصميم أفضل حول ما يجب أن يكون الفصل مسؤولاً عنه.
نموذج جدول قاعدة البيانات (نموذج ActiveRecord في سياق ريلز) ، على سبيل المثال ، يمثل سجل قاعدة بيانات واحد في التعليمات البرمجية ، لذلك لا يوجد سبب يدعو إلى القلق بشأن أي شيء يقوم به المستخدم.
هذا هو المكان الذي تأتي فيه كائنات النموذج.
كائن النموذج مسؤول عن تمثيل نموذج في طلبك. لذلك يمكن معاملة كل حقل إدخال كسمة في الفصل. يمكنه التحقق من أن هذه السمات تلبي بعض قواعد التحقق ، ويمكنه تمرير البيانات "النظيفة" إلى المكان الذي تريد الذهاب إليه (على سبيل المثال ، نماذج قاعدة البيانات الخاصة بك أو ربما منشئ استعلام البحث الخاص بك).
متى يجب استخدام كائن النموذج؟
- عندما تريد استخراج عمليات التحقق من نماذج ريلز.
- عندما يمكن تحديث عدة نماذج من خلال إرسال نموذج واحد ، فقد ترغب في إنشاء كائن نموذج.
يمكّنك هذا من وضع كل منطق النموذج (اصطلاحات التسمية وعمليات التحقق وما إلى ذلك) في مكان واحد.
كيف تقوم بإنشاء كائن نموذج؟
- قم بإنشاء فصل دراسي عادي.
- قم بتضمين
ActiveModel::Model
(في ريلز 3 ، يجب عليك تضمين التسمية والتحويل والتحقق من الصحة بدلاً من ذلك) - ابدأ في استخدام فئة النموذج الجديدة كما لو كانت نموذجًا عاديًا لـ ActiveRecord ، والفرق الأكبر هو أنه لا يمكنك الاستمرار في البيانات المخزنة في هذا الكائن.
يرجى ملاحظة أنه يمكنك استخدام جوهرة الإصلاح ، ولكن بالتمسك بـ POROs entry_form.rb
الذي يبدو كالتالي:
- عينة من الجوهر
وسنقوم بتعديل CreateEntry
للبدء في استخدام نموذج كائن النموذج EntryForm
:
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
ملاحظة: قد يقول البعض منكم أنه ليست هناك حاجة للوصول إلى كائن النموذج من كائن الخدمة وأنه يمكننا فقط استدعاء كائن النموذج مباشرة من وحدة التحكم ، وهي وسيطة صالحة. ومع ذلك ، أفضل الحصول على تدفق واضح ، ولهذا السبب أستدعي دائمًا كائن النموذج من كائن الخدمة.
نقل عمليات الاسترجاعات إلى كائن الخدمة
كما اتفقنا سابقًا ، لا نريد أن تحتوي نماذجنا على عمليات التحقق من الصحة وعمليات رد النداء. استخرجنا عمليات التحقق باستخدام كائنات النموذج. لكننا ما زلنا نستخدم بعض عمليات الاسترجاعات ( after_create
في نموذج Entry
compare_speed_and_notify_user
).
لماذا نريد إزالة عمليات الاسترجاعات من النماذج؟
يبدأ مطورو ريلز عادةً في ملاحظة ألم رد الاتصال أثناء الاختبار. إذا كنت لا تختبر نماذج ActiveRecord الخاصة بك ، فستبدأ في ملاحظة الألم لاحقًا مع نمو التطبيق الخاص بك وبما أن هناك حاجة إلى مزيد من المنطق للاتصال أو تجنب معاودة الاتصال.
تُستخدم عمليات رد after_*
بشكل أساسي فيما يتعلق بحفظ الكائن أو استمراره.
بمجرد حفظ الكائن ، يكون الغرض (أي المسؤولية) للكائن قد تم تحقيقه. لذلك إذا كنا لا نزال نرى استدعاء عمليات الاسترجاعات بعد حفظ الكائن ، فإن ما نراه على الأرجح هو عمليات الاسترجاعات التي تصل إلى خارج منطقة مسؤولية الكائن ، وذلك عندما نواجه مشاكل.
في حالتنا ، نرسل رسالة نصية قصيرة إلى المستخدم بعد أن نحفظ إدخالاً ، وهو أمر لا يتعلق حقًا بمجال الإدخال.
هناك طريقة بسيطة لحل المشكلة عن طريق نقل رد الاتصال إلى كائن الخدمة ذي الصلة. بعد كل شيء ، يرتبط إرسال رسالة SMS للمستخدم النهائي بكائن خدمة CreateEntry
وليس بنموذج Entry نفسه.
عند القيام بذلك ، لم نعد مضطرًا إلى استبعاد طريقة compare_speed_and_notify_user
في اختباراتنا. لقد جعلنا الأمر بسيطًا لإنشاء إدخال دون الحاجة إلى إرسال رسالة نصية قصيرة ، ونحن نتبع التصميم الجيد الموجه للكائنات من خلال التأكد من أن فصولنا تتحمل مسؤولية واحدة (SRP).
حتى الآن يبدو CreateEntry
بنا على النحو التالي:
- عينة من الجوهر
استخدم أدوات الزينة بدلاً من المساعدين
بينما يمكننا بسهولة استخدام مجموعة Draper لنماذج العرض والديكورات ، سألتزم بـ POROs من أجل هذه المقالة ، كما كنت أفعل حتى الآن.
ما أحتاجه هو فئة تستدعي عمليات على الكائن المزين.
يمكنني استخدام method_missing
لتنفيذ ذلك ، لكنني سأستخدم مكتبة روبي القياسية SimpleDelegator
.
يوضح الكود التالي كيفية استخدام SimpleDelegator
لتنفيذ الديكور الأساسي الخاص بنا:
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
فلماذا طريقة _h
؟
تعمل هذه الطريقة كوكيل لسياق العرض. بشكل افتراضي ، سياق العرض هو مثيل لفئة العرض ، فئة العرض الافتراضية هي ActionView::Base
. يمكنك الوصول إلى عرض المساعدين على النحو التالي:
_h.content_tag :div, 'my-div', class: 'my-class'
لجعلها أكثر ملاءمة ، أضفنا طريقة decorate
إلى ApplicationHelper
:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
الآن ، يمكننا نقل مساعدي EntriesHelper
إلى مصممي الديكور:
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
ويمكننا استخدام readable_time_period
و readable_speed
مثل:
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
الهيكل بعد إعادة البناء
انتهى بنا الأمر بمزيد من الملفات ، لكن هذا ليس بالضرورة أمرًا سيئًا (وتذكر أنه منذ البداية ، أقرنا بأن هذا المثال كان لأغراض توضيحية فقط ولم يكن بالضرورة حالة استخدام جيدة لإعادة البناء):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
خاتمة
على الرغم من أننا ركزنا على ريلز في منشور المدونة هذا ، فإن RoR لا يعتمد على كائنات الخدمة الموصوفة وغيرها من POROs. يمكنك استخدام هذا الأسلوب مع أي إطار ويب أو تطبيق جوال أو وحدة تحكم.
باستخدام MVC كبنية لتطبيقات الويب ، يظل كل شيء مقترنًا ويجعلك تسير بشكل أبطأ لأن معظم التغييرات لها تأثير على أجزاء أخرى من التطبيق. أيضًا ، يجبرك على التفكير في مكان وضع بعض منطق الأعمال - هل يجب أن يدخل في النموذج ، أو وحدة التحكم ، أو العرض؟
باستخدام POROs البسيطة ، قمنا بنقل منطق الأعمال إلى النماذج أو الخدمات التي لا ترث من ActiveRecord
، والتي تعد بالفعل فوزًا كبيرًا ، ناهيك عن أن لدينا رمزًا أكثر وضوحًا يدعم SRP واختبارات الوحدة الأسرع.
تهدف البنية النظيفة إلى وضع حالات الاستخدام في منتصف / أعلى الهيكل الخاص بك ، حتى تتمكن من رؤية ما يفعله تطبيقك بسهولة. كما أنه يجعل من السهل تبني التغييرات لأنها أكثر نمطية ومعزولة.
آمل أن أشرح كيف أن استخدام كائنات روبي القديمة البسيطة والمزيد من الأفكار التجريدية يفصل بين المخاوف ويبسط الاختبار ويساعد في إنتاج كود نظيف وقابل للصيانة.