2010-06-14 7 views
1

J'ai beaucoup entendu parler de Ruby pour ses capacités de métaprogrammation super spectaculaires, et je me demandais si quelqu'un pouvait m'aider à résoudre ce problème.Passer à la métaprogrammation Ruby: Générer des méthodes proxy pour plusieurs méthodes internes

J'ai une classe qui fonctionne comme une sorte d '"archive", avec des méthodes internes qui traitent et produisent des données basées sur une entrée. Cependant, les éléments de l'archive de la classe elle-même sont représentés et traités avec des entiers, à des fins de performance. Les éléments réels en dehors de l'archive sont connus par leur représentation sous forme de chaîne, qui est simplement number_representation.to_s (36). Pour cette raison, j'ai raccordé chaque méthode interne avec une "méthode proxy" qui convertit l'entrée dans la forme entière que l'archive reconnaît, exécute la méthode interne et convertit la sortie (soit un seul autre élément, ou une collection d'entre eux) de retour en chaînes.

La convention de dénomination est la suivante: les méthodes internes sont représentées par _method_name; leur méthode de proxy correspondante est représentée par nom_méthode, sans trait de soulignement.

Par exemple:

class Archive 

    ## PROXY METHODS ## 
    ## input: string representation of id's 
    ## output: string representation of id's 

    def do_something_with id 
    result = _do_something_with id.to_i(36) 
    return nil if result == nil 
    return result.to_s(36) 
    end 

    def do_something_with_pair id_1,id_2 
    result = _do_something_with_pair id_1.to_i(36), id_2.to_i(36) 
    return nil if result == nil 
    return result.to_s(36) 
    end 

    def do_something_with_these ids 
    result = _do_something_with_these ids.map { |n| n.to_i(36) } 
    return nil if result == nil 
    return result.to_s(36) 
    end 

    def get_many_from id 
    result = _get_many_from id 
    return nil if result == nil   # no sparse arrays returned 
    return result.map { |n| n.to_s(36) } 
    end 

    ## INTERNAL METHODS ## 
    ## input: integer representation of id's 
    ## output: integer representation of id's 

    private 

    def _do_something_with id 
    # does something with one integer-represented id, 
    # returning an id represented as an integer 
    end 

    def do_something_with_pair id_1,id_2 
    # does something with two integer-represented id's, 
    # returning an id represented as an integer 
    end 

    def _do_something_with_these ids 
    # does something with multiple integer ids, 
    # returning an id represented as an integer 
    end 

    def _get_many_from id 
    # does something with one integer-represented id, 
    # returns a collection of id's represented as integers 
    end 
end 

Il y a deux raisons pour lesquelles je ne peux pas les convertir si id.class == chaîne au début des méthodes internes:

  1. Ces les méthodes internes sont des fonctions récursives un peu intensives en calcul, et je ne veux pas que le temps de contrôle soit multiplié à chaque étape.
  2. Il n'est pas possible, sans ajouter de paramètre supplémentaire, de dire s'il faut reconvertir à la fin
  3. Je veux penser à cela comme un exercice de compréhension ruby ​​méta-programmation

Est-ce que quelqu'un a des idées?


modifier

La solution que je voudrais serait de préférence être en mesure de prendre un tableau de noms de méthode

@@PROXY_METHODS = [:do_something_with, :do_something_with_pair, 
        :do_something_with_these, :get_many_from] 

itérer à travers eux, et à chaque itération, mettre la méthode de proxy. Je ne suis pas sûr de ce qui serait fait avec les arguments, mais y a-t-il un moyen de tester les arguments d'une méthode? Si ce n'est pas le cas, alors un simple typage de canard/un concept analogue ferait aussi bien.


Je suis venu avec ma propre solution, en utilisant #class_eval

@@PROXY_METHODS.each do |proxy| 
    class_eval %{ def #{proxy} *args 
        args.map! do |a| 
        if a.class == String 
         a.to_i(36) 
        else 
         a.map { |id| id.to_i(36) } 
        end 
        end 
        result = _#{proxy}(*args) 

        result and if result.respond_to?(:each) 
           result.map { |r| r.to_s(36) } 
          else 
           result.to_s(36) 
          end 
       end 
       } 
end 

Cependant, #class_eval semble un peu ... désordre? ou inélégant par rapport à ce qu'il "devrait" être.

Répondre

2
class Archive 
    # define a new method-creating method for Archive by opening the 
    # singleton class for Archive 
    class << Archive 
    private # (make it private so no one can call Archive.def_api_method) 
    def def_api_method name, &defn 
     define_method(name) do |*args| 
     # map the arguments to their integer equivalents, 
     # and pass them to the method definition 
     res = defn[ *args.map { |a| a.to_i(36) } ] 
     # if we got back a non-nil response, 
     res and if res.respond_to?(:each) 
        # map all of the results if many returned 
        res.map { |r| r.to_s(36) } 
       else 
        # map the only result if only one returned 
        res.to_s(36) 
       end 
     end 
    end 
    end 
    def_api_method("do_something_with"){ |id| _do_something_with(id) } 
    def_api_method("do_something_with_pair"){ |id_1, id_2| _do_something_with_pair id_1.to_i(36), id_2.to_i(36) } 
    #... 
end 

Au lieu d'ouvrir le singleton pour définir Archive.def_api_method, vous pouvez le définir en utilisant simplement

class Archive 
    def Archive.def_api_method 
    #... 

Mais la raison pour laquelle je ne le faisais pas est alors toute personne ayant accès à la classe Archive pourrait invoquer en utilisant Archive.def_api_method. Ouverture de la classe singleton m'a permis de marquer def_api_method comme privé, de sorte qu'il ne peut être invoqué lorsque self == Archive.

Si vous appelez toujours une version interne avec le même nom (ou dérivable), vous pouvez l'appeler directement (plutôt que de passer un bloc de définition) en utilisant #send.

class Archive 
    # define a method-creating method that wraps an internal method for external use 
    class << Archive 
    private # (make it private so no one can call Archive.api_method) 
    def api_method private_name 
     public_name = private_name.to_s.sub(/^_/,'').to_sym 
     define_method(public_name) do |*args| 
     # map the arguments to their integer equivalents, 
     # and pass them to the private method 
     res = self.send(private_name, *args.map { |a| a.to_i(36) }) 
     # if we got back a non-nil response, 
     res and if res.respond_to?(:each) 
        # map all of the results if many returned 
        res.map { |r| r.to_s(36) } 
       else 
        # map the only result if only one returned 
        res.to_s(36) 
       end   end 
     # make sure the public method is publicly available 
     public public_name 
    end 
    end 

    api_method :_do_something_with 
    api_method :_do_something_with_pair 

    private 

    def _do_something_with 
    #... 
    end 
    def _do_something_with_pair 
    #... 
    end 
end 

C'est plus comme ce qui se fait par d'autres méta-méthodes comme attr_reader et attr_writer.

+0

merci! Cela fonctionne très bien. Juste une petite question - est-ce important de créer une classe singleton, ou pourriez-vous simplement définir self.def_api_method (en termes de convention/style)? En outre, que voulez-vous dire par la dernière chose que vous avez écrite, sur l'utilisation de #send? Merci beaucoup! –

+0

@mstksg: J'en ai ajouté quelques-unes sur la façon dont ça se ferait avec 'send'. Est-ce plus clair? – rampion

+0

merci! Je crois que c'est exactement ce que je cherchais :) –

Questions connexes