OOZOU
تواصلوا معنا
العودة إلى المدونة

ضمان سهولة الصيانة في تطبيقات Rails الكبيرة: الـ Fat Controllers بالطريقة الصحيحة

كان مجتمع Ruby on Rails محقًا في الابتعاد عن الـ fat controllers، لكننا فعلنا ذلك بالطريقة الخاطئة. أريد أن أعيد النظر في معمارية الـ fat controller بخبرة أكبر، وأن أحل المشكلة بطريقة أفضل

27 مايو 2020
5 دقائق للقراءة
ضمان سهولة الصيانة في تطبيقات Rails الكبيرة: الـ Fat Controllers بالطريقة الصحيحة

مررنا بعدة حقب في مجتمع Rails.

البداية: Fat Controllers

في البداية، كان معظم الكود يُكتب داخل الـ controllers. وتضخمت الـ controllers إلى درجة أصبح معها التنقل في الكود مستحيلًا. فبدأنا ننقل ذلك المنطق إلى الـ models لنبقي الـ controllers أصغر.

Skinny Controllers وFat Models

انتقلنا بعدها إلى حقبة الـ skinny controllers. وترك لنا ذلك fat models، فتكررت المشكلة نفسها لكن بصورة أسوأ: لم يعد الكود معزولًا لكل request على حدة كما كان مع الـ fat controllers، فاضطررنا إلى الحذر في إدارة الآثار الجانبية (side-effects) بشكل صحيح. فقد يكون model callback مرغوبًا في request ما وغير مرغوب في آخر، والأمر نفسه ينطبق على الـ validations وغيرها. كما بدأت الـ models تتضخم بدورها، مليئة بـ methods لا تخص إلا جزءًا من الـ requests التي يتعامل معها ذلك الـ model.

Service Objects

في مرحلة ما بعد التحوّل إلى الـ fat models، بدأنا نبني service objects، أي أصناف PORO وmodules لتشغيل المنطق المجرّد. ساعدنا ذلك على تقليص حجم الـ models إلى حجم يمكن إدارته، لكنه لم يحل المشكلة الأساسية فعلًا. وبالنظر إلى الوراء، أعتقد أنه لو بدأ المجتمع باستخدام service objects في وقت أبكر، لما انتقلنا إلى الـ fat models أصلًا. ففي ذلك الوقت، كان المجتمع يحاول جاهدًا الالتزام بعقلية controller أو model أو view، وكان أي شيء خارج ذلك أشبه بالمحظور.

مستقبل أفضل

هناك حلول معمارية متاحة، لكنها غالبًا أعقد بكثير من معمارية Rails التقليدية. وفي رأيي، ظهرت المشكلة الأساسية حين ابتعدنا عن الـ fat controllers. فقد أتاحت لنا الـ fat controllers منطقًا معزولًا خاصًا بكل request، أي بنقطة الدخول إلى تطبيقاتنا. المستخدم النهائي يبدأ دائمًا من controller action، فلماذا نكتب الكود وكأنه يبدأ من الـ model؟ لماذا لا نبدأ من الـ controller ونبني إلى الخارج؟ هذا ما بدأت أفعله.

Form Objects وService Objects

الـ form objects موجودة منذ وجود ActiveModel. وعندما بدأ الناس يروّجون لفكرة الـ form objects، كان الحديث عنها أساسًا بوصفها حالة استخدام للـ models الافتراضية، مثل نماذج التواصل أو مصادر البيانات الأخرى التي لا تُحفظ.

class ContactForm
  include ActiveModel::Model

  attr_accessor :name, :email, :message

  validates :name, presence: true
  validates :email, presence: true
  validates :message, presence: true

  def send_mail
    return false unless valid?

    ContactFormMailer.incoming_message(
      name: name, email: email, message: message
    ).deliver_later
  end
end
<em>language-ruby</em>

يمكننا استخدام التصميم نفسه لتمرير الـ params إلى الـ models لحفظها. ويتيح لنا ذلك القيام بأمور مثيرة للاهتمام كانت ستكون أعقد بكثير بطريقة أخرى. فربما لديكم صفحة تحفظ البيانات في أكثر من model:

class UpdateProfileForm
  include ActiveModel::Model

  attr_accessor :user_id, :name, :contact_number

  def save
    user = User.find(user_id)
    user.update(name: name)
    user.device.update(contact_number: contact_number)
  end
end
<em>language-ruby</em>

والأهم من ذلك أننا نستطيع نقل كل تلك الـ validations والـ callbacks من الـ models إلى أماكنها الصحيحة. والسبب الرئيسي الذي دفعني إلى البحث عن حل لهذه المشكلة هو طبيعة تصميم تجربة المستخدم (UX) في التطبيقات الحديثة: فالتسجيل غالبًا ليس نموذجًا بسيطًا في صفحة واحدة، بل مجموعة من الخطوات. والـ validations لا تنطبق دائمًا على كل خطوة منها. على سبيل المثال، لنتخيل حالة لدينا فيها إجراء KYC يضم 50 حقلًا. الصفحة الأولى فيها 10 حقول، منها أداة لرفع صورة بطاقة الهوية وصورة سيلفي مع حمل البطاقة. وهذا شائع إلى حد ما في إجراءات KYC (اعرف عميلك)، ومن خبرتي غالبًا ما لا يُسمح بتخزين هذه الملفات لفترة طويلة. وفي أكثر من تطبيق عملت عليه، كان يجب التحقق من هذه الملفات في الخطوة الأولى، ثم تُرسل للمعالجة قبل أن نتحقق من الخطوة الثانية. ومع معمارية Rails MVC التقليدية، يصبح هذا فوضى حقيقية. وينتهي بكم الأمر إلى شيء كهذا

class User
  attr_accessor :kyc_step, :updating_profile
  validates :name, on: :create
  validates :id_image, if: -> { kyc_step == '1' }
  validates :document, if: -> { kyc_step == '2' }
  validates :avatar, if: -> { kyc_step == '3' || updating_profile }
end
<em>language-ruby</em>

مع الـ form objects، يكون لكل action مجموعته الخاصة من الـ validations، فلا حاجة إلى هذه الـ attributes الافتراضية والشروط المعقدة. ويصبح الكود أسهل بكثير في القراءة والفهم. فكل منطق الـ controller action موجود داخل class واحد (باستثناء استدعاءات الـ service objects المجرّدة)، وهو معزول عن بقية قاعدة الكود. حالفني الحظ بكتابة تطبيقين من الصفر باستخدام الـ form objects، وفي المرتين شعرت بأن ذلك أفاد تصميم التطبيق، وسيثبت أنه نظام أبسط في الصيانة على المدى الطويل. كما طوّرت مكتبة صغيرة لهذا الغرض اسمها ActionForm، وسأتناولها بتفصيل أكبر في مقال آخر. تعود الـ models لتكون ORM models من جديد. تعرّف الـ associations والـ scopes، وهذا كل شيء. ويُنقل المنطق إلى أماكنه الصحيحة. لكل controller action نموذج (form) خاص به، وهو سهل جدًا في القراءة والـ debugging. بل يمكنكم حتى عرضه في الـ views.

النتيجة

class User < ApplicationRecord
  belongs_to :account
  has_many :devices

  scope :active, -> { where.not(:activated_at: nil) }
end

# app/forms/user/registration_form.rb
class User < ApplicationRecord
  class RegistrationForm
    # more about this in another post,
    # basically a collection of active support
    # modules and some helpers
    include ActiveForm::Form 

    attr_accessor :user

    attribute :email
    attribute :password

    with_options presence: true do
      validates :email, format: { with: Devise.EMAIL_REGEX }
      validates :password, length: { minimum: 8, maximum: 128 }
    end

    def save
      return false unless valid?

      self.user = User.create(email: email, password: password)
    end
  end
end

# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  def new
    @form = User::RegistrationForm.new
  end

  def create
    @form = User::RegistrationForm.new(params: params[:form])

    if @form.save
      redirect_to welcome_path
    else
      render :new
    end
  end
end

# app/views/registrations/new.erb.html
<%= simple_form_for @form, url: registrations_path, method: :post do |f| %>
  <%= f.input :email %>
  <%= f.input :password %>
  <%= f.submit %>
<% end %>

Related Articles

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

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

اقرأوا المزيد
مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

يتطور المشهد الرقمي بسرعة، وعلى الشركات أن تتكيّف لتواكب الطلب المتزايد على السرعة والكفاءة والمعالجة في الوقت الفعلي.

اقرأوا المزيد
مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

يتطور قطاع التجزئة بوتيرة غير مسبوقة بفضل الذكاء الاصطناعي (AI) والواقع المعزّز (AR) وتقنيات التحوّل الرقمي. فلم تعد المتاجر التقليدية مجرد أماكن لبيع المنتجات، بل أصبحت متاجر ذكية وتفاعلية.

اقرأوا المزيد

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

ابدأوا المحادثةhello@oozou.com
OOZOU Logo

بانكوك · سنغافورة · هونغ كونغ

X
Facebook
Instagram
LinkedIn
OOZOU Co., Ltd.
548 One City Centre
Ploenchit Road
Bangkok 10330
Thailand
+66 2544 0000
hello@oozou.com

الخدمات

  • تطوير الويب
  • وكلاء الذكاء الاصطناعي والذكاء الاصطناعي التوليدي
  • تطوير تطبيقات الجوال
  • تحليلات البيانات وهندستها
  • تصميم تجربة المستخدم وواجهته (UX/UI) وتصميم المنتجات
  • التحوّل الرقمي

الشركة

  • من نحن
  • الوظائف
  • المدونة
  • دراسات الحالة
  • عروض حية
  • تعلّمت اليوم
  • تواصل معنا

المزيد

  • القطاعات
  • شبكة الشركاء
© 2026 OOZOU. جميع الحقوق محفوظة.
سياسة الخصوصيةمدونة قواعد السلوكسياسة مكافحة الرشوة والفساد