Vol. 3 · № 01 · Un atelier RubyA Ruby workshop

Le duck typing, ou réponds-tu au message ? Duck typing, or do you answer the message?

Une méthode qui appelle x.quack fonctionne sur n'importe quel objet répondant à quack — sans interface à implémenter, sans superclasse commune. Le seul contrat, c'est le message. On ne demande pas quel type tu es : on demande si tu réponds. A method calling x.quack works on any object that responds to quack — no interface to implement, no shared superclass. The only contract is the message. We don't ask what type you are: we ask whether you respond.

AudienceAudience
Dev venant d'un langage à types, prêt à coder contre un protocole Dev from a typed language, ready to code against a protocol
Format
Self-paced
ChapitresChapters
5
Date
Fév 2026 Feb 2026
≈ 18 min ●●○○ Duck typingProtocolerespond_to?
SommaireContents ·
№ 01CadrageFraming3 min

Ne demande pas qui tu es. Demande si tu réponds.Don't ask what you are. Ask whether you respond.

Deux volumes durant, on a envoyé des messages sans jamais demander le type de personne. C'était le sujet sans le nommer. Le voici : le duck typing. « Si ça marche comme un canard et que ça cancane comme un canard, c'est un canard. » Une méthode qui appelle x.quack fonctionne sur n'importe quel objet répondant à quack — sans interface à implémenter, sans superclasse commune. Le seul contrat, c'est le message lui-même.For two volumes we've sent messages without ever asking anyone's type. That was the subject, unnamed. Here it is: duck typing. 'If it walks like a duck and quacks like a duck, it's a duck.' A method calling x.quack works on any object responding to quack — no interface to implement, no shared superclass. The only contract is the message itself.

Trois classes sans aucun lien, un seul messageThree unrelated classes, one message
def faire_crier(x) = x.quack   # j'envoie juste le message quack

class Canard;   def quack = "Coin !"      end
class Klaxon;   def quack = "POUET !"     end   # aucun lien avec Canard
class FauxCanard; def quack = "(silence)" end   # un objet de test

faire_crier(Canard.new)     # => "Coin !"
faire_crier(Klaxon.new)     # => "POUET !"
faire_crier(FauxCanard.new) # => "(silence)"
# aucune interface, aucune superclasse commune : il suffit de répondre
Ce qui compte, c'est le messageWhat matters is the message
Canard
#quack
Klaxon
#quack
FauxCanard
#quack
→
.quack
le seul contratthe only contract

Aucune des trois classes n'« est » un canard, ne partage d'ancêtre, ne déclare d'interface. Elles sont interchangeables parce qu'elles répondent au même message, point.None of the three "is" a duck, shares an ancestor, or declares an interface. They're interchangeable because they answer the same message, full stop.

Le réflexe du volumeThe volume's reflex

« Je ne demande pas quel type tu es — je demande si tu réponds à ce message. » Code contre un protocole (le ou les messages attendus), jamais contre une classe nommée. Ce que ton objet « est » ne te regarde pas ; ce qu'il sait faire, si."I don't ask what type you are — I ask whether you respond to this message." Code against a protocol (the expected message(s)), never against a named class. What your object "is" is none of your business; what it can do, is.

Pourquoi ça suit les deux premiers volumes.Why this follows the first two volumes.

Le Vol 1 a posé que tout est un message à un objet ; le Vol 2, qu'on délègue du comportement. Le duck typing est leur conséquence directe : si seul compte le message qu'un objet reçoit, alors son type n'a jamais été la question. Ce volume ne fait que nommer ce qu'on pratiquait déjà — et en montrer le prix.Vol 1 set that everything is a message to an object; Vol 2, that you delegate behavior. Duck typing is their direct consequence: if only the message an object receives matters, then its type was never the question. This volume merely names what we were already doing — and shows its price.

№ 02La surpriseThe surprise4 min

Ailleurs, il faut déclarer qu'on est compatible. Ici, il suffit de l'être.Elsewhere, you must declare you're compatible. Here, it's enough to be so.

Dans un langage statique, pour que deux classes soient interchangeables, elles doivent partager un type déclaré : une interface implements, une superclasse commune. Un objet qui aurait pourtant la bonne méthode mais sans la déclaration est refusé. Ruby renverse la priorité : le comportement prime sur le type. Si tu réponds au message, tu passes — qui que tu sois.In a static language, for two classes to be interchangeable, they must share a declared type: an interface implements, a common superclass. An object that has the right method but lacks the declaration is rejected. Ruby flips the priority: behavior trumps type. If you answer the message, you pass — whoever you are.

Java — l'interface commune, obligatoireJava — the common interface, mandatory
// Java : pour être interchangeables, il FAUT un type commun déclaré
interface Quackable { String quack(); }

class Duck   implements Quackable { public String quack() { return "Coin"; } }
class Klaxon implements Quackable { public String quack() { return "Pouet"; } }

void shout(Quackable q) { System.out.println(q.quack()); }
// un objet qui aurait pourtant un quack() mais n'implémente PAS
// Quackable serait REFUSÉ à la compilation. Le type prime sur le comportement.

Pour être acceptés par shout, Duck et Klaxon doivent déclarer qu'ils sont Quackable. Un objet d'une lib tierce, avec un quack() parfait mais sans cette déclaration, est rejeté. Le type prime sur le comportement.To be accepted by shout, Duck and Klaxon must declare they're Quackable. An object from a third-party lib, with a perfect quack() but no such declaration, is rejected. Type trumps behavior.

Un langage statique demande « es-tu déclaré du bon type ? ». Ruby demande « sais-tu faire ce que je vais te demander ? ».A static language asks "are you declared the right type?". Ruby asks "can you do what I'm about to ask?".
№ 03La mécaniqueThe mechanics5 min

respond_to? interroge le message. is_a? interroge le type.respond_to? asks about the message. is_a? asks about the type.

Le duck typing ne veut pas dire « ne jamais rien vérifier ». Il veut dire vérifier la bonne chose. respond_to?(:each) demande « sais-tu faire each ? » — une question sur le comportement. is_a?(Array) demande « es-tu de ce type ? » — une question sur l'identité, plus rigide. La première colle au protocole ; la seconde lie ton code à une classe précise. Et le vrai bonus tombe en test : un objet qui répond au message remplace n'importe quel mock.Duck typing doesn't mean 'never check anything'. It means check the right thing. respond_to?(:each) asks 'can you do each?' — a question about behavior. is_a?(Array) asks 'are you this type?' — a question about identity, more rigid. The first sticks to the protocol; the second ties your code to a precise class. And the real bonus lands in testing: an object that answers the message replaces any mock.

Trois questions, trois rigiditésThree questions, three rigidities
x.respond_to?(:each)« sais-tu faire each ? »'can you do each?'le bon réflexe — teste le messagethe right reflex — test the message
x.is_a?(Enumerable)« hérites-tu d'Enumerable ? »'do you inherit Enumerable?'parfois utile, mais lie au typesometimes useful, but ties to type
x.instance_of?(Array)« es-tu EXACTEMENT Array ? »'are you EXACTLY Array?'presque toujours trop strictalmost always too strict
Coder contre un protocole, pas une classeCoding against a protocol, not a class
# Une méthode qui n'exige qu'un protocole : "répondre à <<"
def log_into(sink, message)
  sink << "[#{Time.now}] #{message}\n"   # le seul message attendu : <<
end

log_into($stdout, "démarré")      # un IO
log_into(File.open("a.log", "a"), "ok")
require "stringio"                # pas chargé par défaut
log_into(StringIO.new, "en mémoire")
log_into([], "dans un tableau")   # Array répond aussi à <<
# quatre types sans parenté, un seul protocole accepté
Tester sans mock : un objet qui répondTesting without mocks: an object that responds
# Le bonus : tester sans framework de mock — un objet qui répond, c'est tout
class FakeSink
  attr_reader :lines
  def initialize = @lines = []
  def <<(s) = @lines << s        # il suffit de répondre à <<
end

sink = FakeSink.new
log_into(sink, "salut")
sink.lines.first                 # => "[…] salut\n"
# pas de double, pas de stub : un vrai petit objet qui répond au message
Le cadeau caché du duck typing.Duck typing's hidden gift.

Comme log_into n'exige aucun type, un petit objet de test qui répond à << suffit à le vérifier — pas besoin de framework de mock, de stub, de double. La testabilité n'est pas un effort en plus : c'est la même substituabilité que celle dont profite la prod.Since log_into demands no type, a tiny test object responding to << is enough to verify it — no mocking framework, stub, or double needed. Testability isn't extra effort: it's the same substitutability production already enjoys.

№ 04Le piègeThe trap4 min

Le is_a? « par sécurité » rejette des objets qui savaient répondre.The "just-in-case" is_a? rejects objects that knew how to respond.

Venant d'un langage à types, le réflexe rassurant est de vérifier la classe en entrée : « il me faut un Array ». Mais ta méthode n'avait pas besoin d'un Array — elle avait besoin d'un each. En exigeant le type, tu refuses un Range, un Set, un Enumerator paresseux : des objets parfaitement capables de faire ce que tu demandes. Tu as recréé à la main le système de types rigide que Ruby venait d'éviter, en pire — car le tien ne connaît que les classes que tu as pensé à lister.Coming from a typed language, the reassuring reflex is to check the class on input: 'I need an Array'. But your method didn't need an Array — it needed an each. By demanding the type, you reject a Range, a Set, a lazy Enumerator: objects perfectly able to do what you ask. You've hand-rebuilt the rigid type system Ruby just avoided, but worse — because yours only knows the classes you thought to list.

Le contrôle de type qui rejette le valideThe type check that rejects the valid
# Le réflexe importé d'un langage statique : "je vérifie le type d'abord"
def somme(collection)
  raise ArgumentError, "il me faut un Array" unless collection.is_a?(Array)
  collection.reduce(0, :+)
end

somme([1, 2, 3])        # => 6
somme(1..3)             # ArgumentError — alors qu'un Range sait reduce !
somme(Set[1, 2, 3])     # ArgumentError — un Set aussi
somme(lazy_enum)        # ArgumentError — et un Enumerator paresseux aussi
# tu as REJETÉ des objets parfaitement valides au nom d'un type
Exiger le protocole, pas la classeDemand the protocol, not the class
# Le correctif : n'exige que le protocole dont tu te sers vraiment
def somme(collection)
  collection.reduce(0, :+)    # reduce n'a besoin que de each
end

somme([1, 2, 3])        # => 6
somme(1..3)             # => 6
somme(Set[1, 2, 3])     # => 6

# et SI tu dois vraiment garder une frontière, teste le COMPORTEMENT :
raise ArgumentError unless collection.respond_to?(:each)
Le contre-exemple obligatoire.The mandatory counter-example.

Truffer le code de is_a?(Array) rejette un objet qui répondait pourtant à each — on a reconstruit un système de types à la main, mais aveugle à tout ce qu'on n'a pas énuméré. La règle : ne teste pas la classe, envoie le message. Et quand une vraie frontière l'exige (entrée externe douteuse), demande le comportement avec respond_to?, pas l'identité avec is_a?. is_a? ne se justifie que lorsque le type est réellement la question — un cas rare, et alors assumé.Peppering code with is_a?(Array) rejects an object that did respond to each — you've rebuilt a type system by hand, but blind to everything you didn't enumerate. The rule: don't test the class, send the message. And when a real boundary demands it (dubious external input), ask for behavior with respond_to?, not identity with is_a?. is_a? is only justified when the type genuinely is the question — a rare case, and then deliberate.

Chaque is_a? que tu poses « pour être sûr » ferme la porte à un objet qui aurait répondu. Tu vérifies le mauvais contrat.Every is_a? you add "to be safe" shuts the door on an object that would have responded. You're checking the wrong contract.
№ 05Bilan & éditoWrap-up & editorial2 min

Code contre un protocole, jamais contre une classe.Code against a protocol, never against a class.

Le duck typing n'est pas l'absence de discipline — c'est une discipline déplacée. On ne s'engage pas sur ce qu'un objet est, mais sur ce qu'il sait faire. Un objet qui répond au message passe, quelle que soit sa classe ; et ta méthode, en retour, ne dépend que du protocole qu'elle utilise vraiment. C'est ce qui rend Ruby substituable et testable presque sans effort.Duck typing isn't the absence of discipline — it's discipline relocated. You don't commit to what an object is, but to what it can do. An object answering the message passes, whatever its class; and your method, in return, depends only on the protocol it actually uses. That's what makes Ruby substitutable and testable almost effortlessly.

La questionThe questionL'outilThe toolLe réflexeThe reflex
« sais-tu faire X ? »'can you do X?'Envoie le message. C'est la voie par défaut du duck typing.Send the message. The default path of duck typing.Ne teste rien : appelle, et fais confiance au protocole.Test nothing: call, and trust the protocol.
À une vraie frontièreAt a real boundaryrespond_to?(:msg) — une question sur le comportement.respond_to?(:msg) — a question about behavior.Réserve-le à l'entrée externe douteuse, pas au domaine interne.Reserve it for dubious external input, not the internal domain.
« es-tu de ce type ? »'are you this type?'is_a? / instance_of? — une question sur l'identité.is_a? / instance_of? — a question about identity.Rare et assumé — seulement quand le type EST vraiment la question.Rare and deliberate — only when the type genuinely IS the question.
En testIn testsUn petit objet qui répond au message remplace tout mock.A small object that answers the message replaces any mock.La substituabilité de la prod est aussi celle des tests.Production's substitutability is also the tests'.
Demander la classe, c'est demander « qui es-tu ? » à un objet qui n'attendait qu'une chose : qu'on lui dise quoi faire.Asking the class asks "who are you?" of an object that wanted only one thing: to be told what to do.
Mot de l'éditeurFrom the editor

La liberté a une contrepartie — on la nommera.The freedom has a flip side — we'll name it.

Le duck typing est sans doute ce qui déroute le plus un dev venant d'un langage à types : on lui retire le filet déclaratif auquel il s'était habitué. Mais ce filet avait un coût qu'on oublie — la cérémonie des interfaces, le couplage à des hiérarchies, le refus d'objets pourtant capables. Ruby fait le pari inverse : faire confiance au message. Le pari est payant en souplesse et en testabilité ; il se paie en une erreur repoussée à l'exécution, le NoMethodError tardif. La série ne cache pas ce prix — elle le situe, et apprend à le contenir là où il compte.Duck typing is probably what most disorients a dev from a typed language: it removes the declarative safety net they'd grown used to. But that net had a cost we forget — the ceremony of interfaces, the coupling to hierarchies, the refusal of objects that were nonetheless capable. Ruby makes the opposite bet: trust the message. The bet pays off in flexibility and testability; it costs an error pushed to runtime, the late NoMethodError. The series doesn't hide that price — it locates it, and learns to contain it where it matters.

Prochain numéro : les protocoles implicites. On a dit « réponds au message » ; mais Ruby attend souvent des messages précis sans jamais les déclarer — to_s, <=>, each, to_proc. Ce sont les « interfaces » tacites du langage : les connaître, c'est rendre tes objets compatibles avec toute la bibliothèque standard d'une seule méthode.Next issue: implicit protocols. We said 'answer the message'; but Ruby often expects precise messages without ever declaring them — to_s, <=>, each, to_proc. These are the language's tacit 'interfaces': knowing them makes your objects compatible with the whole standard library through a single method.