كائنات خدمة ريلز: دليل شامل
نشرت: 2022-03-11يأتي Ruby on Rails مع كل ما تحتاجه لإنشاء نموذج أولي لتطبيقك بسرعة ، ولكن عندما يبدأ قاعدة التعليمات البرمجية الخاصة بك في النمو ، ستواجه سيناريوهات حيث ينكسر شعار Fat Model التقليدي ، Skinny Controller. عندما لا يمكن أن يتلاءم منطق عملك مع نموذج أو وحدة تحكم ، عندها تأتي كائنات الخدمة وتسمح لنا بفصل كل إجراء عمل إلى كائن Ruby الخاص به.
في هذه المقالة ، سأشرح متى يكون كائن الخدمة مطلوبًا ؛ كيفية الشروع في كتابة كائنات الخدمة النظيفة وتجميعها معًا من أجل سلامة المساهم ؛ القواعد الصارمة التي أفرضها على كائنات الخدمة لربطها مباشرةً بمنطق عملي ؛ وكيف لا تحول كائنات خدمتك إلى أرض تفريغ لجميع الأكواد التي لا تعرف ماذا تفعل بها.
لماذا أحتاج كائنات الخدمة؟
جرب هذا: ماذا تفعل عندما يحتاج التطبيق الخاص بك إلى تغريدة النص من params[:message]
؟
إذا كنت تستخدم Vanilla Rails حتى الآن ، فمن المحتمل أنك فعلت شيئًا كهذا:
class TweetController < ApplicationController def create send_tweet(params[:message]) end private def send_tweet(tweet) client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(tweet) end end
تكمن المشكلة هنا في أنك أضفت ما لا يقل عن عشرة أسطر إلى وحدة التحكم الخاصة بك ، لكنها لا تنتمي إليها حقًا. أيضًا ، ماذا لو أردت استخدام نفس الوظيفة في وحدة تحكم أخرى؟ هل تنقل هذا إلى مصدر قلق؟ انتظر ، لكن هذا الرمز لا ينتمي حقًا إلى وحدات التحكم على الإطلاق. لماذا لا يمكن أن تأتي واجهة برمجة تطبيقات Twitter بكائن واحد جاهز لكي أتصل به؟
في المرة الأولى التي قمت فيها بهذا ، شعرت وكأنني فعلت شيئًا قذرًا. في السابق ، بدأت وحدات التحكم في ريلز الخالية من الدهون بشكل جميل في الحصول على الدهون ولم أكن أعرف ماذا أفعل. في النهاية ، أصلحت وحدة التحكم الخاصة بي باستخدام كائن خدمة.
قبل أن تبدأ في قراءة هذا المقال ، دعنا نتظاهر:
- يتعامل هذا التطبيق مع حساب Twitter.
- The Rails Way تعني "طريقة Ruby on Rails التقليدية في فعل الأشياء" والكتاب غير موجود.
- أنا خبير في ريلز ... وهو ما يتم إخباري به كل يوم ، ولكن أجد صعوبة في تصديق ذلك ، لذلك دعونا نتظاهر بأنني واحد بالفعل.
ما هي كائنات الخدمة؟
كائنات الخدمة هي كائنات روبي القديمة البسيطة (PORO) المصممة لتنفيذ إجراء واحد في منطق المجال الخاص بك والقيام بذلك بشكل جيد. ضع في اعتبارك المثال أعلاه: طريقتنا لديها بالفعل منطق للقيام بشيء واحد ، وهو إنشاء تغريدة. ماذا لو تم تغليف هذا المنطق داخل فئة روبي واحدة يمكننا إنشاء مثيل لها واستدعاء طريقة لها؟ شيء مثل:
tweet_creator = TweetCreator.new(params[:message]) tweet_creator.send_tweet # Later on in the article, we'll add syntactic sugar and shorten the above to: TweetCreator.call(params[:message])
هذا هو الى حد كبير ذلك؛ بمجرد إنشائه ، يمكن استدعاء كائن خدمة TweetCreator
بنا من أي مكان ، وسيفعل هذا الشيء بشكل جيد للغاية.
إنشاء كائن خدمة
لنقم أولاً بإنشاء TweetCreator
جديد في مجلد جديد يسمى app/services
:
$ mkdir app/services && touch app/services/tweet_creator.rb
ودعونا نتخلص من كل منطقنا داخل فئة روبي جديدة:
# app/services/tweet_creator.rb class TweetCreator def initialize(message) @message = message end def send_tweet client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
ثم يمكنك الاتصال TweetCreator.new(params[:message]).send_tweet
في أي مكان في تطبيقك ، وسيعمل. ستقوم ريلز بتحميل هذا الكائن بطريقة سحرية لأنه يقوم بتحميل كل شيء تلقائيًا ضمن app/
. تحقق من ذلك عن طريق تشغيل:
$ rails c Running via Spring preloader in process 12417 Loading development environment (Rails 5.1.5) > puts ActiveSupport::Dependencies.autoload_paths ... /Users/gilani/Sandbox/nazdeeq/app/services
هل تريد معرفة المزيد حول كيفية عمل autoload
؟ اقرأ دليل ثوابت التحميل التلقائي وإعادة التحميل.
إضافة السكر النحوي لجعل كائنات خدمة القضبان تمتص بشكل أقل
انظر ، هذا شعور رائع من الناحية النظرية ، لكن TweetCreator.new(params[:message]).send_tweet
مجرد كلام. إنه مطول للغاية مع وجود كلمات زائدة عن الحاجة ... يشبه إلى حد كبير HTML (نسيج ba-dum ! ). لكن بكل جدية ، لماذا يستخدم الناس HTML عندما يكون HAML موجودًا؟ أو حتى نحيف. أعتقد أن هذا مقال آخر لوقت آخر. العودة إلى المهمة في متناول اليد:
TweetCreator
هو اسم فئة قصير لطيف ، لكن الخلاف الإضافي حول إنشاء الكائن واستدعاء الطريقة طويل جدًا! إذا كان هناك فقط أسبقية في Ruby لاستدعاء شيء ما وجعله ينفذ نفسه على الفور باستخدام المعلمات المعطاة ... أوه انتظر ، هناك! إنها Proc#call
.
يستدعي
Proccall
الكتلة ، ويضبط معلمات الكتلة على القيم الموجودة في المعلمات باستخدام شيء قريب من الطريقة التي تستدعي الدلالات. تقوم بإرجاع قيمة التعبير الأخير الذي تم تقييمه في الكتلة.aproc = Proc.new {|scalar, values| values.map {|value| valuescalar } } aproc.call(9, 1, 2, 3) #=> [9, 18, 27] aproc[9, 1, 2, 3] #=> [9, 18, 27] aproc.(9, 1, 2, 3) #=> [9, 18, 27] aproc.yield(9, 1, 2, 3) #=> [9, 18, 27]
توثيق
إذا كان هذا يحيرك ، دعني أوضح. يمكن call
proc
لتنفيذ نفسه باستخدام المعلمات المحددة. مما يعني أنه إذا كانت TweetCreator
عملية ، فيمكننا تسميتها بـ proc
TweetCreator.call(message)
وستكون النتيجة مكافئة لـ TweetCreator.new(params[:message]).call
TweetCreator.new(params[:message]).send_tweet
.
لذلك دعونا نجعل كائن خدمتنا يتصرف مثل proc
!
أولاً ، لأننا ربما نرغب في إعادة استخدام هذا السلوك عبر جميع كائنات الخدمة لدينا ، فلنستعير من طريقة ريلز وننشئ فئة تسمى ApplicationService
:
# app/services/application_service.rb class ApplicationService def self.call(*args, &block) new(*args, &block).call end end
هل رأيت ما فعلته هناك؟ لقد أضفت طريقة فئة تسمى call
والتي تنشئ مثيلًا جديدًا للفئة مع الوسائط أو تمنعك من تمريرها إليها ، وتستدعي call
على المثيل. بالضبط ما أردناه! آخر شيء يجب القيام به هو إعادة تسمية الطريقة من فئة TweetCreator
call
بها ، والحصول على الفئة ترث من ApplicationService
:
# app/services/tweet_creator.rb class TweetCreator < ApplicationService attr_reader :message def initialize(message) @message = message end def call client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
وأخيرًا ، دعنا نختتم هذا عن طريق استدعاء كائن خدمتنا في وحدة التحكم:
class TweetController < ApplicationController def create TweetCreator.call(params[:message]) end end
تجميع كائنات الخدمة المتشابهة من أجل سلامة العقل
يحتوي المثال أعلاه على كائن خدمة واحد فقط ، ولكن في العالم الحقيقي ، يمكن أن تصبح الأمور أكثر تعقيدًا. على سبيل المثال ، ماذا لو كان لديك المئات من الخدمات ، وكان نصفها عبارة عن إجراءات تجارية ذات صلة ، على سبيل المثال ، وجود خدمة Follower
تتبع حساب Twitter آخر؟ بصراحة ، سأصاب بالجنون إذا احتوى المجلد على 200 ملف فريد المظهر ، ومن الجيد أن هناك نمطًا آخر من Rails Way يمكننا نسخه - أعني ، استخدم كمصدر إلهام: مسافة الأسماء.
دعنا نتظاهر بأننا مكلفنا بإنشاء كائن خدمة يتبع ملفات تعريف Twitter الأخرى.
لنلقِ نظرة على اسم كائن الخدمة السابق: TweetCreator
. يبدو وكأنه شخص ، أو على الأقل دور في منظمة. شخص يقوم بإنشاء تغريدات. أحب تسمية كائنات خدمتي كما لو كانت مجرد: أدوار في مؤسسة. بعد هذا الاصطلاح ، سأتصل بكائن جديد: ProfileFollower
.
الآن ، نظرًا لأنني الرئيس الأعلى لهذا التطبيق ، سأقوم بإنشاء منصب إداري في التسلسل الهرمي للخدمة الخاصة بي وتفويض المسؤولية عن هاتين الخدمتين إلى هذا المنصب. TwitterManager
هذا المنصب الإداري الجديد مدير تويتر.
نظرًا لأن هذا المدير لا يفعل شيئًا سوى الإدارة ، فلنجعله وحدة نمطية ونضع كائنات خدمتنا ضمن هذه الوحدة. سيبدو هيكل المجلد الخاص بنا الآن كما يلي:
services ├── application_service.rb └── twitter_manager ├── profile_follower.rb └── tweet_creator.rb
وكائنات خدمتنا:
# services/twitter_manager/tweet_creator.rb module TwitterManager class TweetCreator < ApplicationService ... end end
# services/twitter_manager/profile_follower.rb module TwitterManager class ProfileFollower < ApplicationService ... end end
وستصبح مكالماتنا الآن TwitterManager::TweetCreator.call(arg)
و TwitterManager::ProfileManager.call(arg)
.

كائنات الخدمة للتعامل مع عمليات قاعدة البيانات
قام المثال أعلاه بإجراء مكالمات API ، ولكن يمكن أيضًا استخدام كائنات الخدمة عندما تكون جميع الاستدعاءات لقاعدة البيانات الخاصة بك بدلاً من واجهة برمجة التطبيقات. هذا مفيد بشكل خاص إذا كانت بعض إجراءات العمل تتطلب تحديثات متعددة لقاعدة البيانات مغلفة في معاملة. على سبيل المثال ، قد يستخدم نموذج التعليمات البرمجية هذا خدمات لتسجيل عملية تبادل العملات الجارية.
module MoneyManager # exchange currency from one amount to another class CurrencyExchanger < ApplicationService ... def call ActiveRecord::Base.transaction do # transfer the original currency to the exchange's account outgoing_tx = CurrencyTransferrer.call( from: the_user_account, to: the_exchange_account, amount: the_amount, currency: original_currency ) # get the exchange rate rate = ExchangeRateGetter.call( from: original_currency, to: new_currency ) # transfer the new currency back to the user's account incoming_tx = CurrencyTransferrer.call( from: the_exchange_account, to: the_user_account, amount: the_amount * rate, currency: new_currency ) # record the exchange happening ExchangeRecorder.call( outgoing_tx: outgoing_tx, incoming_tx: incoming_tx ) end end end # record the transfer of money from one account to another in money_accounts class CurrencyTransferrer < ApplicationService ... end # record an exchange event in the money_exchanges table class ExchangeRecorder < ApplicationService ... end # get the exchange rate from an API class ExchangeRateGetter < ApplicationService ... end end
ماذا أعيد من كائن الخدمة الخاص بي؟
لقد ناقشنا كيفية call
كائن الخدمة ، ولكن ما الذي يجب أن يعود الكائن؟ هناك ثلاث طرق للتعامل مع هذا:
- إرجاع
true
أوfalse
- إرجاع قيمة
- إرجاع التعداد
إرجاع true
أو false
هذا واحد بسيط: إذا كان الإجراء يعمل على النحو المنشود ، فقم بإرجاع true
؛ خلاف ذلك ، إرجاع false
:
def call ... return true if client.update(@message) false end
إرجاع قيمة
إذا كان كائن الخدمة الخاص بك يجلب البيانات من مكان ما ، فربما تريد إرجاع هذه القيمة:
def call ... return false unless exchange_rate exchange_rate end
الرد باستخدام Enum
إذا كان كائن الخدمة الخاص بك أكثر تعقيدًا بعض الشيء ، وتريد التعامل مع سيناريوهات مختلفة ، فيمكنك فقط إضافة تعدادات للتحكم في تدفق خدماتك:
class ExchangeRecorder < ApplicationService RETURNS = [ SUCCESS = :success, FAILURE = :failure, PARTIAL_SUCCESS = :partial_success ] def call foo = do_something return SUCCESS if foo.success? return FAILURE if foo.failure? PARTIAL_SUCCESS end private def do_something end end
وبعد ذلك في تطبيقك ، يمكنك استخدام:
case ExchangeRecorder.call when ExchangeRecorder::SUCCESS foo when ExchangeRecorder::FAILURE bar when ExchangeRecorder::PARTIAL_SUCCESS baz end
ألا يجب أن أضع كائنات الخدمة في lib/services
بدلاً من app/services
؟
هذا شخصي. تختلف آراء الناس حول مكان وضع أشياء خدمتهم. يضعها بعض الأشخاص في lib/services
، بينما ينشئ البعض app/services
. أنا أقع في المعسكر الأخير. يصف دليل البدء في ريلز المجلد lib/
المجلد كمكان لوضع "الوحدات النمطية الموسعة لتطبيقك".
في رأيي المتواضع ، "الوحدات الموسعة" تعني الوحدات التي لا تغلف منطق المجال الأساسي ويمكن استخدامها بشكل عام عبر المشاريع. في الكلمات الحكيمة لإجابة Stack Overflow العشوائية ، ضع رمزًا هناك "يمكن أن يصبح جوهرة خاصة به".
هل كائنات الخدمة فكرة جيدة؟
هذا يعتمد على حالة الاستخدام الخاصة بك. انظر - حقيقة أنك تقرأ هذه المقالة الآن تشير إلى أنك تحاول كتابة رمز لا ينتمي بالضبط إلى نموذج أو وحدة تحكم. لقد قرأت مؤخرًا هذه المقالة حول كيف أن كائنات الخدمة تعتبر مضادًا للنمط. الكاتب لديه آرائه ، لكنني بكل احترام لا أوافق.
لا يعني مجرد إفراط شخص آخر في استخدام كائنات الخدمة أنها سيئة بطبيعتها. عند بدء تشغيلي ، Nazdeeq ، نستخدم كائنات الخدمة بالإضافة إلى نماذج غير ActiveRecord. لكن كان الفرق بين ما يحدث دائمًا واضحًا بالنسبة لي: احتفظ بجميع إجراءات العمل في كائنات الخدمة مع الاحتفاظ بالموارد التي لا تحتاج حقًا إلى المثابرة في نماذج غير ActiveRecord. في نهاية اليوم ، عليك أن تقرر النمط المناسب لك.
ومع ذلك ، هل أعتقد أن عناصر الخدمة فكرة جيدة بشكل عام؟ إطلاقا! إنهم يحافظون على الكود الخاص بي منظمًا بدقة ، وما يجعلني واثقًا في استخدامي لـ POROs هو أن روبي يحب الأشياء. لا ، بجدية ، روبي تحب الأشياء. إنه جنون ، مجنون تمامًا ، لكني أحبه! مثال على ذلك:
> 5.is_a? Object # => true > 5.class # => Integer > class Integer ?> def woot ?> 'woot woot' ?> end ?> end # => :woot > 5.woot # => "woot woot"
يرى؟ 5
حرفيا كائن.
في العديد من اللغات ، لا تعتبر الأرقام والأنواع البدائية الأخرى أشياء. تتبع Ruby تأثير لغة Smalltalk من خلال إعطاء الطرق ومتغيرات الحالة لجميع أنواعها. هذا يسهل استخدام الفرد لروبي ، لأن القواعد المطبقة على الكائنات تنطبق على كل روبي. Ruby-lang.org
متى يجب ألا أستخدم كائن خدمة؟
هذا سهل. لدي هذه القواعد:
- هل تتعامل التعليمات البرمجية الخاصة بك مع التوجيه أو المعلمات أو القيام بأشياء أخرى خاصة بوحدة التحكم؟
إذا كان الأمر كذلك ، فلا تستخدم كائن خدمة - فشفرتك تنتمي إلى وحدة التحكم. - هل تحاول مشاركة التعليمات البرمجية الخاصة بك في وحدات تحكم مختلفة؟
في هذه الحالة ، لا تستخدم كائن خدمة — استخدم مصدر قلق. - هل الكود الخاص بك مثل نموذج لا يحتاج إلى المثابرة؟
إذا كان الأمر كذلك ، فلا تستخدم كائن خدمة. استخدم نموذجًا غير ActiveRecord بدلاً من ذلك. - هل الكود الخاص بك هو إجراء تجاري محدد؟ (على سبيل المثال ، "إخراج القمامة" أو "إنشاء ملف PDF باستخدام هذا النص" أو "حساب الرسوم الجمركية باستخدام هذه القواعد المعقدة")
في هذه الحالة ، استخدم كائن خدمة. ربما لا يتناسب هذا الرمز منطقيًا مع وحدة التحكم أو النموذج الخاص بك.
بالطبع ، هذه هي قواعدي ، لذا فنحن نرحب بك لتكييفها مع حالات الاستخدام الخاصة بك. لقد عملت هذه بشكل جيد جدًا بالنسبة لي ، ولكن قد يختلف عدد الأميال التي قطعتها.
قواعد لكتابة كائنات الخدمة الجيدة
لدي أربع قواعد لإنشاء كائنات الخدمة. هذه ليست مكتوبة على الحجر ، وإذا كنت تريد حقًا كسرها ، يمكنك ذلك ، لكن ربما سأطلب منك تغييرها في مراجعات الكود ما لم يكن تفكيرك سليمًا.
القاعدة 1: أسلوب عام واحد فقط لكل كائن خدمة
كائنات الخدمة هي إجراءات عمل فردية . يمكنك تغيير اسم طريقتك العامة إذا أردت. أنا أفضل استخدام call
، لكن كود perform
CE يدعو إلى execute
وقد يستخدمه الأشخاص الآخرون. استخدم ما تريد - يمكنك تسميته nermin
لكل ما يهمني. فقط لا تنشئ طريقتين عامتين لكائن خدمة واحد. قسّمها إلى جسمين إذا احتجت إلى ذلك.
القاعدة 2: تسمية كائنات الخدمة مثل الأدوار الغبية في الشركة
كائنات الخدمة هي إجراءات عمل فردية. تخيل لو وظفت شخصًا واحدًا في الشركة للقيام بهذه الوظيفة ، فماذا ستسميه؟ إذا كانت مهمتهم إنشاء تغريدات ، فاتصل بهم TweetCreator
. إذا كانت وظيفتهم قراءة تغريدات معينة ، فاتصل بهم TweetReader
.
القاعدة 3: لا تقم بإنشاء كائنات عامة لأداء إجراءات متعددة
كائنات الخدمة هي إجراءات عمل فردية. لقد قسمت الوظيفة إلى جزأين: TweetReader
و ProfileFollower
. ما لم أفعله هو إنشاء كائن عام واحد يسمى TwitterHandler
وتفريغ جميع وظائف API هناك. من فضلك لا تفعل هذا. هذا يتعارض مع عقلية "العمل التجاري" ويجعل كائن الخدمة يبدو مثل Twitter Fairy. إذا كنت ترغب في مشاركة التعليمات البرمجية بين كائنات الأعمال ، فما عليك سوى إنشاء كائن أو وحدة BaseTwitterManager
في كائنات الخدمة الخاصة بك.
القاعدة 4: معالجة الاستثناءات داخل كائن الخدمة
للمرة الألف: كائنات الخدمة هي إجراءات عمل فردية. لا أستطيع أن أقول هذا بما فيه الكفاية. إذا كان لديك شخص يقرأ التغريدات ، فسيعطيك تغريدة ، أو يقول ، "هذه التغريدة غير موجودة." وبالمثل ، لا تدع كائن الخدمة يصاب بالذعر ، واقفز على مكتب وحدة التحكم الخاصة بك ، واطلب منه إيقاف كل الأعمال بسبب "خطأ!" فقط قم بإرجاع false
واترك وحدة التحكم تتحرك من هناك.
الاعتمادات والخطوات التالية
لم تكن هذه المقالة ممكنة بدون المجتمع المذهل لمطوري Ruby في Toptal. إذا واجهت مشكلة في أي وقت ، فإن المجتمع هو أكثر مجموعة من المهندسين الموهوبين الذين قابلتهم على الإطلاق.
إذا كنت تستخدم كائنات الخدمة ، فقد تجد نفسك تتساءل عن كيفية فرض إجابات معينة أثناء الاختبار. أوصي بقراءة هذه المقالة حول كيفية إنشاء كائنات خدمة وهمية في Rspec والتي ستعيد دائمًا النتيجة التي تريدها ، دون الوصول فعليًا إلى كائن الخدمة!
إذا كنت ترغب في معرفة المزيد عن حيل روبي ، فإنني أوصي بإنشاء Ruby DSL: دليل للبرمجة الوصفية المتقدمة بواسطة زميل Toptaler Mate Solymosi. لقد قام بتقسيم كيف أن ملف routes.rb
لا يشبه روبي ويساعدك على بناء DSL الخاص بك.