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

extend & composition, ou être plutôt que savoir faire extend & composition, or being versus knowing how

extend mixe au niveau d'un objet : dans une classe, des méthodes de classe ; sur une instance, un décorateur. Et quand on ne veut ni être ni mixer, la composition délègue (has-a, Forwardable). Le piège : sous-classer un conteneur pour trois méthodes et en hériter cent. extend mixes at the object level: in a class, class methods; on an instance, a decorator. And when you want neither to be nor to mix, composition delegates (has-a, Forwardable). The trap: subclassing a container for three methods and inheriting a hundred.

AudienceAudience
Dev qui hésite entre hériter, mixer et composer Dev torn between inheriting, mixing and composing
Format
Self-paced
ChapitresChapters
5
Date
Avr 2026 Apr 2026
≈ 18 min ●●●○ extendCompositionForwardable
SommaireContents ·
№ 01CadrageFraming3 min

include vise les instances ; extend vise un objet.include targets instances; extend targets one object.

Le № 2 a inséré des modules dans la chaîne d'une classe, pour ses instances. extend déplace la greffe d'un cran : il mixe un module sur un seul objet. Et comme une classe est elle-même un objet (Vol 1), extend dans un corps de classe produit des méthodes de classe — l'idiome le plus courant. Sur un objet ordinaire, c'est un décorateur ponctuel : on équipe cette instance-là, et elle seule. Même module, même mécanique de chaîne, mais appliquée à l'objet plutôt qu'au moule qui le fabrique.№ 2 inserted modules into a class's chain, for its instances. extend moves the graft one notch: it mixes a module onto a single object. And since a class is itself an object (Vol 1), extend in a class body produces class methods — the most common idiom. On an ordinary object, it's a one-off decorator: you equip that instance, and it alone. Same module, same chain mechanics, but applied to the object rather than the mold that makes it.

extend greffe sur l'objet, pas sur les instancesextend grafts onto the object, not the instances
module Helpers
  def help = "j'aide #{self}"
end

# extend mixe sur UN objet précis — pas sur toutes les instances
obj = Object.new
obj.extend(Helpers)
obj.help              # => "j'aide #<Object>"   greffé sur CET objet seul

# et comme une classe EST un objet (Vol 1)…
class Widget
  extend Helpers      # …extend dans une classe = méthode de CLASSE
end
Widget.help           # => "j'aide Widget"
Widget.new.respond_to?(:help)   # => false   les instances ne l'ont pas
Deux verbes, deux ciblesTwo verbs, two targets
include Minclude M
méthodes d'INSTANCEINSTANCE methods
toutes les instancesevery instance
extend Mextend M
méthodes de L'OBJETthe OBJECT's methods
classe → méthodes de classeclass → class methods

Sous le capot, obj.extend(M) revient à inclure M dans la classe singleton de l'objet — sa chaîne d'ancêtres personnelle (Vol 1). C'est la même mécanique que le № 2, appliquée à un objet unique.Under the hood, obj.extend(M) amounts to including M in the object's singleton class — its personal ancestor chain (Vol 1). It's the same mechanics as № 2, applied to a single object.

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

« Cette méthode doit répondre sur les instances (include) ou sur cet objet précis (extend) ? » Le plus souvent, extend dans une classe sert à poser des méthodes de classe — un constructeur nommé, une fabrique, une config. Sur une instance, il décore au cas par cas."Should this method answer on instances (include) or on this precise object (extend)?" Most often, extend in a class lays down class methods — a named constructor, a factory, a config. On an instance, it decorates case by case.

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

Ce numéro ferme le Vol 5 en élargissant la question du partage : après l'espace de noms (№ 1) et la chaîne (№ 2), on mixe au niveau d'un objet — puis on prend du recul. Car tout ce volume tourne autour d'un arbitrage qu'on va enfin nommer : avant d'hériter ou de mixer, ai-je besoin d'être cette chose, ou seulement de savoir faire ce qu'elle fait ? La composition, souvent, répond mieux.This issue closes Vol 5 by widening the sharing question: after the namespace (№ 1) and the chain (№ 2), we mix at the object level — then step back. Because this whole volume circles a call we'll finally name: before inheriting or mixing, do I need to be this thing, or only to know how to do what it does? Composition often answers better.

№ 02La surpriseThe surprise4 min

Un module qui se mixe lui-même, ou qui équipe les deux côtés.A module that mixes itself, or equips both sides.

Deux idiomes naissent de extend. Le premier, extend self, fait qu'un module s'inclut dans lui-même : ses méthodes deviennent appelables directement (Config.root) tout en restant mixables ailleurs — pratique pour une config ou un sac d'utilitaires à double usage. Le second résout un besoin fréquent : un seul include qui équipe à la fois les instances ET la classe. Le hook included intercepte le mixin et fait un extend ClassMethods au passage. C'est le motif des « concerns » : include d'un côté, methodes d'instance et de classe greffées ensemble.Two idioms grow from extend. The first, extend self, makes a module include itself: its methods become directly callable (Config.root) while staying mixable elsewhere — handy for a config or a dual-use utility bag. The second solves a common need: a single include that equips both the instances AND the class. The included hook intercepts the mixin and does an extend ClassMethods along the way. It's the 'concerns' pattern: include on one side, instance and class methods grafted together.

extend self : appelable ET mixableextend self: callable AND mixable
module Config
  extend self            # le module se mixe LUI-MÊME

  def root = "/app"
  def env  = ENV.fetch("APP_ENV", "dev")
end

Config.root              # => "/app"   appelable directement sur le module
Config.env               # => "dev"

# et comme c'est un module, on peut aussi l'inclure ailleurs :
class Worker
  include Config         # root et env deviennent méthodes d'instance
end
Worker.new.root          # => "/app"

extend self dit : « inclus-moi dans mon propre objet ». Le module devient alors un porte-fonctions appelable (Config.root) — même service que module_function du № 1, mais qui garde les méthodes publiques une fois mixées par include (module_function, lui, les y fait entrer en privé). Un seul module, deux portes d'entrée.extend self says: "include me in my own object". The module then becomes a callable function-holder (Config.root) — same service as № 1's module_function, but keeping the methods public once mixed in via include (module_function brings them in as private). One module, two doors.

Le hook included n'est pas de la magie : c'est extend appelé pour toi, au moment du mixin.The included hook isn't magic: it's extend called for you, at mixin time.
№ 03La mécaniqueThe mechanics5 min

Déléguer ce qu'on sait faire, sans être ce qu'on délègue.Delegate what you know how to do, without being what you delegate to.

La composition est l'autre réponse au partage : au lieu d'hériter d'une classe (is-a) ou de mixer un module, on détient un objet (has-a) et on lui délègue ce qu'il sait faire. Forwardable industrialise ce geste : def_delegators expose, une à une, les méthodes qu'on choisit de faire suivre à l'objet contenu — et seulement celles-là. La surface reste sous contrôle : Playlist a un tableau, en publie quatre méthodes, et garde le droit d'en ajouter ou d'en cacher. include Enumerable, lui, rebranche tout le réflexe fonctionnel du Vol 8 sur un simple each.Composition is the other answer to sharing: instead of inheriting a class (is-a) or mixing a module, you hold an object (has-a) and delegate to it what it knows how to do. Forwardable industrializes this: def_delegators exposes, one by one, the methods you choose to forward to the contained object — and only those. The surface stays under control: Playlist has an array, publishes four of its methods, and keeps the right to add or hide. include Enumerable, meanwhile, wires the whole functional reflex of Vol 8 onto a single each.

Forwardable : déléguer une surface choisieForwardable: delegate a chosen surface
require "forwardable"

class Playlist
  extend Forwardable                       # extend : méthodes de classe
  def_delegators :@songs, :each, :size, :<<, :empty?   # délègue à @songs

  include Enumerable                        # via each → map/select/sort… gratis

  def initialize = @songs = []
  def total_minutes = @songs.sum(&:minutes) # le comportement PROPRE à Playlist
end

list = Playlist.new
list << song            # délégué à @songs.<<
list.size               # délégué à @songs.size
list.map(&:title)       # via Enumerable, sur la base d'each
is-a hérite tout, has-a expose ce qu'il choisitis-a inherits all, has-a exposes what it picks
# is-a (héritage)            vs   has-a (composition)
class Playlist < Array          class Playlist
  # EST un Array :                 def initialize = @songs = []
  # hérite ~100 méthodes          # A un Array, en expose 4 :
end                               #   each, size, <<, empty?
                                end
Identité vs capacité, en pratique.Identity vs capability, in practice.

L'héritage dit « est un » : Playlist < Array hérite de tout Array, y compris ce qui n'a aucun sens pour une playlist (pack, flatten!…). La composition dit « a un » : Playlist détient un tableau et n'en expose que ce qu'elle assume. Forwardable rend la délégation courte ; Enumerable redonne map/select via each. Tu choisis la surface au lieu de l'hériter en bloc.Inheritance says "is a": Playlist < Array inherits all of Array, including what makes no sense for a playlist (pack, flatten!…). Composition says "has a": Playlist holds an array and exposes only what it owns. Forwardable makes delegation short; Enumerable restores map/select via each. You choose the surface instead of inheriting it wholesale.

La grille du volume se complète : :: pour nommer, include pour donner aux instances, extend pour donner à un objet, et la composition quand on ne veut ni être ni mixer, mais déléguer. Quatre outils, une seule question à chaque fois — quelle relation, exactement, lie mon objet à ce comportement ?The volume's grid completes: :: to name, include to give to instances, extend to give to an object, and composition when you want neither to be nor to mix, but to delegate. Four tools, one question each time — what relationship, exactly, ties my object to this behavior?

№ 04Le piègeThe trap4 min

Sous-classer Array pour trois méthodes : cent en cadeau.Subclassing Array for three methods: a hundred for free.

Le contre-exemple obligatoire du volume. On veut une Playlist avec trois méthodes maison, alors on hérite d'Array « pour avoir push, size, each gratis ». Mais l'héritage est tout ou rien : on récupère aussi pack, rotate, flatten!, l'opérateur &, cent méthodes dont aucune n'a de sens pour une playlist et qu'on ne contrôle plus. Pire, l'abstraction fuit — first(3), select, map renvoient des Array nus, pas des Playlist : le type s'évapore au premier appel. La composition isolait proprement : Playlist a un tableau, en expose la surface qu'elle choisit, et garde son type de bout en bout.The volume's mandatory counter-example. You want a Playlist with three custom methods, so you inherit from Array 'to get push, size, each for free'. But inheritance is all-or-nothing: you also get pack, rotate, flatten!, the & operator, a hundred methods none of which make sense for a playlist and which you no longer control. Worse, the abstraction leaks — first(3), select, map return bare Arrays, not Playlists: the type evaporates on the first call. Composition isolated cleanly: Playlist has an array, exposes the surface it picks, and keeps its type throughout.

L'héritage est tout ou rien — et le type fuitInheritance is all-or-nothing — and the type leaks
# Sous-classer Array pour ajouter 3 méthodes : on en hérite ~100
class Playlist < Array
  def total_minutes = sum(&:minutes)
  def titles        = map(&:title)
  def shuffle_fair  = sort_by { rand }
end

list = Playlist.new
list.push(song)          # ok… mais aussi pack, rotate, flatten!, &, |, etc.
top3 = list.first(3)     # => un Array NU, plus une Playlist
list.select(&:fav?)      # => un Array NU : l'abstraction fuit dès qu'on filtre
list.class               # Playlist, mais ses dérivés retombent en Array
# on a hérité 100 méthodes qu'on ne contrôle pas, et perdu son type au 1er map
has-a : surface choisie, type conservéhas-a: chosen surface, type kept
# Composition : Playlist A un tableau, n'EST PAS un tableau
require "forwardable"

class Playlist
  extend Forwardable
  def_delegators :@songs, :size, :<<, :each, :empty?
  include Enumerable                      # map/select via each…

  def initialize(songs = []) = @songs = songs
  def total_minutes = @songs.sum(&:minutes)
  def select(&blk) = Playlist.new(@songs.select(&blk))  # garde le TYPE
end

list = Playlist.new
list << song
list.select(&:fav?)      # => une Playlist, surface maîtrisée de bout en bout
Le contre-exemple obligatoire.The mandatory counter-example.

La question qui tranche : Playlist est-elle un Array (substituable partout où un Array est attendu, avec ses cent méthodes), ou a-t-elle un Array dont elle n'expose qu'une poignée d'opérations ? Presque toujours, c'est la seconde. Sous-classer un conteneur de la lib standard hérite d'une interface qu'on ne maîtrise pas et qui fuit son type. Forwardable + Enumerable donnent les mêmes commodités, mais sur une surface qu'on a dessinée.The deciding question: is Playlist an Array (substitutable anywhere an Array is expected, with its hundred methods), or does it have an Array from which it exposes a handful of operations? Almost always, the latter. Subclassing a standard-library container inherits an interface you don't control and that leaks its type. Forwardable + Enumerable give the same conveniences, but on a surface you drew.

Hériter d'Array pour trois méthodes, c'est adopter cent méthodes étrangères — et perdre son type au premier map.Inheriting Array for three methods is adopting a hundred foreign methods — and losing your type on the first map.
№ 05Bilan & éditoWrap-up & editorial2 min

Être, ou savoir faire ? La relation décide de l'outil.To be, or to know how? The relationship decides the tool.

extend mixe au niveau d'un objet : dans une classe, ce sont les méthodes de classe ; sur une instance, un décorateur. extend self rend un module à la fois appelable et mixable ; le hook included greffe instances et classe d'un seul include. Et quand on ne veut ni être ni mixer, la composition délègue : has-a plutôt que is-a, Forwardable pour une surface choisie. Le piège, c'est de sous-classer un conteneur pour trois méthodes et d'en hériter cent.extend mixes at the object level: in a class, those are class methods; on an instance, a decorator. extend self makes a module both callable and mixable; the included hook grafts instances and class from a single include. And when you want neither to be nor to mix, composition delegates: has-a rather than is-a, Forwardable for a chosen surface. The trap is subclassing a container for three methods and inheriting a hundred.

La relationThe relationshipL'outilThe toolLe réflexeThe reflex
« est un » (identité)'is a' (identity)Héritage < — substituable partout.Inheritance < — substitutable everywhere.Rare et assumé : seulement une vraie identité.Rare and deliberate: only a true identity.
« sait faire » (instances)'knows how' (instances)include Module — une capacité.include Module — a capability.Le mixin par défaut : Comparable, Enumerable.The default mixin: Comparable, Enumerable.
« sur cet objet »'on this object'extend Module — méthodes de classe, décorateur.extend Module — class methods, decorator.extend self pour un module à double usage.extend self for a dual-use module.
« a un » (délégation)'has a' (delegation)Composition + Forwardable — surface choisie.Composition + Forwardable — chosen surface.Le défaut quand l'identité n'est pas réelle.The default when the identity isn't real.
Hériter, mixer, déléguer ne sont pas des goûts : ce sont trois relations différentes entre un objet et un comportement.Inherit, mix, delegate aren't tastes: they're three different relationships between an object and a behavior.
Mot de l'éditeurFrom the editor

Fin du Vol 5 : le partage est un choix de relation.End of Vol 5: sharing is a choice of relationship.

Trois numéros pour transformer une question vague — « comment partager du code ? » — en un arbitrage net. Le № 1 a séparé les deux métiers du module : ranger et partager. Le № 2 a montré la chaîne d'ancêtres, ce parcours déterministe où include, prepend et l'héritage insèrent des maillons. Le № 3 a complété la palette avec extend et la composition. Le fil rouge n'est pas « les modules, c'est mieux que l'héritage » ; c'est : chaque façon de partager encode une relation différente — est-un, sait-faire, a-un — et la bonne se reconnaît à la phrase qu'on peut dire de l'objet sans mentir. Quand l'héritage tente, demande d'abord si c'est vraiment une identité ; ça l'est plus rarement qu'on ne croit.Three issues to turn a vague question — 'how do I share code?' — into a clean call. № 1 separated the module's two jobs: organizing and sharing. № 2 showed the ancestor chain, that deterministic path where include, prepend and inheritance insert links. № 3 completed the palette with extend and composition. The thread isn't 'modules beat inheritance'; it's: each way of sharing encodes a different relationship — is-a, knows-how, has-a — and the right one is recognized by the sentence you can say about the object without lying. When inheritance tempts you, first ask whether it's truly an identity; it is more rarely than you'd think.

Prochain volume : la métaprogrammation. On a vu que les classes et les modules sont des objets qu'on manipule, et que la chaîne d'ancêtres décide quelle méthode répond. Le Vol 6 pousse cette logique au bout : du code qui écrit du code — method_missing au bout de la chaîne, define_method qui fabrique des méthodes à la volée, les classes qu'on rouvre. La puissance est réelle, le prix aussi — un code que ni un lecteur ni un grep ne retrouvent. On entrera donc par la prudence, pas par l'éblouissement.Next volume: metaprogramming. We saw that classes and modules are objects you manipulate, and that the ancestor chain decides which method answers. Vol 6 pushes this logic to its end: code that writes code — method_missing at the end of the chain, define_method forging methods on the fly, classes you reopen. The power is real, the price too — code neither a reader nor a grep can find. So we'll enter through caution, not dazzle.