فصل منطق العرض في Ruby on Rails باستخدام Presenters
حشو نماذج ActiveRecord بدوال مخصصة للعرض ممارسة سيئة وعلامة على مشكلة في الكود. تضيف الـ Presenters طبقة تجريد بين النماذج والواجهات لتساعدنا على بناء تطبيق أسهل في الصيانة.

بصفتكم مطوّري Rails، ستجدون كثيرًا نماذج (models) تحتوي على دوال يبدو أن كل ما تفعله هو بعض التنسيق. تُستخدم هذه الدوال عادة في الواجهات (views) لعرض البيانات بصيغة مألوفة أكثر للمستخدمين النهائيين. من المغري جدًا تعريف دوال التنسيق والدوال المنطقية (boolean) هذه داخل النموذج، لأننا اعتدنا على فكرة أن النماذج هي ما يُعرض في الواجهات. لكن إذا توقفنا وتأملنا دور كل كائن في تطبيقنا، فسنصل بسهولة إلى أن تعريف هذه الدوال يخلط بين المسؤوليات. ومع أنني لا أرى أن خلط المسؤوليات يجب أن يُمنع دائمًا، فإنني أعتقد أن حل هذه المشكلة سهل، ولذلك يستحق العناء. الـ Presenters تجريد بسيط يحل مشكلتنا بجهد قليل وفائدة كبيرة. فهي تجعل تطبيقاتنا أسهل كثيرًا في الصيانة عبر تقليل المسؤوليات، وهذا بدوره يقلل الفوضى ويعزل المنطق الذي يخص جزءًا صغيرًا واحدًا فقط من التطبيق ككل.
ما هو الـ Presenter؟
الـ Presenter كائن يقع بين النموذج والواجهة ليغلّف منطق التنسيق وغيره من منطق العرض المعقد. على سبيل المثال، إذا كان لدينا BankCard ونريد إخفاء جزء من الرقم، يمكننا إنشاء BankCardPresenter وتعريف دالة masked_card_number
class BankCardPresenter
def initialize(bank_card)
@bank_card = bank_card
end
def masked_card_number
"**-**-**-#{@bank_card.card_number[-4..-1]}"
end
def valid?
Time.current.before?(@bank_card.expiration)
end
def bank_name
return 'KBank' if @bank_card.bank_name == 'Kasikorn'
@bank_card.bank_name
end
end
BankCardPresenter.new(BankCard.first).masked_card_number
# => **-**-**-4242
أو ربما نريد عرض رمز QR لنظام الدفع لدينا. يمكننا إنشاء TransactionPresenter وتعريف دالة 'qr_code'
class TransactionPresenter
def initialize(transaction)
@transaction = transaction
end
def qr_code
QRCode.new(transaction.transaction_identifier)
end
end
مع الوقت، تبدأ هذه الدوال في إثقال نماذجنا. لا يمثل ذلك مشكلة عادة إلى أن تصبح لديكم مئات الدوال في نماذجكم العملاقة (god models). على الأرجح نحتاج إلى qr_code في واجهة واحدة فقط، وحتى لو احتجنا إليها في كل الواجهات، فلن نحتاج إليها في النماذج أو في المهام الخلفية (background jobs) وغيرها. يجب أن يُصمَّم منطق التطبيق ليعمل مع البيانات كما تأتينا مباشرة من قاعدة البيانات. ولا نحتاج إلى التنسيق إلا عند عرض البيانات للمستخدمين النهائيين، لتظهر بصيغة مألوفة يسهل استيعابها.
هل يمكننا تحسين ذلك؟
تقدم Ruby صنفًا جميلًا يساعدنا على إنشاء الـ Presenters، وهو SimpleDelegator! وربما تكون الـ Presenters من أنسب حالات استخدام SimpleDelegator. SimpleDelegator صنف أساسي في Ruby يتيح لنا تفويض جميع الدوال غير المعرّفة في الكائن الحالي إلى الكائن المغلَّف. ولتغليف كائن، يكفي أن نمرره عند الإنشاء. يتيح لنا ذلك إنشاء Presenters لا تحتاج إلى منطق إنشاء مخصص، ويمكننا التعامل مع الـ Presenter كأنه الكائن المعروض نفسه، فنستدعي transaction_identifier على الـ Presenter مباشرة بدلًا من الكائن @transaction. وهذا يجعل الـ Presenters أنظف وأبسط.
class BankCardPresenter < SimpleDelegator
def masked_card_number
"**-**-**-#{card_number[-4..-1]}"
end
end
BankCardPresenter.new(BankCard.first).masked_card_number
# => **-**-**-4242
قد ترغبون أحيانًا في تجاوز (override) دالة موجودة في الكائن المغلَّف، ويتيح لنا SimpleDelegator استدعاء super للوصول إلى الكائن المعروض، مثلًا:
class BankCardPresenter < SimpleDelegator
def card_number
"**-**-**-#{super[-4..-1]}"
end
end
BankCardPresenter.new(BankCard.first).card_number
# => **-**-**-4242
متى يجب استخدام الـ Presenter؟
شخصيًا أستخدمها في الواجهات والـ serializers حين يتطلب الموقف بعض التنسيق للخصائص. وكلما احتجت إلى خاصية غير موجودة أصلًا في النموذج، أنشئ Presenter. تنظيم الـ Presenters أصعب قليلًا، وهو أمر يجب الانتباه إليه، وستحتاجون إلى التجربة. على سبيل المثال، إذا أنشأتم Presenter باسم UserRegistrationPresenter واستخدمتموه في 4 واجهات مختلفة، ثم احتاجت واجهة واحدة منها إلى خاصية إضافية، فيمكنكم إما إضافة تلك الخاصية رغم أنها تخص واجهة واحدة من أربع، أو إنشاء Presenter جديد بالكامل. يمكنكم بالطبع إنشاء أصناف فرعية منها لمشاركة الدوال، أو حتى استخدام concerns أو وحدات PORO أو أساليب أخرى للمشاركة بين الـ Presenters المختلفة. ومن تجربتي، لن تحتاجوا في أغلب الأحيان إلى عدد كبير من الـ Presenters لإنجاز المهمة. أميل إلى إضافة تلك الدالة الإضافية فحسب، حتى لو كانت تخص بضع واجهات فقط. فهذا يظل تحسنًا كبيرًا مقارنة بحشر هذه الدوال في النماذج. من الأمور اللطيفة في الـ Presenters أنها تعرض الكائن الممرَّر إليها، لذا يمكنكم نظريًا تمرير Presenter إلى Presenter آخر. لا أنصح بذلك بالضرورة، لأنني متأكد من أنه سيصبح من الصعب تتبع وجهة الاستدعاءات بدقة بعد مستوى معين من التداخل. وكالعادة، القرار لكم. تمنحنا Ruby قدرًا هائلًا من المرونة والقوة، فاستخدموها بمسؤولية! الـ Presenters مجرد أداة إضافية نستخدمها لبناء تطبيقات سهلة الصيانة، وأرجو أن تجدوها مفيدة كما أجدها أنا.


