2010-03-04 6 views
4

J'ai un groupe d'étendues nommées et j'ai une méthode dans l'une d'entre elles que j'aimerais partager entre les autres étendues nommées. J'ai en quelque sorte accompli cela en utilisant define_method et un lambda. Cependant, il y a encore du code répété et je me demande s'il y a une meilleure approche?Méthodes de partage entre des étendues nommées

Voici un exemple simplifié de ce que j'ai. Supposons que j'ai une table de projets et que chaque projet compte de nombreux utilisateurs.

Dans le modèle de l'utilisateur ... Je

filter_by_name = lambda { |name| detect {|user| user.name == name} } 

named_scope :active, :conditions => {:active => true} do 
    define_method :filter_by_name, filter_by_name 
end 

named_scope :inactive, :conditions => {:active => false} do 
    define_method :filter_by_name, filter_by_name 
end 

named_scope :have_logged_in, :conditions => {:logged_in => true} do 
    define_method :filter_by_name, filter_by_name 
end 

Alors je l'utiliser comme ...

active_users = Project.find(1).users.active 

some_users = active_users.filter_by_name ["Pete", "Alan"] 
other_users = active_users.filter_by_name "Rob" 

logged_in_users = Project.find(1).users.logged_in 

more_users = logged_in_users.filter_by_name "John" 

Répondre

2

Voici une solution entièrement différente qui est probablement plus en accord avec ce que la question demandait. Named_scope prend un bloc, ce qui pourrait être n'importe quel Proc. Donc, si vous créez un lambda/Proc qui définit la méthode filter_by_name, vous pouvez le passer comme dernier argument à un named_scope.

filter_by_name = lambda { |name| detect {|user| user.name == name} } 

add_filter_by_name = lambda { define_method :filter_by_name, filter_by_name } 

named_scope(:active, :conditions => {:active => true}, &add_filter_by_name) 

named_scope(:inactive, :conditions => {:active => false}, &add_filter_by_name) 

named_scope(:have_logged_in, :conditions => {:logged_in => true}, &add_filter_by_name) 

Ceci fera ce que vous cherchez. Si vous pensez toujours que c'est trop répétitif, vous pouvez le combiner avec les techniques de la solution de mrjake2 pour définir plusieurs étendues nommées à la fois. Quelque chose comme ceci:

method_params = { 
    :active => { :active => true }, 
    :inactive => { :active => false }, 
    :have_logged_in => { :logged_in => true } 
} 

filter_by_name = lambda { |name| detect {|user| user.name == name} } 

add_filter_by_name = lambda { define_method :filter_by_name, filter_by_name } 

method_params.keys.each do |method_name| 
    send(:named_scope method_name, :conditions => method_params[method_name], 
    &add_filter_by_name) 
end 
+0

J'aime vraiment votre approche ici. J'avais oublié que vous pouviez passer un bloc en utilisant & method. Cela fonctionne exactement comme j'ai besoin, merci! –

1

Je serais probablement utiliser un peu de metaprogramming:

method_params = { 
    :active => { :active => true }, 
    :inactive => { :active => false }, 
    :have_logged_in => { :logged_in => true } 
} 

method_params.keys.each do |method_name| 
    send :named_scope method_name, :conditions => method_params[method_name] do 
    define_method :filter_by_name, filter_by_name 
    end 
end 

De cette façon, si vous vouliez ajouter plus de trouveurs dans le futur, vous pourriez simplement ajouter le nom et les conditions de la méthode à methods_param ha sh.

+0

Merci pour l'entrée. Je vais tester et voir ce qui se passe. Aurais-je besoin d'utiliser 'send: named_scope'? Pourquoi ne pas simplement appelé named_scope directement? –

+0

C'est parce que vous êtes à l'intérieur de chaque boucle - vous n'êtes plus au niveau de la classe à ce moment-là parce que vous êtes dans un bloc de code. – mrjake2

2

Les portées nommées peuvent être enchaînées, ce qui vous rend la tâche plus difficile que nécessaire.

qui suit lorsqu'elle est définie dans le modèle utilisateur vous obtiendrez ce que vous voulez:

class User < ActiveRecord::Base 
    ... 
    named_scope :filter_by_name, lambda { |name| 
    {:conditions => { :name => name} } 
    } 

    named_scope :active, :conditions => {:active => true} 

    named_scope :inactive, :conditions => {:active => false} 

    named_scope :have_logged_in, :conditions => {:logged_in => true} 
end 

Ensuite, les extraits suivants fonctionneront:

active_users = Project.find(1).users.active 

some_users = active_users.filter_by_name(["Pete", "Alan"] 
other_users = active_users.filter_by_name "Rob" 

logged_in_users = Project.find(1).users.have_logged_in 

more_users = logged_in_users.filter_by_name "John" 

Je vois que vous utilisez detect, probablement pour éviter les hits excessifs à la DB. Mais vos exemples ne l'utilisent pas correctement. Detect ne renvoie que le premier élément d'une liste pour laquelle le bloc renvoie true. Dans l'exemple ci-dessus, some_users ne sera qu'un seul enregistrement, le premier utilisateur nommé "Pete" ou "Alan". Si vous cherchez à obtenir tous les utilisateurs nommés "Pete" ou "Alan" alors vous voulez select. Et si vous voulez select alors vous feriez mieux d'utiliser une portée nommée.

Les portées nommées, lorsqu'elles sont évaluées, renvoient un objet spécial contenant les composants nécessaires pour générer l'instruction SQL afin de générer les résultats. La chaînage d'autres étendues nommées n'exécute toujours pas l'instruction. Pas jusqu'à ce que vous essayiez d'accéder à des méthodes sur l'ensemble de résultats, telles que l'appel de chaque ou de la carte.

+0

J'ai oublié que les portées nommées peuvent être enchaînées - définitivement mieux votre solution. – mrjake2

+0

Merci pour la suggestion. J'étais conscient que c'était possible, mais j'essayais d'éviter les requêtes db redondantes. Dans mes tests, j'ai trouvé que chaîner d'autres étendues nommées sur 'active_users' construirait essentiellement toute la requête et l'exécuterait, plutôt que de simplement filtrer l'objet de la mémoire. Mon exemple a été très réduit, mais le code actuel a quelques modèles et conditions jointes, donc ce type d'optimisation semble important. –

0

Vous pouvez également le faire avec une deuxième portée nommée.

named_scope :active, :conditions => {:active => true} 
named_scope :inactive, :conditions => {:active => false} 
named_scope :have_logged_in, :conditions => {:logged_in => true} 
named_scope :filter_by_name, lambda {|name| :conditions => ["first_name = ? OR last_name = ?", name, name]} 

Ensuite, vous pouvez faire @project.users.active.filter_by_name('Francis').

Si vous avez vraiment besoin de le faire aveC# Enumerable détecter, je définirais la méthode filter_by_name dans un module qui peut alors étendre les champs d'application nommés:

with_options(:extend => FilterUsersByName) do |fubn| 
    fubn.named_scope :active, :conditions => {:active => true} 
    fubn.named_scope :inactive, :conditions => {:active => false} 
    fubn.named_scope :have_logged_in, :conditions => {:logged_in => true} 
end 

module FilterUsersByName 
    def filter_by_name(name) 
    detect {|user| user.name == name} 
    end 
end 

Ajoute la méthode filter_by_name à la classe retournée par tous les trois domaines nommés.

Questions connexes