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

Classes ouvertes, ou le monkey-patch et sa portée Open classes, or monkey-patching and its scope

Une classe Ruby n'est jamais fermée : on rouvre n'importe laquelle, même String. Mais il n'en existe qu'une — la rouvrir agit à distance, sur tout le programme. Le réflexe : ce patch doit-il être visible partout (monkey-patch), ou seulement dans mon fichier (refinement) ? Souvent, pas de patch du tout. A Ruby class is never closed: you reopen any of them, even String. But there's only one — reopening it acts at a distance, on the whole program. The reflex: must this patch be visible everywhere (monkey-patch), or only in my file (refinement)? Often, no patch at all.

AudienceAudience
Dev tenté de patcher String, prêt à en peser la portée Dev tempted to patch String, ready to weigh the scope
Format
Self-paced
ChapitresChapters
5
Date
Mai 2026 May 2026
≈ 18 min ●●●○ Classes ouvertesMonkey-patchrefinements
SommaireContents ·
№ 01CadrageFraming3 min

Aucune classe n'est fermée — on peut rouvrir même String.No class is closed — you can reopen even String.

Dernier barreau de l'échelle du volume, et le plus puissant. Le № 1 interceptait, le № 2 générait dans nos classes ; ici on rouvre n'importe quelle classe — la nôtre, celle d'une gem, ou String, Integer, Array du cœur de Ruby — pour y ajouter ou y changer du comportement. Ce n'est pas une faille : une classe Ruby n'est jamais close, écrire class String une seconde fois la rouvre. La conséquence directe du Vol 1 (les classes sont des objets qu'on manipule) et du Vol 5 (la méthode s'insère dans la chaîne d'ancêtres). La puissance est totale ; sa portée aussi — et c'est précisément le danger.The volume's last rung, and the most powerful. № 1 intercepted, № 2 generated in our classes; here you reopen any class — yours, a gem's, or String, Integer, Array from Ruby's core — to add or change behavior. It's not a loophole: a Ruby class is never closed, writing class String a second time reopens it. The direct consequence of Vol 1 (classes are objects you manipulate) and Vol 5 (the method inserts into the ancestor chain). The power is total; so is its reach — and that's precisely the danger.

Rouvrir String : tous les String gagnent la méthodeReopen String: every String gains the method
# une classe n'est jamais « fermée » : on la ROUVRE, même String
class String
  def shout = upcase + "!"
end

"hello".shout                 # => "HELLO!"
"".respond_to?(:shout)        # => true   TOUS les String l'ont désormais
"x".method(:shout).owner      # => String  la méthode vit DANS String même

# rien de spécial : on a juste ajouté un maillon à la classe String (Vol 5)
String.instance_methods(false).include?(:shout)   # => true
L'échelle du volume, dernier barreauThe volume's ladder, last rung
№ 1
intercepterintercept
un objetone object
›
№ 2
générergenerate
une classeone class
›
№ 3
rouvrirreopen
tout le programmethe whole program

Rouvrir une classe agit sur tout le programme à la fois : chaque String, partout, gagne la méthode — y compris ceux que manipulent d'autres bibliothèques. La portée maximale, donc le prix maximal.Reopening a class acts on the whole program at once: every String, everywhere, gains the method — including those other libraries handle. Maximum reach, hence maximum price.

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

« Ce patch doit-il être visible partout, ou seulement dans mon fichier ? » Si la réponse honnête est « mon fichier », un monkey-patch global est le mauvais outil : il impose à tout le programme un changement que toi seul voulais. Le refinement, lui, le garde local."Does this patch need to be visible everywhere, or only in my file?" If the honest answer is "my file", a global monkey-patch is the wrong tool: it forces on the whole program a change only you wanted. The refinement keeps it local.

Le dernier numéro du volume.The volume's last issue.

On clôt la métaprog par son outil le plus tranchant. Rouvrir une classe du cœur paraît pratique — ajouter blank? à String, to_slug, un helper bien à soi. Mais l'action porte à distance, sur du code qu'on n'a pas écrit. Tout le numéro tient dans une question de portée : global (monkey-patch) ou local (refinement) — et, souvent, pas de patch du tout.We close metaprogramming with its sharpest tool. Reopening a core class looks handy — adding blank? to String, to_slug, a helper of your own. But the action reaches at a distance, into code you didn't write. The whole issue is a question of scope: global (monkey-patch) or local (refinement) — and, often, no patch at all.

№ 02La surpriseThe surprise4 min

Un monkey-patch agit à distance, sur le code des autres.A monkey-patch acts at a distance, on everyone else's code.

Rouvrir une classe du cœur ne touche pas « ta » classe : il n'y en a qu'une, partagée par tout le processus. Ajouter ou écraser une méthode sur String ou Array la change pour chaque bout de code qui les utilise — le tien, mais aussi celui de chaque gem chargée. Si deux endroits définissent la même méthode, la dernière chargée gagne, sans erreur ni avertissement. C'est l'action à distance : un changement local en apparence, global en effet. Le refinement répond exactement à ça — il confine l'ajout au fichier qui l'active par using, et laisse la classe intacte partout ailleurs.Reopening a core class doesn't touch 'your' class: there's only one, shared by the whole process. Adding or overriding a method on String or Array changes it for every piece of code using them — yours, but also every loaded gem's. If two places define the same method, the last loaded wins, with no error or warning. That's action at a distance: a change local in appearance, global in effect. The refinement answers exactly that — it confines the addition to the file that activates it via using, and leaves the class intact everywhere else.

Le patch fuit dans tout le programmeThe patch leaks into the whole program
# Dans TON fichier, un patch global qui paraît anodin
class Array
  def sum = "total inconnu"      # tu écrases la vraie méthode sum de Ruby
end

[1, 2, 3].sum                    # => "total inconnu"   ← chez toi, ok

# … mais AUSSI partout ailleurs, à distance :
require "csv"                     # une gem qui fait des .sum en interne
prices.sum                       # cassé, silencieusement, dans du code
                                  # que tu n'as jamais lu — la dernière
                                  # définition chargée gagne, sans erreur

Il n'existe qu'un seul Array dans le processus. Le rouvrir change l'unique classe que tout le monde partage — y compris la gem CSV qui fait des .sum en interne. Le bug n'apparaît pas chez toi : il surgit au fond d'un code que tu n'as pas écrit, sans la moindre trace de qui a patché.There's only one Array in the process. Reopening it changes the single class everyone shares — including the CSV gem doing .sum internally. The bug doesn't show in your code: it erupts deep in code you didn't write, with no trace of who patched.

Il n'y a qu'un seul String dans tout le programme. Le rouvrir, c'est écrire dans la classe de tout le monde.There's only one String in the whole program. Reopening it is writing into everyone's class.
№ 03La mécaniqueThe mechanics5 min

refine décrit, using active — et la portée est lexicale.refine describes, using activates — and the scope is lexical.

Un refinement se fait en deux temps. refine String do … end, dans un module, décrit un ajout sans encore rien changer : la classe reste intacte. C'est using MonModule qui l'active — et seulement à partir de cette ligne, jusqu'à la fin du fichier, du module ou de la classe où il est écrit. La portée est lexicale : elle dépend de l'endroit où le code est écrit, pas de qui appelle quoi à l'exécution. C'est ce qui rend le refinement sûr là où le monkey-patch ne l'est pas — le changement ne peut pas fuir hors du texte qui l'a demandé. Le prix : quelques limites et un cran de lenteur.A refinement is two steps. refine String do … end, in a module, describes an addition without changing anything yet: the class stays intact. It's using MyModule that activates it — and only from that line, to the end of the file, module or class where it's written. The scope is lexical: it depends on where the code is written, not on who calls what at runtime. That's what makes the refinement safe where the monkey-patch isn't — the change can't leak beyond the text that asked for it. The price: a few limits and a notch of slowness.

refine + using : actif dans un fichier, pas dans l'autrerefine + using: active in one file, not the other
module Sluggable
  refine String do            # l'ajout est DÉCRIT, pas encore actif
    def to_slug = downcase.strip.gsub(/\s+/, "-")
  end
end

# fichier A : on l'active
using Sluggable               # portée lexicale : d'ici à la fin du fichier
"Hello World".to_slug         # => "hello-world"

# fichier B : on ne l'active pas
"Hello World".to_slug         # NoMethodError — String y est resté vierge
Portée lexicale, et ses limitesLexical scope, and its limits
# La portée est LEXICALE, pas dynamique : décidée par où le code est ÉCRIT
module Helper
  using Sluggable
  def self.run(s) = s.to_slug   # ok : le using couvre ce module
end

class Other
  def run(s) = s.to_slug         # NoMethodError : pas de using ici
end

# limites à connaître :
#  · ne s'active qu'au top-level d'un fichier, d'un module ou d'une classe
#  · invisible au code défini ailleurs, même appelé depuis ici
#  · un cran plus lent que l'appel direct
Sûr parce que confiné.Safe because confined.

Le monkey-patch modifie l'objet-classe global, vu de partout (portée dynamique, à distance). Le refinement, lui, n'existe que là où on a écrit using : deux fichiers, deux mondes. Mais ce confinement a des contreparties à connaître — il ne s'active qu'au sommet d'un fichier, d'un module ou d'une classe, il reste invisible au code défini hors de sa portée — une méthode d'une gem qui appelle to_slug ne le voit pas, même si tu l'appelles depuis ton fichier — et il coûte un peu à l'appel. Sûr, mais pas gratuit : on l'emploie quand on a vraiment besoin de patcher, et seulement localement.The monkey-patch modifies the global class-object, seen everywhere (dynamic scope, at a distance). The refinement only exists where you wrote using: two files, two worlds. But that confinement has trade-offs to know — it only activates at the top of a file, module or class, it stays invisible to code defined outside its scope — a gem method calling to_slug won't see it, even when you call it from your file — and it costs a little at call time. Safe, but not free: you use it when you really need to patch, and only locally.

Garde la hiérarchie des recours en tête : d'abord, pas de patch du tout — une fonction ou un module qui prend l'objet (Slug.from(str)) ne touche aucune classe et ne peut entrer en collision avec personne. Ensuite, si un patch est vraiment justifié, le refinement, local. Le monkey-patch global, en tout dernier, et seulement quand le changement doit être universel et que tu en assumes la portée.Keep the ladder of recourse in mind: first, no patch at all — a function or module that takes the object (Slug.from(str)) touches no class and can collide with no one. Then, if a patch is truly justified, the refinement, local. The global monkey-patch, last of all, and only when the change must be universal and you own its reach.

№ 04Le piègeThe trap4 min

String#to_slug en global dans une lib : la collision est une échéance.String#to_slug global in a lib: the collision is a deadline.

Le contre-exemple obligatoire du numéro. On ajoute String#to_slug en monkey-patch global, souvent dans une bibliothèque partagée — c'est élégant à l'appel, str.to_slug se lit bien. Mais on vient de réserver un nom sur l'unique String que tout le monde partage. Le jour où une autre gem, ou une version de Rails, définit to_slug autrement, la dernière chargée gagne : silencieusement, sans erreur, le comportement change selon l'ordre des require. Ce n'est pas un risque hypothétique, c'est une question de temps. Et la parade ne coûte presque rien : une fonction qui prend la chaîne ne touche aucune classe ; un refinement la garde locale.The issue's mandatory counter-example. You add String#to_slug as a global monkey-patch, often in a shared library — it's elegant at the call site, str.to_slug reads well. But you just reserved a name on the single String everyone shares. The day another gem, or a Rails version, defines to_slug differently, the last loaded wins: silently, with no error, behavior shifts with require order. It's not a hypothetical risk, it's a matter of time. And the remedy costs almost nothing: a function taking the string touches no class; a refinement keeps it local.

Réserver un nom sur le String de tout le mondeReserving a name on everyone's String
# Dans une gem PARTAGÉE : on ajoute to_slug à String, en global
class String
  def to_slug = downcase.strip.gsub(/\s+/, "-").gsub(/[^a-z0-9-]/, "")
end

"Été 2026".to_slug              # => "t-2026"   pratique, sur le moment

# … jusqu'au jour où une AUTRE gem définit aussi String#to_slug,
# autrement (en gardant les accents, par ex.). La dernière chargée gagne.
# Aucune erreur, aucun avertissement : juste un slug qui change selon
# l'ordre des require. La collision n'est pas un risque — c'est une échéance.
Une fonction, ou un refinement localA function, or a local refinement
# Option A — pas de patch : une fonction qui PREND la String
module Slug
  def self.from(str) = str.downcase.strip.gsub(/\s+/, "-").gsub(/[^a-z0-9-]/, "")
end
Slug.from("Été 2026")           # => "t-2026"   zéro classe touchée, zéro collision

# Option B — un refinement, si tu tiens à la syntaxe str.to_slug
module Sluggable
  refine String do
    def to_slug = Slug.from(self)
  end
end
# le fichier qui en a besoin fait « using Sluggable » — et lui seul
Le contre-exemple obligatoire.The mandatory counter-example.

Avant de patcher une classe que tu ne possèdes pas, demande : ce comportement doit-il vraiment vivre sur l'objet, ou peut-il être une fonction qui le prend ? Slug.from(str) rend exactement le même service, sans réserver de nom sur String, donc sans collision possible — jamais. Si la syntaxe str.to_slug compte vraiment, le refinement la donne, confinée au fichier qui l'active. Le monkey-patch global d'une classe du cœur, dans du code partagé, n'a presque aucune justification : sa commodité d'aujourd'hui est le bug d'ordre-de-chargement de demain.Before patching a class you don't own, ask: does this behavior really need to live on the object, or can it be a function taking it? Slug.from(str) renders exactly the same service, without reserving a name on String, hence with no possible collision — ever. If the str.to_slug syntax truly matters, the refinement gives it, confined to the file that activates it. The global monkey-patch of a core class, in shared code, has almost no justification: today's convenience is tomorrow's load-order bug.

Patcher une classe du cœur dans du code partagé, c'est parier qu'aucune autre gem ne choisira ce nom — un pari que le temps fait perdre.Patching a core class in shared code is betting no other gem will pick that name — a bet time makes you lose.
№ 05Bilan & éditoWrap-up & editorial2 min

Avant de patcher une classe : partout, ou seulement chez moi ?Before patching a class: everywhere, or only at home?

Une classe Ruby n'est jamais fermée : on rouvre n'importe laquelle, même String. Mais il n'en existe qu'une par processus — la rouvrir agit à distance, sur tout le code qui l'utilise, gems comprises. Le monkey-patch global est donc le dernier recours, pas le premier. La hiérarchie : d'abord pas de patch (une fonction qui prend l'objet), puis le refinement local si la syntaxe l'exige, et le patch global tout en bas, assumé. Le réflexe tient en une question : ce changement doit-il être visible partout, ou seulement dans mon fichier ?A Ruby class is never closed: you reopen any of them, even String. But there's only one per process — reopening it acts at a distance, on all the code using it, gems included. The global monkey-patch is therefore the last resort, not the first. The ladder: first no patch (a function taking the object), then the local refinement if syntax demands, and the global patch at the very bottom, owned. The reflex is one question: must this change be visible everywhere, or only in my file?

Le besoinThe needL'outilThe toolLa portéeThe scope
Un comportement utilitaireA utility behaviorUne fonction qui prend l'objet : Slug.from(str).A function taking the object: Slug.from(str).Aucune classe touchée. Le défaut.No class touched. The default.
La syntaxe str.méthodeThe str.method syntaxrefine + using.refine + using.Lexicale : le fichier qui active, point.Lexical: the file that activates, period.
Un changement vraiment universelA truly universal changeMonkey-patch (rouvrir la classe).Monkey-patch (reopen the class).Tout le programme. Dernier recours, assumé.The whole program. Last resort, owned.
Patcher une classe d'autruiPatching someone else's classDans du code partagé : presque jamais.In shared code: almost never.La collision est une question de temps.The collision is a matter of time.
Le pouvoir de tout rouvrir n'oblige pas à tout rouvrir. La bonne portée d'un patch est la plus petite qui résout le problème.The power to reopen everything doesn't oblige you to. The right scope for a patch is the smallest that solves the problem.
Mot de l'éditeurFrom the editor

Fin du Vol 6 : la puissance maximale, la portée minimale.End of Vol 6: maximum power, minimum scope.

Le volume referme son échelle. Intercepter (№ 1) agissait sur un objet, générer (№ 2) sur une classe, rouvrir (№ 3) sur tout le programme — trois crans de portée croissante, et la même discipline à chaque cran : prends l'outil le plus faible qui suffit. Toute la métaprog tient dans ce mouvement, car elle découle d'un seul fait du Vol 1 — les classes et les méthodes sont des objets manipulables à l'exécution — et chacune de ses libertés se paie en lisibilité, en traçabilité, en portée non maîtrisée. La leçon n'est pas « la métaprog est dangereuse », c'est : sa puissance est réelle, mais elle ne se justifie que là où l'explicite est impossible, et toujours à la plus petite portée qui fait le travail.The volume closes its ladder. Intercepting (№ 1) acted on an object, generating (№ 2) on a class, reopening (№ 3) on the whole program — three rungs of increasing reach, and the same discipline at each: take the weakest tool that suffices. All of metaprogramming fits this move, because it flows from one fact of Vol 1 — classes and methods are objects manipulable at runtime — and each of its freedoms is paid in readability, traceability, uncontrolled reach. The lesson isn't 'metaprogramming is dangerous', it's: its power is real, but it's justified only where the explicit is impossible, and always at the smallest scope that does the job.

Prochain volume : les exceptions. On a manipulé le flux normal des messages ; le Vol 7 prend le flux d'erreur — lever, rattraper par type, garantir le nettoyage — et la question qui le traverse : est-ce une condition vraiment exceptionnelle, ou un cas normal déguisé en erreur ? On y verra pourquoi rescue Exception avale le Ctrl-C de l'utilisateur, et quand une valeur de retour disait mieux ce qu'une exception criait.Next volume: exceptions. We handled the normal flow of messages; Vol 7 takes the error flow — raising, rescuing by type, guaranteeing cleanup — and the question running through it: is this a truly exceptional condition, or a normal case disguised as an error? We'll see why rescue Exception swallows the user's Ctrl-C, and when a return value said better what an exception shouted.