في بداية المقال ، أود أن أشير على الفور إلى أنني لا أتظاهر بأنني جديد ، لكنني أريد فقط مشاركة / استدعاء هذه الفرصة مثل IoC DI.
أيضًا ، ليس لدي أي خبرة تقريبًا في كتابة المقالات ، هذه هي تجربتي الأولى. لقد بذلت قصارى جهدي ، إذا لم تحكم بدقة.
ما نتحدث عنه
تواجه معظم مشاريع ريلز التي صادفتها مشكلة كبيرة واحدة. إما أنهم لا يجرون اختبارات على الإطلاق ، أو أن اختباراتهم تتحقق من بعض الأجزاء غير المهمة ، في حين أن جودة هذه الاختبارات لا تتطلب الكثير.
السبب الرئيسي لذلك هو أن المطور ببساطة لا يعرف كيفية كتابة الرمز حتى أنه في اختبارات الوحدة يختبر الرمز الذي كتبه فقط ، ولا يختبر الرمز ، والذي ، على سبيل المثال ، موجود في كائن خدمة أو مكتبة أخرى.
علاوة على ذلك ، يتم تشكيل سلسلة منطقية في رأس المبرمج ، ولكن لماذا أحتاج إلى نقل رمز منطق الأعمال إلى طبقة أخرى ، سأضيف فقط سطرين هنا وسيفي كل شيء بمتطلبات العميل.
وهذا أمر سيئ للغاية ، لأن اختبار الوحدة يفقد مقاومته لإعادة البناء ، ويصبح من الصعب إدارة التغييرات في مثل هذا الرمز. الانتروبيا تتزايد تدريجيا. وإذا كنت تخشى بالفعل إعادة صياغة التعليمات البرمجية الخاصة بك ، فإن الأمور سيئة للغاية.
لحل مثل هذه المشاكل في عالم جافا ، يوجد عدد من المكتبات لفترة طويلة وليس من المنطقي إعادة اختراع العجلة ، على الرغم من أنه يجب ملاحظة أن هذه الحلول معقدة للغاية ولا يوجد دائمًا سبب لاستخدامها. يبدو أن علماء الروبيات يحلون بطريقة ما هذه المشاكل بطريقة مختلفة ، ولكن بصراحة ، ما زلت لا أفهم كيف. لذلك ، قررت مشاركة كيف قررت القيام بذلك.
الفكرة العامة لكيفية حل هذا في مشاريع روبي
الفكرة الأساسية هي أنه بالنسبة للأشياء التي لها تبعيات ، يجب أن نكون قادرين على إدارتها.
فكر في مثال:
class UserService def initialize() @notification_service = NotificationService.new end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end
لاختبار طريقة block_user ، نجد أنفسنا في لحظة غير سارة ، لأن الإخطار من NotificationService سيعمل لصالحنا ونحن مضطرون إلى معالجة جزء صغير للغاية تؤديه هذه الطريقة.
يسمح لنا الانقلاب بالخروج ببساطة من هذا الموقف إذا قمنا بتطبيق UserService ، على سبيل المثال ، مثل:
class UserService def initialize(notification_service = NotificationService.new) @notification_service = notification_service end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end
الآن ، عند الاختبار ، نمرر كائنًا على أنه نموذج NotificationService ، ونتحقق من أن block_user يسحب أساليب خدمة الإخطار بالترتيب الصحيح وبالوسائط الصحيحة.
RSpec.describe UserService, type: :service do let (:notification_service) { instance_double(NotificationService) } let (:service) { UserService.new(notification_service) } describe ".block_user" do let (:user) { instance_double(User) } it "should block user and send notification" do expect(user).to receive :block! expect(notification_service).to receive(:send).with(user, "you have been blocked") service.block_user(user) end end end
دراسة حالة للسكك الحديدية
عندما يكون هناك الكثير من كائنات الخدمة في النظام ، يصبح من الصعب إنشاء كل التبعيات بنفسك ، ويبدأ الكود في النمو باستخدام أسطر إضافية من الكود تقلل من سهولة القراءة.
في هذا الصدد ، حدث لي أن أكتب وحدة صغيرة تقوم بأتمتة إدارة التبعية.
module Services module Injector def self.included(base)
هناك تحذير واحد ، يجب أن تكون الخدمة Singleton ، أي لديك طريقة مثيل. أسهل طريقة للقيام بذلك هي عن طريق الكتابة include Singleton
في فئة الخدمة.
الآن في إضافة ApplicationController
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
والآن في وحدات التحكم يمكننا القيام بذلك
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
في مواصفات وحدة التحكم هذه ، نحصل تلقائيًا على example_double (خدمة ترحيب مرحبًا) كمُعال.
RSpec.describe WelcomeController, type: :controller do describe "index" do it "should render text from test service" do allow(controller.welcome_service).to receive(:text).and_return "OK" get :index expect(response).to have_attributes body: "OK" expect(response).to have_http_status 200 end end end
ما يمكن تحسينه
تخيل ، على سبيل المثال ، أنه في نظامنا هناك العديد من الخيارات لكيفية إرسال الإشعارات ، على سبيل المثال ، في الليل سيكون مزودًا وآخر خلال النهار. في الوقت نفسه ، لدى مزودي الخدمة بروتوكولات إرسال مختلفة تمامًا.
بشكل عام ، تظل واجهة NotificationService كما هي ، ولكن هناك تطبيقان محددان.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
الآن يمكننا كتابة فصل دراسي يقوم بإجراء رسم خرائط مشروط للخدمات
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
الآن عندما نأخذ مثيل الخدمة في Services :: Helpers :: Service.instance نحتاج إلى التحقق مما إذا كان هناك كائن * Mapper ، وإذا كان الأمر كذلك ، فعليك أخذ الفصل الثابت من خلال أخذ.