gdpelican
(James Kiesel)
2018 年 3 月 20 日午前 11:37
1
最近、プラグインから既存の Ruby メソッドをオーバーライドするケースが増えているので、私のベストプラクティスを共有したいと思います。
インスタンスメソッドのオーバーライド
class ::TopicQuery
module BabbleDefaultResults
def default_results(options={})
super(options).where('archetype <> ?', Archetype.chat)
end
end
prepend BabbleDefaultResults
end
ここでは、トピックのリストを返すインスタンスメソッドからチャットトピックを除外しています。
モジュール名 BabbleDefaultResults は任意に設定できます。私は通常、メソッド名にプラグイン名を組み合わせて命名し、名前の衝突リスクを最小限に抑えています(すでに衝突リスクは低いですが)。
Module#prepend は非常に優れており、Ruby 製のプラグインを開発する場合は知っておくべきです。オーバーライドされたメソッド内で super を呼び出せるのは、モジュールを prepend しているからです。
PS、必ず super を呼び出してください! これにより、基盤となる実装が変更された際、プラグインが壊れる可能性を大幅に減らせます。あなたの機能が基盤のメソッド内の「すべて」を完全に置き換えると「本当に、本当に確信がある」場合を除き、super を呼び出してその結果を変更するようにしてください。そうすれば、Discourse コア側でこのメソッドが変更されても、プラグインが将来的に壊れるのを防げます。
::TopicQuery の :: は、モジュール化されたバージョン(例:Babble::TopicQuery)ではなく、トップレベルの TopicQuery クラスを参照してオーバーライドしていることを保証します。
これはそのまま plugin.rb に記述できますが、プラグインが大きい場合は、各オーバーライドを別ファイルに分離することを検討してください。
クラスメソッドのオーバーライド
class ::Topic
module BabbleForDigest
def for_digest(user)
super(user).where('archetype <> ?', Archetype.chat)
end
end
singleton_class.prepend BabbleForDigest
end
ここでは、Topic クラスに存在する self.for_digest メソッドを取得し、結果からチャットトピックを除外しています。
インスタンスメソッドのオーバーライドと非常に似ていますが、単に prepend するのではなく singleton_class.prepend を呼び出している点が異なります。singleton_class は「インスタンスレベルではなくクラスレベルにこれを追加したい」という少し奇妙な表現ですが、詳しくは こちら をご覧ください(Ruby 関連の深い探求を探している場合)。
スコープのオーバーライド
class ::Topic
@@babble_listable_topics = method(:listable_topics).clone
scope :listable_topics, ->(user) {
@@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
}
end
これは少しトリッキーです。スコープは super と相性が良くない(少なくとも私にはできませんでした)ため、既存のメソッド定義を取得してクローンを作成し、保存してから後で呼び出すようにしています。
同様に、@@babble_listable_topics は任意の名前で大丈夫ですが、名前空間としてプラグイン名を使うのが良いでしょう。
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 メソッドが <root>/app/models/topic.rb の 282 行目で定義されていることを示しています)
以上が私の持てるすべてです。修正、追加、あるいは明確化が必要な点があれば教えてください
「いいね!」 35
angus
(Angus McLeod)
2018 年 3 月 20 日午後 12:52
2
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” 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
fzngagan
(Faizaan Gagan)
2019 年 8 月 4 日午前 6:12
3
@angus @gdpelican Thanks for this. This is great stuff. . This would be really essential all the (especially newbies like me) plugin developers out there.
gdpelican:
PS, always call super ! This makes your plugin far less likely to break when the underlying implementation changes. Unless you’re really, really sure that your functionality completely replaces everything in the underlying method, you want to call super and modify the results from there, so that changes to this method in Discourse core don’t make your plugin break later.
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
こんにちは、コードのこの部分について質問があります。なぜこのrequire_dependencyが必要なのでしょうか?これなしでもコードは動作しているように見えるのですが。
angus
(Angus McLeod)
2020 年 5 月 31 日午後 11:09
5
はい、その通りです。その投稿以降、Discourse の Rails 使用に関する更新により、require_dependency は不要となりました。その点について投稿を編集することはできません。詳細は以下をご覧ください:
「いいね!」 3
モジュールクラスをオーバーライドする際のヒントはありますか?GroupGuardian にいくつかの変更を加えたいと考えています(特別な種類のグループに対する特別な条件)。
よろしくお願いします。
「いいね!」 1
michaeld
(Michael - Communiteq)
2022 年 1 月 30 日午後 8:05
7
「いいね!」 3
DiscourseとRails開発初心者です。ローカルでDev Container環境(VS Code内)を使用しています。ガイドやドキュメントは参考になりました。
コアなDiscourseクラスをオーバーライドする方法について、特にローカル開発環境で永続化させるためのヒントがあれば教えていただけますでしょうか。
プラグインで、コアなDiscourseのTopicEmbedクラスのメソッドをオーバーライドしようとしています。(上記の@angusさんが丁寧に文書化してくれた一般的なアプローチを使用しています。)VS Codeを再構築してリロードしたときは一度機能しますが、その後のHTTPリクエストではオーバーライドが呼び出されません。
オーバーライドは/plugins/my-plugin/app/models/override.rbで定義されており、plugin.rbにこのファイルをインクルードするためにrequire_relativeを使用しています。
#override.rb:
class ::TopicEmbed
# TopicEmbed.singleton_class に prepend されるモジュール
module TopicEmbedOverrideModule
# TopicEmbed のメソッド
def first_paragraph_from(html)
Rails.logger.info(“私のオーバーライドが実行されています!”)
# TopicEmbed によって提供される元の実装を続行します。
super
end
end
# ここで prepend を実行します
singleton_class.prepend TopicEmbedOverrideModule
end
この永続化の課題は、開発環境とRubyコードのコンパイル/キャッシュの方法によるものだと疑っています。
rm -rf tmp; bin/ember-cli -uやbundle exec rake tmp:cache:clearも試しました。
これでシングルトンクラスで動作するようになりました。
# my overrides.rb
# TopicEmbed.singleton_class にプリペンドされるモジュール
module TopicEmbedOverrides
# parse_html メソッドをオーバーライド
def parse_html(html, url) # 注: ここでは self. を使用していません
# ここに新しい処理
# その後、元の実装を実行
super
end
end
# オーバーライドを実行
class ::TopicEmbed
singleton_class.prepend TopicEmbedOverrides
end