Ho notato diverse situazioni recenti in cui è necessario sovrascrivere metodi Ruby esistenti forniti da plugin e ho pensato di condividere qui le mie migliori pratiche.
Sovrascrittura di un metodo di istanza
class ::TopicQuery
module BabbleDefaultResults
def default_results(options={})
super(options).where('archetype <> ?', Archetype.chat)
end
end
prepend BabbleDefaultResults
end
Qui sto rimuovendo gli argomenti di chat da un metodo di istanza che restituisce un elenco di argomenti.
Il nome del modulo BabbleDefaultResults può essere quello che preferisci; di solito lo faccio coincidere con il nome del metodo più il nome del mio plugin per minimizzare i rischi di conflitto di nomi (anche se sono già piuttosto bassi).
Module#prepend è davvero utile e dovresti conoscerlo se stai scrivendo plugin per qualsiasi cosa in Ruby. Nota che è il fatto che stiamo preponendo un modulo a permetterci di chiamare super all’interno del metodo di sovrascrittura.
PS, chiama sempre super! Questo rende il tuo plugin molto meno propenso a rompersi quando l’implementazione sottostante cambia. A meno che tu non sia davvero, davvero sicuro che la tua funzionalità sostituisca completamente tutto nel metodo sottostante, vuoi chiamare super e modificare i risultati da lì, in modo che le modifiche a questo metodo nel core di Discourse non facciano rompere il tuo plugin in futuro.
Il :: in ::TopicQuery garantisce che mi riferisca alla classe TopicQuery di livello superiore da sovrascrivere e non a una sua versione modularizzata (come Babble::TopicQuery).
Questo può essere inserito direttamente in plugin.rb così com’è, oppure, se il tuo plugin è grande, puoi valutare di separare ogni sovrascrittura in un file distinto.
Sovrascrittura di un metodo di classe
class ::Topic
module BabbleForDigest
def for_digest(user)
super(user).where('archetype <> ?', Archetype.chat)
end
end
singleton_class.prepend BabbleForDigest
end
Qui sto prendendo un metodo self.for_digest esistente sulla classe Topic e rimuovendo gli argomenti di chat dal risultato.
Molto simile alla sovrascrittura del metodo di istanza, nota la differenza: stiamo chiamando singleton_class.prepend invece di semplice prepend. singleton_class è un modo leggermente strano per dire “voglio aggiungere questo a livello di classe, non a livello di istanza”; ulteriori letture se stai cercando un tunnel di coniglio legato a Ruby.
Sovrascrittura di uno scope
class ::Topic
@@babble_listable_topics = method(:listable_topics).clone
scope :listable_topics, ->(user) {
@@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
}
end
Questa è un po’ delicata perché gli scope non si integrano bene con super (o, almeno, non sono riuscito a farli funzionare). Quindi, invece, stiamo prendendo una definizione di metodo esistente, la cloniamo, la memorizziamo e poi la chiamiamo in un secondo momento.
Ancora una volta, @@babble_listable_topics può essere qualsiasi cosa tu voglia, ma usare il nome del tuo plugin come namespace è probabilmente una buona idea.
Maggiori informazioni sulla funzione method, che è anche davvero utile, anche se i casi in cui ne avresti davvero bisogno sono piuttosto rari. Bonus: una curiosità gratuita correlata; quando stai facendo il debug, se hai difficoltà a capire quale codice viene eseguito per una specifica chiamata a un metodo (solitamente “Quale gem sta definendo questo metodo?”), puoi usare source_location per ottenere la riga esatta del codice sorgente in cui è definito il metodo.
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
@angus@gdpelican Thanks for this. This is great stuff. . 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.
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:
Qualche consiglio per sovrascrivere le classi dei moduli? Voglio apportare alcune modifiche a GroupGuardian (alcune condizioni speciali per un tipo speciale di gruppo).
Nuovo in discourse e nello sviluppo Rails. Sto usando l’ambiente Dev Container (in VS Code) localmente. Le guide e la documentazione sono state utili.
Mi chiedevo se qualcuno avesse qualche consiglio su come sovrascrivere le classi principali di discourse, in particolare come farla persistere in un ambiente di sviluppo locale.
Nel mio plugin, sto cercando di sovrascrivere un metodo nella classe principale di discourse TopicEmbed. (utilizzando l’approccio generale ben documentato da @angus sopra.) Funziona una volta quando ricostruisco e ricarico VS Code, ma nelle successive richieste http la mia sovrascrittura non viene mai invocata.
La mia sovrascrittura è definita in /plugins/my-plugin/app/models/override.rb e uso require_relative per includere questo file nel mio plugin.rb.
#override.rb:
class ::TopicEmbed
# un modulo che verrà anteposto alla singleton_class di TopicEmbed.
module TopicEmbedOverrideModule
# metodo in TopicEmbed
def first_paragraph_from(html)
Rails.logger.info(“la mia sovrascrittura sta avvenendo! ”)
# continua con l'implementazione originale fornita da TopicEmbed.
super
end
end
# fai l'anteposizione qui
singleton_class.prepend TopicEmbedOverrideModule
end
Sospetto che questa mia sfida di persistenza possa essere dovuta al mio ambiente di sviluppo e a come il codice ruby viene compilato/messo in cache.
Ho anche provato rm -rf tmp; bin/ember-cli -u e bundle exec rake tmp:cache:clear.
l’ho fatto funzionare per una classe singleton in questo modo:
# my overrides.rb
# Un modulo che verrà anteposto a TopicEmbed.singleton_class
module TopicEmbedOverrides
# Sovrascrivi il metodo parse_html
def parse_html(html, url) # nota: non uso self. qui
# le mie nuove cose qui
# quindi esegui l'implementazione originale
super
end
end
# fai l'override qui
class ::TopicEmbed
singleton_class.prepend TopicEmbedOverrides
end