Vol. 6 · № 02 · Un atelier RubyA Ruby workshop

define_method, ou la magie qu'on peut retrouver define_method, or the magic you can find again

define_method crée de vraies méthodes à partir d'un nom calculé : listées, vues par respond_to?, trouvées par grep — l'inverse de l'interception. attr_accessor n'est que cette boucle. Le réflexe : générer si les noms sont des données ou nombreux ; sinon, def gagne. define_method creates real methods from a computed name: listed, seen by respond_to?, found by grep — the opposite of interception. attr_accessor is just that loop. The reflex: generate if names are data or many; otherwise, def wins.

AudienceAudience
Dev qui veut générer du code sans le rendre introuvable Dev who wants to generate code without making it unfindable
Format
Self-paced
ChapitresChapters
5
Date
Mai 2026 May 2026
≈ 18 min ●●●○ define_methodattr_accessorDSL
SommaireContents ·
№ 01CadrageFraming3 min

define_method fabrique une vraie méthode, à partir d'un nom.define_method forges a real method, from a name.

Le № 1 interceptait les messages inconnus ; voici la moitié lumineuse du volume. Plutôt que de détourner method_missing, define_method crée des méthodes réelles à l'exécution, à partir d'un nom calculé et d'un bloc. La différence avec method_missing est totale : ces méthodes existent vraiment. instance_methods les liste, respond_to? les voit, un grep les trouve, le débogueur s'y arrête. C'est la métaprog qui produit du code visible au lieu de le cacher — et le seul moyen de générer une famille de méthodes dont les noms sont des données, pas du texte écrit à la main.№ 1 intercepted unknown messages; here's the volume's bright half. Rather than hijacking method_missing, define_method creates real methods at runtime, from a computed name and a block. The difference from method_missing is total: these methods actually exist. instance_methods lists them, respond_to? sees them, a grep finds them, the debugger stops in them. It's the metaprogramming that produces visible code instead of hiding it — and the only way to generate a family of methods whose names are data, not hand-written text.

Générer des accesseurs depuis une liste de nomsGenerating accessors from a list of names
class Person
  [:name, :email].each do |attr|              # une boucle sur des noms
    define_method(attr) do                     # crée une VRAIE méthode lecteur
      instance_variable_get("@#{attr}")
    end
    define_method("#{attr}=") do |value|       # … et son écrivain
      instance_variable_set("@#{attr}", value)
    end
  end
end

p = Person.new
p.name = "Ada"
p.name                          # => "Ada"
Person.instance_methods(false).sort  # => [:email, :email=, :name, :name=]
                                     #    de vraies méthodes : listées, grep-ables
                                     #    (.sort : l'ordre de retour n'est pas garanti)
def vs define_methoddef vs define_method
def name
nom LITTÉRAL, écrit à la mainLITERAL name, hand-written
scope neuf : oublie l'entouragefresh scope: forgets surroundings
define_method(x)
nom CALCULÉ, à l'exécutionCOMPUTED name, at runtime
bloc = closure : capture x (Vol 2)block = closure: captures x (Vol 2)

def exige un nom écrit et ouvre un scope neuf. define_method prend un nom calculé et un bloc — donc une closure (Vol 2) qui capture la variable de boucle. C'est exactement ce qui permet de générer une méthode par nom.def demands a written name and opens a fresh scope. define_method takes a computed name and a block — hence a closure (Vol 2) capturing the loop variable. That's precisely what lets you generate one method per name.

Le réflexe du numéroThe issue's reflex

« Ai-je vraiment besoin de générer ce comportement, ou une méthode explicite suffisait-elle ? » define_method paie quand les noms sont des données (une liste de champs, une config) ou quand ils sont nombreux. Pour deux ou trois méthodes distinctes écrites une fois, def reste plus clair."Do I really need to generate this behavior, or would an explicit method have done?" define_method pays when names are data (a field list, a config) or when they're many. For two or three distinct methods written once, def stays clearer.

Le bloc est une closure.The block is a closure.

La raison technique pour laquelle define_method marche en boucle là où def échouerait : son bloc est une closure (Vol 2), il capture attr de l'itération courante. Un def attr dans la boucle, lui, n'aurait pas accès à la variable — def coupe le lien avec le scope englobant. La métaprog d'ici n'invente rien : elle réutilise le bloc-closure du Vol 2.The technical reason define_method works in a loop where def wouldn't: its block is a closure (Vol 2), it captures the current iteration's attr. A def attr in the loop would have no access to the variable — def cuts the link to the enclosing scope. The metaprogramming here invents nothing: it reuses Vol 2's closure block.

№ 02La surpriseThe surprise4 min

attr_accessor n'est pas un mot-clé : c'est cette boucle.attr_accessor isn't a keyword: it's this loop.

La surprise qui démystifie tout le volume : attr_accessor n'est pas une construction du langage, c'est une méthode de classe ordinaire qui appelle define_method en boucle. Rien de réservé, rien de magique — on peut la réécrire en dix lignes, et c'est instructif de le faire. Elle révèle le motif central de la métaprog déclarative : une méthode appelée au niveau de la classe (via extend, Vol 5) qui génère des méthodes d'instance. Tous les attr_*, et la moitié des macros de Rails, ne sont que ça : du define_method habillé d'un joli nom.The surprise that demystifies the whole volume: attr_accessor isn't a language construct, it's an ordinary class method calling define_method in a loop. Nothing reserved, nothing magic — you can rewrite it in ten lines, and it's instructive to do so. It reveals the central pattern of declarative metaprogramming: a method called at the class level (via extend, Vol 5) that generates instance methods. All the attr_*, and half of Rails's macros, are just that: define_method dressed in a nice name.

La macro familière qu'on croit réservéeThe familiar macro we think is reserved
class Point
  attr_accessor :x, :y      # « magie » familière : lecteurs + écrivains
end

p = Point.new
p.x = 3
p.x                         # => 3
Point.instance_methods(false).sort   # => [:x, :x=, :y, :y=]

attr_accessor :x, :y ressemble à un mot-clé, mais c'est un simple appel de méthode exécuté pendant la définition de la classe. Il reçoit des symboles et génère quatre méthodes. Aucune syntaxe spéciale : juste une méthode de Module que Point hérite et appelle.attr_accessor :x, :y looks like a keyword, but it's a plain method call run during the class definition. It receives symbols and generates four methods. No special syntax: just a Module method that Point inherits and calls.

La « magie » de attr_accessor est une boucle define_method avec un joli nom — et tu peux écrire la tienne.The "magic" of attr_accessor is a define_method loop with a nice name — and you can write your own.
№ 03La mécaniqueThe mechanics5 min

Une macro de classe génère ; class_eval et send complètent.A class macro generates; class_eval and send complete it.

Le motif déclaratif se précise. field :name, écrit dans le corps de la classe, n'est qu'un appel de méthode de classe qui fait define_method — c'est le squelette de tout DSL Ruby, des validations Rails aux schémas dry-struct du Vol 3. Deux outils l'entourent. class_eval exécute un bloc dans le contexte d'une classe, comme si on rouvrait sa définition — utile pour ajouter des méthodes à une classe qu'on n'a pas écrite (on y reviendra au № 3). Et send appelle une méthode dont le nom est une donnée, complétant la génération : on crée des méthodes par leur nom, on les appelle par leur nom.The declarative pattern sharpens. field :name, written in the class body, is just a class-method call doing define_method — the skeleton of every Ruby DSL, from Rails validations to Vol 3's dry-struct schemas. Two tools surround it. class_eval runs a block in a class's context, as if reopening its definition — useful to add methods to a class you didn't write (more on that in № 3). And send calls a method whose name is data, completing generation: you create methods by name, you call them by name.

Un DSL déclaratif : field se lit comme une donnéeA declarative DSL: field reads like data
class Model
  def self.field(name)              # une macro déclarative au niveau classe
    define_method(name) { @attrs[name] }
    define_method("#{name}=") { |v| @attrs[name] = v }
  end

  def initialize = @attrs = {}
end

class User < Model
  field :name                       # se lit comme une déclaration
  field :age
end

u = User.new
u.name = "Ada"
u.name                             # => "Ada"   field a généré de vraies méthodes
class_eval ouvre un contexte ; send appelle par nomclass_eval opens a context; send calls by name
# class_eval : exécuter du code DANS le contexte d'une classe
target = String
target.class_eval do                # le bloc s'exécute « comme si on était dans String »
  def shout = upcase + "!"
end
"hi".shout                          # => "HI!"

# send : appeler une méthode dont le nom est une donnée (Vol 1)
record = User.new
%i[name age].each do |field|
  record.send("#{field}=", "?")     # send force aussi le privé ; public_send sinon
end
Le bloc, jamais la chaîne.The block, never the string.

class_eval et define_method acceptent un bloc — la bonne forme : code réel, coloré, vérifié, qui capture son scope. Ils acceptent aussi une chaîne (class_eval("def …")) : à fuir presque toujours. Une chaîne interpolée, c'est de l'injection possible, aucune coloration, et des erreurs pointant une ligne fantôme. send, lui, prolonge le « tout est message » du Vol 1 : le nom de la méthode redevient une donnée qu'on calcule. Préfère public_send si tu ne veux pas forcer le privé.class_eval and define_method accept a block — the good form: real, highlighted, checked code that captures its scope. They also accept a string (class_eval("def …")): almost always to avoid. An interpolated string means possible injection, no highlighting, and errors pointing at a phantom line. send, for its part, extends Vol 1's "everything is a message": the method name becomes computable data again. Prefer public_send if you don't want to force private.

Ce trio — define_method pour créer, class_eval pour ouvrir un contexte, send pour appeler par nom — couvre la quasi-totalité de la métaprog constructive. Toute la différence avec le № 1 tient là : on produit des méthodes que les outils retrouvent, au lieu d'intercepter des messages qu'ils ne voient pas. Reste à savoir quand ne pas s'en servir.This trio — define_method to create, class_eval to open a context, send to call by name — covers nearly all constructive metaprogramming. The whole difference from № 1 sits there: you produce methods tools can find, instead of intercepting messages they can't see. What remains is knowing when not to use it.

№ 04Le piègeThe trap4 min

Générer trois méthodes qu'on aurait écrites plus vite à la main.Generating three methods you'd have written faster by hand.

Le contre-exemple du numéro, et le piège du dev qui vient de découvrir define_method. Grisé par l'outil, il génère tout — y compris trois méthodes distinctes qu'un def aurait dites plus clairement et en moins de lignes. La génération ne paie que quand les noms sont des données (une liste qui grandit, une config externe) ou quand ils sont vraiment nombreux. Sur un petit ensemble fixe, elle ne fait qu'ajouter de l'opacité : grep ne trouve rien, source_location pointe la boucle au lieu du comportement, et le lecteur doit exécuter la table mentalement. Et la variante en chaîne (class_eval d'un texte interpolé) ajoute l'injection à l'illisibilité.The issue's counter-example, and the trap of the dev who just discovered define_method. Drunk on the tool, they generate everything — including three distinct methods a def would have stated more clearly and in fewer lines. Generation only pays when names are data (a growing list, an external config) or when they're genuinely many. On a small fixed set, it only adds opacity: grep finds nothing, source_location points at the loop instead of the behavior, and the reader must run the table in their head. And the string variant (class_eval of interpolated text) adds injection to illegibility.

Générer du fixe : opaque, et pas plus courtGenerating the fixed: opaque, and no shorter
# Le piège : un générateur pour 2 méthodes qui n'ont rien de commun
class Report
  { summary: :short, detail: :long, audit: :full }.each do |name, mode|
    define_method(name) { render(@data, mode) }
  end
end

# où est défini summary ? que fait audit ? il faut LIRE la boucle et la table.
# $ grep "def summary"    → rien
Report.new.method(:detail).source_location   # pointe la ligne du define_method,
                                              # pas le comportement — debug opaque
# pire encore, la variante en chaîne :
# klass.class_eval("def #{name}; render(@data, :#{mode}); end")  ← injection, zéro coloration
L'explicite, quand l'ensemble est petit et fixeThe explicit, when the set is small and fixed
# Trois méthodes distinctes ? On les écrit. C'est tout.
class Report
  def summary = render(@data, :short)
  def detail  = render(@data, :long)
  def audit   = render(@data, :full)
end

# $ grep "def summary"    → trouvé, à sa ligne
Report.new.method(:detail).source_location   # pointe la vraie définition
# lisible, traçable, débogable — et plus COURT que le générateur
Le contre-exemple obligatoire.The mandatory counter-example.

Le test avant de générer : les noms sont-ils des données ou du texte fixe ? Données (champs d'un schéma, colonnes d'une table, liste qui évolue) ⇒ define_method rend service, et reste traçable. Texte fixe (trois méthodes que tu pourrais lister là, maintenant) ⇒ écris-les : c'est plus court, grep les trouve, source_location pointe juste. La génération n'est DRY que si la répétition est réelle ; sur trois cas distincts, elle troque la clarté contre rien. Et jamais la forme en chaîne quand le bloc fait l'affaire.The test before generating: are the names data or fixed text? Data (schema fields, table columns, an evolving list) ⇒ define_method earns its keep, and stays traceable. Fixed text (three methods you could list right here, now) ⇒ write them: it's shorter, grep finds them, source_location points right. Generation is only DRY if the repetition is real; on three distinct cases, it trades clarity for nothing. And never the string form when the block will do.

Générer n'est DRY que si la répétition est réelle. Sur trois cas distincts, c'est de la magie qui coûte la clarté — pour rien.Generating is only DRY if the repetition is real. On three distinct cases, it's magic that costs clarity — for nothing.
№ 05Bilan & éditoWrap-up & editorial2 min

Génère quand les noms sont des données — sinon, écris-les.Generate when names are data — otherwise, write them out.

define_method crée de vraies méthodes à partir d'un nom calculé : listées, vues par respond_to?, trouvées par grep — l'inverse de l'interception du № 1. attr_accessor n'est que cette boucle sous un joli nom, et tout DSL déclaratif est une macro de classe qui appelle define_method. class_eval ouvre un contexte, send appelle par nom — toujours en bloc, jamais en chaîne. Et la règle qui tient tout : on génère si les noms sont des données ou nombreux ; sinon, l'explicite est plus court et plus clair.define_method creates real methods from a computed name: listed, seen by respond_to?, found by grep — the opposite of № 1's interception. attr_accessor is just that loop under a nice name, and every declarative DSL is a class macro calling define_method. class_eval opens a context, send calls by name — always as a block, never a string. And the rule that holds it all: generate if names are data or many; otherwise, the explicit is shorter and clearer.

L'outilThe toolCe qu'il faitWhat it doesLe réflexeThe reflex
define_methoddefine_methodCrée une vraie méthode depuis un nom + un bloc.Creates a real method from a name + a block.Quand les noms sont des données, ou nombreux.When names are data, or many.
attr_* / macroattr_* / macroUne méthode de classe qui boucle sur define_method.A class method looping over define_method.Écris la tienne quand un vrai motif se répète.Write your own when a real pattern repeats.
class_eval / sendclass_eval / sendOuvrir un contexte ; appeler par nom (Vol 1).Open a context; call by name (Vol 1).Toujours en bloc ; public_send pour le public.Always a block; public_send for public.
def (l'explicite)def (the explicit)Une méthode nommée, écrite à la main.A named method, written by hand.Le défaut sur un ensemble petit et fixe.The default on a small, fixed set.
La métaprog constructive produit ce que le № 1 interceptait — des méthodes que grep retrouve. Mais générer reste un coût : ne le paie que pour une vraie répétition.Constructive metaprogramming produces what № 1 intercepted — methods grep finds. But generating is still a cost: pay it only for real repetition.
Mot de l'éditeurFrom the editor

La métaprog qui montre, pas celle qui cache.The metaprogramming that shows, not the one that hides.

Ce numéro est le contrepoint lumineux du précédent. method_missing intercepte des messages invisibles ; define_method produit des méthodes que tous les outils retrouvent. La même puissance — manipuler le code à l'exécution, conséquence directe du « tout est objet » du Vol 1 — mais retournée vers la clarté. Démystifier attr_accessor n'est pas une curiosité : c'est comprendre que Ruby n'a presque pas de mots-clés magiques, juste des méthodes qui se manipulent. Une fois ça vu, on écrit ses propres macros avec mesure — et on garde en tête le seul garde-fou qui compte ici : générer n'est utile que là où la répétition est réelle. Partout ailleurs, def gagne.This issue is the bright counterpoint to the last. method_missing intercepts invisible messages; define_method produces methods every tool can find. The same power — manipulating code at runtime, a direct consequence of Vol 1's 'everything is an object' — but turned toward clarity. Demystifying attr_accessor isn't a curiosity: it's grasping that Ruby has almost no magic keywords, just methods that manipulate methods. Once seen, you write your own macros with measure — keeping in mind the only guardrail that matters here: generating helps only where repetition is real. Everywhere else, def wins.

Prochain numéro : classes ouvertes, monkey-patch & refinements. On a généré des méthodes dans NOS classes ; le № 3 rouvre n'importe quelle classe — même String, même Integer — pour y ajouter ou changer du comportement. Le pouvoir est total et global, et c'est précisément le danger : un patch d'String casse silencieusement le code d'autrui à distance. On verra le contraste entre le patch global et le refinement, local à un fichier — et pourquoi la portée est toute la question.Next issue: open classes, monkey-patching & refinements. We generated methods in OUR classes; № 3 reopens any class — even String, even Integer — to add or change behavior. The power is total and global, and that's precisely the danger: a String patch silently breaks others' code at a distance. We'll see the contrast between the global patch and the refinement, local to a file — and why scope is the whole question.