تجاوز طرق Discourse الموجودة في الإضافات

لقد واجهتُ مؤخرًا عدة حالات تتطلب تجاوز (override) لطرق Ruby الموجودة مسبقًا في الإضافات، ففكرتُ في مشاركة أفضل الممارسات الخاصة بي هنا.

تجاوز طريقة مثلية (Instance Method)

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • هنا أقوم بإزالة مواضيع الدردشة من طريقة مثلية تُرجع قائمة بالمواضيع.
  • يمكن أن يكون اسم الوحدة النمطية BabbleDefaultResults أي شيء تريده؛ عادةً ما أجعله يتطابق مع اسم الطريقة بالإضافة إلى اسم إضافتي لتقليل مخاطر تضارب الأسماء (على الرغم من أنها منخفضة بالفعل).
  • Module#prepend ميزة رائعة جدًا، ويجب أن تكون على دراية بها إذا كنت تكتب إضافات لأي شيء بلغة Ruby. لاحظ أن حقيقة أننا نستخدم prepend مع وحدة نمطية هي ما يسمح لنا باستدعاء super داخل طريقة التجاوز.
  • ملاحظة: دائمًا استدر super! هذا يجعل إضافتك أقل عرضة للكسر عند تغيير التطبيق الأساسي. ما لم تكن متأكدًا تمامًا أن وظيفتك تحل محل كل شيء في الطريقة الأساسية، فأنت تريد استدعاء super وتعديل النتائج من هناك، بحيث لا تؤدي التغييرات في هذه الطريقة في نواة Discourse إلى كسر إضافتك لاحقًا.
  • الرمز :: في ::TopicQuery يضمن أنني أشير إلى فئة TopicQuery على المستوى الأعلى للتجاوز، وليس بعض النسخة الموحدة منها (مثل Babble::TopicQuery).
  • يمكن وضع هذا مباشرة في ملف plugin.rb كما هو، أو إذا كانت إضافتك كبيرة، يمكنك التفكير في فصل كل تجاوز إلى ملف منفصل.

تجاوز طريقة فئة (Class Method)

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • هنا آخذ طريقة self.for_digest الموجودة مسبقًا في فئة Topic، وأزيل مواضيع الدردشة من النتيجة.
  • مشابه جدًا لتجاوز الطريقة المثلية، مع ملاحظة الفرق وهو أننا نستدعي singleton_class.prepend بدلاً من مجرد prepend. singleton_class هي طريقة غريبة بعض الشيء للقول ‘أريد إرفاق هذا على مستوى الفئة، وليس على مستوى المثيل’، مزيد من القراءة إذا كنت تبحث عن مغامرة في عالم Ruby.

تجاوز نطاق (Scope)

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • هذه الحالة معقدة قليلاً لأن النطاقات (scopes) لا تعمل بشكل جيد مع super (أو على الأقل، لم أستطع جعلها تعمل). لذا، بدلاً من ذلك، نأخذ تعريف طريقة موجود، ونقوم باستنساخه، وتخزينه، ثم استدعاؤه لاحقًا.
  • مرة أخرى، يمكن أن يكون @@babble_listable_topics أي شيء تريده، لكن استخدام اسم إضافتك كاسم فضاء (namespacer) فكرة جيدة على الأرجح.
  • المزيد حول دالة method، وهي أيضًا رائعة جدًا، على الرغم من أن الأوقات التي تحتاج فيها إليها حقًا نادرة جدًا. حقيقة ممتعة إضافية تتعلق بذلك؛ عند التصحيح، إذا واجهت صعوبة في معرفة الكود الذي يتم تنفيذه لاستدعاء طريقة معينة (عادةً “أي مكتبة (gem) تعرف هذه الطريقة؟”)، يمكنك استخدام source_location للحصول على السطر الدقيق من كود المصدر حيث تُعرف الطريقة.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

( ^^ هذا يعني أن طريقة best_post في موضوع جديد مُعرَّفة في /app/models/topic.rb، في السطر 282)

حسنًا، هذا كل ما لدي. أخبرني إذا كان يجب عليّ تصحيح أو توسيع أو توضيح أي شيء :slight_smile:

35 إعجابًا

This is great, thanks for sharing James!

Overriding instance and class methods

My fav resource on this is: https://stackoverflow.com/a/4471202

I use basically the same structure, except I tend to seperate out the module and the prepend.

As you pointed out, this pattern is “super” :wink: useful when trying to avoid overriding core logic.

module InviteMailerEventExtension
  def send_invite(invite)
     ## stuff
     super(invite)
  end
end

require_dependency 'invite_mailer'
class ::InviteMailer
  prepend InviteMailerEventExtension
end

One small tip here is that when overriding private or protected methods, your overriding method also needs to be private or protected, e.g.

module UserNotificationsEventExtension
  protected def send_notification_email(opts)
    ## stuff
    super(opts)
  end
end
20 إعجابًا

@angus @gdpelican Thanks for this. This is great stuff. :smiley: . This would be really essential all the (especially newbies like me) plugin developers out there.

This is what I really really needed to be aware of. I use to think that if you override a method only to make a few changes to it, you’d probably copy the code to your new method and make changes to it which by the very thought of it sounded hacky.

3 إعجابات

Hi, I was wondering about this part of the code? why is this require_dependency needed? It seems like the code is working without it as well.

Yes, indeed. Since that post, updates to Discourse’s use of rails have made require_dependency unecessary. I’m unable to edit the post to address that. See further:

3 إعجابات

أي نصائح لتجاوز فئات الوحدات؟ أريد إجراء بعض التغييرات على GroupGuardian (بعض الشروط الخاصة لنوع خاص من المجموعات).

شكرا.

إعجاب واحد (1)

يمكنك ببساطة إعادة تعريف الوحدة النمطية وإعادة تعريف الدالة. استخدم alias_method بهذا الشكل RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info إذا كنت ترغب في الاحتفاظ بالوصول إلى الطريقة القديمة.

3 إعجابات

جديد في تطوير discourse و Rails. أستخدم بيئة Dev Container (في VS Code) محليًا. كانت الأدلة والوثائق مفيدة.

كنت أتساءل عما إذا كان لدى أي شخص أي نصائح حول كيفية تجاوز فئات discourse الأساسية، وتحديدًا جعلها مستمرة في بيئة تطوير محلية.

في المكون الإضافي الخاص بي، أحاول تجاوز طريقة في فئة discourse الأساسية TopicEmbed. (باستخدام النهج العام الموثق بشكل جيد من قبل @angus أعلاه.) يعمل مرة واحدة عند إعادة بناء وإعادة تحميل VS Code، ولكن في طلبات HTTP اللاحقة لا يتم استدعاء التجاوز الخاص بي أبدًا.

تم تعريف التجاوز الخاص بي في /plugins/my-plugin/app/models/override.rb وأستخدم require_relative لتضمين هذا الملف في plugin.rb الخاص بي.

#override.rb:
class ::TopicEmbed

  # وحدة سيتم إلحاقها بـ TopicEmbed.singleton_class
  module TopicEmbedOverrideModule
    # طريقة في TopicEmbed
    def first_paragraph_from(html)
      Rails.logger.info(“my override is happening! ”)

      # استمر في التنفيذ الأصلي المقدم من TopicEmbed.
      super

    end
  end

  # قم بالإلحاق هنا
  singleton_class.prepend TopicEmbedOverrideModule
end

أشك في أن هذا التحدي المتعلق بالاستمرارية قد يكون بسبب بيئة التطوير الخاصة بي وكيفية تجميع/تخزين ذاكرة التخزين المؤقت لرمز ruby.
لقد جربت أيضًا rm -rf tmp; bin/ember-cli -u و bundle exec rake tmp:cache:clear.

لقد جعلتها تعمل لفئة مفردة (singleton class) بهذه الطريقة:

# my overrides.rb

# وحدة سيتم إلحاقها بفئة TopicEmbed المفردة (singleton_class)
module TopicEmbedOverrides

  # تجاوز (Override) الدالة parse_html
  def parse_html(html, url) # ملاحظة: لا أستخدم self. هنا
    # محتوياتي الجديدة هنا
    # ثم تشغيل التنفيذ الأصلي
    super
  end

end

# قم بالتجاوز هنا
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end