Les modules

Dans ce chapitre, nous allons continuer sur la lancée du chapitre précédente en voyant un nouvel outil pour regrouper et organiser du code.

Les modules

Création de module

Un module est une structure regroupant à la fois des constantes et des méthodes. On peut alors regrouper des éléments qui partagent un thème commun dans une même structure. On peut par exemple imaginer un module de messagerie dans lequel on mettrait des méthodes ayant un lien avec notre classe Message ou encore un module mathématique contenant divers outils mathématiques.

Un module se construit de cette façon.

module Football
end

Un module commence par le mot-clé module et se finit par le mot-clé end. Après le mot-clé module, on écrit le nom du module (ici Football). Ce nom doit commencer par une majuscule sinon nous obtiendrons l’erreur « class/module name must be CONSTANT ». De plus, il est conseillé d’écrire le nom des modules en « CamelCase », c’est-à-dire en mettant en majuscule la première lettre de chaque mot.

Dans le module, on pourra ensuite définir des méthodes et des constantes (un peu comme dans une classe). Comme un module ne s’instancie pas (on ne crée pas d’objets à partir du « moule » du module), on ne va pas définir de méthodes d’instances, mais des méthodes de module (un peu comme des méthodes de classe), en utilisant def self.méthode.

Les méthodes d’instance de module ne sont pas inutiles et nous pouvons les croiser dans plusieurs codes Ruby. Elles servent notamment à créer ce que l’on appelle des mixins et que nous verrons au prochain chapitre.

Néanmoins, plutôt que d’écrire self.fonction, nous allons préférer utiliser module_function qui s’utilise comme private par exemple, et permet d’indiquer que les méthodes qui suivent sont des méthodes de module. Utiliser module_function n’est pas tout à fait similaire à utiliser self, mais ces subtilités ne nous intéressent pas pour le moment.

module Multiplication
  
  PLAYERS_NB = 11

  module_function
  
  def rules
    puts 'On a demandé les règles du football.'
  end
end

Ici, nous avons défini la constante MAX et la méthode table. Dans une méthode d’un module, nous pouvons faire appel à d’autres méthodes du module et aux constantes du module. Pour utiliser la constante, il suffit d’écrire son nom et pour faire appel à une méthode, on écrit nom_méthode.

module Multiplication

  PLAYERS_NB = 11

  module_function
  
  def rules
    puts 'On a demandé les règles du football.'
    puts "C'est un sport qui se joue à #{PLAYERS_NB}."
    puts '...'
  end
  
  def presentation
    puts 'Le football est un sport d'équipe.'
    puts 'Voici ses règles.'
    rules
  end
end
Des espaces de noms

Jusqu’à maintenant un module ressemble beaucoup à une classe (sauf qu’il n’y a pas d’attributs). ET l’utilisation ne fait pas exception. Pour utiliser une méthode d’un module, on écrit NomModule.nom_méthode. Par exemple, pour utiliser la méthode table du module Multiplication défini précédemment, nous allons utiliser ce code.

print Football.rules

Nous pouvons aussi accéder aux constantes du module. Pour cela, nous devons utiliser la syntaxe NomModule::NOM_CONSTANTE. Ainsi, pour accéder à la constante définie dans notre code précédent, nous allons utiliser ce code.

print Football::PLAYERS_NB

Notons que cette syntaxe fonctionne également pour utiliser les méthodes du module (et pour les méthodes de classe). Nous pouvons donc écrire NomModule::nom_méthode (en revanche, nous ne pouvons pas écrire NomModule.NOM_CONSTANTE sous peine d’obtenir une erreur). Cependant, pour bien identifier les appels aux méthodes et les utilisations des constantes, nous allons privilégier l’écriture NomModule.nom_méthode.

puts "Au football il y a  #{Football::PLAYERS_NB} joueurs."
Football.rules                          # Bonne écriture.
Football::rules                         # Écriture déconseillée.

En fait, un avantage des modules, en plus de permettre un regroupement thématique est que nous pouvons avoir dans chaque module une méthode ou un objet qui ont le même nom sans que cela ne pose de problème. Par exemple, on pourrait tout à fait imaginer une méthode rules dans un autre module Handball.

module Handball

   PLAYERS_NB = 7

  module_function
  
  def rules
    puts 'Voici les règles du handball.'
    puts '...'
  end
end

puts 'Dans le module Football.'
Football.rules
puts 'Dans le module Handball.'
Handball.rules

Les modules permettent alors de former ce que l’on appelle des espaces de noms (namespace en anglais). Un espace de noms est comme un tiroir dans lequel on met des objets (méthodes, constantes, etc.). En appelant Fooball.rules, on appelle la méthode rules du tiroir Football, et donc il n’y a pas de risques de la confondre avec celle du tiroir Handball.

Déverser le tiroir

Notre programme est une chambre, les modules sont des tiroirs. Ainsi, nous avons un tiroir avec tous les outils du football (et en particulier les règles) et un autre tiroir avec tous les outils du handball (dont les règles). Mais, comme tout enfant gaffeur, nous allons déverser notre tiroir dans la chambre.

Euh, mais ça retire tout l’intérêt des espaces de noms, non ?

Oui, mais dans certains cas, on sait qu’il n’y a pas de conflit possible et on veut écrire rules plutôt que Football.rules. Et dans ce cas, on voudra peut-être juste écrire rules plutôt que Football.rules.

Pour ce faire, nous allons utiliser la méthode include qui permet nous d’inclure un module dans l’« espace courant ». On peut alors écrire ce code.

include Football

puts 'Dans le module Football.'
Football.rules
puts 'Comme on a inclus ce module.'
rules
puts 'Dans le module Menuiserie.'
Handball.rules

Et voilà, l’affaire est faite.

Le mot-clé self

Cela fait plusieurs fois que nous utilisons le mot-clé self. Il est temps de voir ce qu’il signifie.

À quoi se rapporte le mot-clé self

L’objet self se rapporte à l’objet courant et le représente. Par exemple, écrire self.m dans un module signifie que l’on définit la méthode m du module et c’est la même chose pour les classes. En fait, écrire self.m équivaut à écrire ModuleName.m.

Utiliser le mot-clé self plutôt que le nom du module est une bonne habitude. Elle permet par exemple de changer le nom du module sans se faire de souci. Nous allons donc adopter cette habitude.

Notons que puisque self fait référence à l’objet courant, écrire self.m revient à appeler la méthode m de l’objet courant. Dans une méthode de classe ou de module, cela revient à appeler la méthode m de la classe ou du module, dans une méthode d’instance, cela revient à appeler la méthode m de l’objet courant donc de l’instance de la classe.

Cela est très utile et permet d’utiliser l’objet et de le modifier. Par exemple, écrivons pour la classe Array une méthode encrypt qui ajoute à tous les éléments du tableau une certaine valeur (le code) fournit en paramètre. Pour cela, nous allons ouvrir la classe Array et lui ajouter la méthode.

class Array
  def encrypt(code)
    each_with_index { |x, i| self[i] += code }
  end
end

tab = [2, 7, 8, 4]
tab.encrypt(9)
print tab

On obtient bien le résultat attendu (et on illustre au passage le fonctionnement de l’ouverture de classe).

Lorsque l’on écrit une méthode « dangereuse » ou qui modifie l’objet, il est d’usage de terminer le nom de la méthode par un point d’exclamation. On parle alors de méthode bang. Généralement on définit une méthode bang (celle qui change l’objet) et une méthode normale, la méthode normale étant définie en utilisant la méthode bang quand c’est possible (en utilisant dup). Pour notre exemple, on obtient ce résultat.

class Array
  def encrypt!(code)
    each_with_index { |x, i| self[i] += code }
  end
  
  def encrypt(code)
    dup.encrypt!(code)
  end
end

tab = [2, 7, 8, 4]
puts "chiffrer retourne un tableau : #{tab.chiffrer(9)}."
puts "Mais tab n’a pas été modifié : #{tab}."

Ici, on utilise le fait que each_with_index (et each), renvoie le tableau. Ainsi, crypt! renvoie le tableau chiffré. En écrivant dup.crypt!(code), on copie le tableau et on chiffre cette copie avec crypt!, le retour de crypt! sur cette copie (donc le tableau chiffré) étant renvoyé. Le tableau original n’est pas modifié.

Du self implicite dans les méthodes

Avec tout ce que nous venons de voir, nous devons remarquer qu’utiliser m revient à utiliser self.m. En effet, utiliser m dans une méthode d’instance revient à appeler la méthode m de l’instance de même que dans une classe ou un module ça revient à appeler la méthode de la classe, ou du module. En gros, cela revient à appeler la méthode de l’objet courant, donc à écrire self.m. Le self est donc implicite.

Il est donc équivalent d’écrire m et d’écrire self.m. Nous allons éviter d’utiliser self. Cela mène à un problème que nous n’avons jamais abordé jusqu’à maintenant.

Que se passe-t-il quand une fonction a le même nom qu’une variable ?

La question est légitime ici. Si nous avons une méthode f et une variable f, que se passe-t-il quand on écrit f ? Le mieux reste d’essayer, et là, on remarque que dans ce cas, c’est la variable qui est utilisée. En fait, pour utiliser la méthode f, nous sommes obligés d’utiliser des parenthèses.

def f
  0
end

f = 1
puts f    # => 1.
puts f()  # => 0.

Pour éviter cela, nous allons éviter de nommer de la même manière des variables et des méthodes. Bien sûr, dans certains cas, ça reste plus pratique de les nommer de la même manières (et donner à nos variables et à nos méthodes une bonne portée aide à limiter ces problèmes de même que la bonne utilisation des modules et des classes). Par exemple, dans le cas de la méthode initialize, avoir les paramètres qui ont le même nom que les attributs (et donc que l’accesseur potentiel) ne pose pas beaucoup de problèmes et permet de garder un code lisible.

class Message
  attr_reader :author, :date, :recipient, :content

  def initialize(content, author, recipient, date = '21/12/2112')
    @content = content
    @date = date
    @recipient = recipient
    @author = author
  end
end

Ici, les noms des paramètres ne posent pas problème, mais dans le cas général, il faut éviter de masquer la méthode avec une variable locale.

Et pour les attributs, dans une méthode d’instance faut-il utiliser la variable d’instance ou les accesseurs et mutateurs potentiels ?

Là, c’est un choix personnel. Le mieux est de s’en tenir à son choix. Par contre, si une variable n’a pas à être modifié, il ne faudra pas créer d’accesseurs ou de mutateurs (même en les mettant en privé), sous prétexte qu’on a décidé de ne pas utiliser les variables d’instances dans la classe. Par exemple, l’énergie de notre chat n’avait pas à être connue en dehors de l’objet, et nous n’avions pas créé d’accesseurs.

Retour sur include et module_function

Maintenant que nous savons à quoi correspond self, nous allons expliquer un peu mieux ce que font include et module_function. Et nous allons être un peu surpris.

En fait, include copie les méthodes d’instances du module pour les ajouter aux méthodes d’instances de l’objet courant. C’est pour cela que lorsque nous utilisons include, nous pouvons ensuite avoir accès aux méthodes d’un module. Et là, nous pouvons déjà voir un problème.

Mais, nous avons dit qu’avec module_function, on indiquait qu’on définissait des méthodes de module. Si utiliser module_funtion est similaire à utiliser self, on ne définit jamais de méthodes d’instance dans le module et alors include ne copie aucune méthode, non ?

Il faut bien croire que si. N’oublions pas, nous avons dit qu’utiliser module_function n’était pas tout à fait similaire à utiliser self. En fait, avec module_function on déclare deux méthodes, une méthode d’instance et une méthode de module. Avec module_function on serait alors plus proche de ça.

module Handball
   PLAYERS_NB = 7

  def self.rules
    puts 'Voici les règles du handball.'
    puts '...'
  end

  def rules
    puts 'Voici les règles du handball.'
    puts '...'
  end
end

Et ceci nous explique comment fonctionne include. Pour vérifier ce comportement, nous pouvons tester d’inclure un module, mais où la méthode a juste été définie en tant que méthode de module.

module Handball
   PLAYERS_NB = 7

  def self.rules
    puts 'Voici les règles du handball.'
    puts '...'
  end
end

include Handball

puts rules

Et on obtient une erreur « undefined local variable or method `rule' for main:Object (NameError) ». La méthode d’instance rules du module n’existait pas, et include n’inclut pas les méthodes de module.

Un peu de subtilité

Nous avons dit que la méthode include copiait les méthodes d’instances de son argument pour les rajouter aux méthodes d’instance de l’objet courant. En fait c’est faux, Ruby ne les copie pas, mais crée simplement une référence vers le module inclus. Cela signifie que tous les objets qui incluent un module M ont une référence vers ce module M et qu’en particulier toute modification du module sera répercutée sur ces objets.

module M
  module_function

  def f
    'Version 1 du module.'
  end
end

include M

puts f # => Version 1 du module.

module M
  module_function
  
  def f
    'Version 2 du module.'
  end
end

puts f # => Version 2 du module

Puisqu’il ne s’agit pas de copies mais bien de référence au module, nous n’avons même pas eu à l’inclure, la modification était déjà effective.

Nous allons éviter de modifier des modules de la sorte. Nous les déclarerons une fois, avec leurs méthodes et leurs constantes, et n’y toucherons plus.

Modules et classes

Classes ou modules ?

Les modules et les classes sont très liées et il est facile de confondre les deux. Pourtant les deux sont complètement différents dans le sens ou un module regroupe des objets alors qu’une classe représente vraiment quelque chose. D’ailleurs, on remarquera qu’un module ne peut pas être instancié.

module M
end

m = M.new
# => undefined method `new' for M:Module

En fait, si nous avons juste besoin d’une structure pour regrouper des méthodes et des constantes, nous avons sûrement besoin d’un module et non d’une classe. De même, si nous commençons à vouloir faire une classe sans attributs, c’est sûrement un module qu’il nous faut. Généralement, il est plutôt simple de savoir qu’est-ce qui correspond à nos besoins.

De plus, les classes et les modules ne sont pas incompatibles comme nous allons le voir dans la suite du tutoriel.

De l’imbrication de classes et de modules

Une des premières manière de combiner les classes et les modules est de définir une classe dans un module. La syntaxe pour cela est plutôt naturelle.

module Football
  class Player
  end
end

Pour avoir accès à la classe, nous écrirons ensuite Football::Player (comme pour avoir accès une constante de modules). Ceci peut servir à avoir des espaces de noms pour les classes. On peut par exemple imaginer deux classes Player, la première dans un module de football, la seconde dans un module de handball. Reste à imaginer une application qui aurait besoin de ces deux classes, mais l’idée est là (en fait, même sans risque éventuel de conflits, on peut tout simplement vouloir que la classe soit liée à un espace de nom).

module Football
  class Player
  end
end

module Handball
  class Player
  end
end

player = Handball::Player.new

(ou exemple classe Client pour module Network et classe Client pour module Trade).

C’est l’utilisation la plus courante de classes imbriquées dans un module. Notons qu’en anglais on parle de nested class (classe emboîtée, imbriquée).

De même, nous pouvons imbriquer des modules dans des modules, ce qui permet de former en quelque sorte des sous modules.

module Sport
  module Football
  end
end 
Accéder aux méthodes et aux constantes du module principal

Les méthodes (et les constantes) du module principal ne sont pas des méthodes (ni des constantes) du module imbriqué ou de la classe imbriquée. Si on veut les appeler, on veillera à utiliser la syntaxe appropriée.

module Football
  PLAYERS_NB = 11
    
  class Team
    def introduce
      puts "Et voici les #{Football::PLAYERS_NB} joueurs de l'effectif"
      puts '...'
    end
  end
  
  module_function

 def rules
   puts 'On a demandé les règles du football.'
   puts "C'est un sport qui se joue à #{PLAYERS_NB}."
   puts '...'
 end

Football.rules
Football::Team.new.introduce

De même, on peut imbriquer des classes dans des classes. Par exemple, on peut imaginer une classe pour représenter un moteur dans une classe pour représenter un véhicule.

class Vehicle
  class Engine
  end

  def initialize
    @engine = Engine.new
  end
end

engine = Vehicle::Engine.new

Dans certains langages, cela permet de n’autoriser l’instanciation de Engine que dans la classe Vehicle. En Ruby ce n’est pas le cas et dans le code précédent nous l’utilisons pour créer une variable engine en dehors de la classe Vehicle. Mais c’est une indication pour le développeur (peut-être qu’il ne veut définir des moteurs que dans la classe Véhicle), et là encore cela peut servir d’espaces de noms.

Bonnes pratiques

Nous avons dit que le but d’un module était de ne pas avoir à réécrire plusieurs fois le même code et de pouvoir utiliser le même module dans plusieurs programmes. Cependant, pour le moment, nous avons toujours écrit le module dans le même fichier que notre programme, ce qui signifie qu’il faudra le recopier chaque fois que nous voudrons l’utiliser. Or, c’est justement ce que nous voulons éviter.

En fait, ce qu’il nous faudrait, c’est pouvoir écrire le module dans un autre fichier. Ainsi, on pourrait utiliser ce même fichier dans plusieurs projets.

Ceci est possible grâce à la méthode require_relative qui nous permet d’indiquer qu’un fichier Ruby requiert un autre fichier Ruby. En l’utilisant, nous pourrions écrire notre module Sport dans un fichier sport.rb.

module Sport
  module Handball
    # The content of the Handball module.
  end

  module Football
    # The content of the Handball module.
  end
end

Ensuite, dans notre fichier principal, disons main.rb, nous allons indiquer qu’il a besoin de multiplication.rb.

require_relative 'sport.rb'

puts "Les règles du hand : #{Sport::Handball.rules}"
Sport::Football::Team.new.introduce

Notons qu’avec ce code, le fichier sport.rb doit être placé dans le même dossier. En effet, le paramètre de require_relative est le chemin relatif du fichier requis. Si nous voulions placer notre fichier sport.rb dans un dossier module, il faudrait alors écrire require_relative 'sport/multiplication.rb'. Notons de plus que l’extension du fichier n’est pas obligatoire et qu’il est parfaitement possible d’écrire require_relative 'sport'.

Il existe également des méthodes require et load qui sont un peu différentes et dont nous n’allons pas parler en détail ici.

Conventions de nommage pour les dossiers et les fichiers

De même que pour les noms de variables et de modules, il y a des conventions de nommage pour les noms de fichiers et de dossiers. Elles ne sont pas compliquées et les suivre ne devrait pas nous poser de problèmes. Les voici :

  • il est conseillé d’écrire les noms des fichiers en « snake_case » ;
  • il est conseillé d’écrire les noms des dossiers en « snake_case ».

Il est également conseillé d’avoir un seul module par fichier et de nommer ce fichier par le nom du module (en « snake_case » pour rester en cohérence avec la règle sur les noms des fichiers). Ainsi, nous avons le module Multiplication dans le fichier multiplication.rb. De même, il est conseillé d’avoir une classe par fichier et de le nommer par le nom de la classe en « snake_case ».

De plus, notons que généralement nous faisons un fichier par classe et un fichier par module. Ceci permet alors de bien répartir le code. Les règles de nommage pour les fichiers contenant les classes sont les mêmes que celles pour les modules (par exemple la classe MyClass dans le fichier my_class.rb).

Principe de responsabilité unique

Le principe de responsabilité unique (SRP pour Single Responsibility Principle) est le premier principe de SOLID. En gros il indique qu’une classe ou une méthode ne doit avoir qu’une seule responsabilité (elle ne fait qu’une chose et elle le fait bien). On l’exprime souvent en disant qu’« une classe ne doit avoir qu’une seule raison de changer » (si elle avait plusieurs responsabilités, elle aurait plusieurs raisons de changer).

L’image de l’article humoristique était celle du couple qui dans sa maison avait un interrupteur était chargé d’allumer ou éteindre la lumière de l’entrée, mais aussi de contrôler le broyeur de la cuisine… Assez gênant si nous pensons à la fin de l’histoire.

Considérons notre classe Message, elle devrait par exemple juste se charger du message et de son auteur, si un jour nous voulons envoyer des messages, ça ne devra pas être sa responsabilité, mais celle d’un module ou d’une autre classe. De même, si nous voulons formater le message de plusieurs manières différentes (en HTML, en Markdown, en LaTeX, etc.), ça ne devrait pas être la responsabilité de la classe Message d’effectuer ce formatage, mais plutôt de différents modules ou classes de formatage.

Ce principe permet d’avoir du code plus robuste car plus simple à modifier et à déboguer. En nous exerçant, en faisant des projets et en discutant avec d’autres programmeurs, nous comprendront progressivement comment écrire du code respectant ses principes.

Exercices

Avec tout ce que nous avons vu, nous avons déjà de quoi faire un bon paquet d’exercices et de projets. En fait, les classes et les modules ne nous permettent pas de faire beaucoup plus de projets que ce que nous pouvions déjà faire… Mais ces notions nous permettent de le faire de manière plus organisée et nous permettent donc d’avoir du code plus simple à écrire et surtout à lire (un programmeur passe beaucoup plus de temps à lire du code qu’à en écrire, autant que le code soit simple à lire).

Pour mettre à l’épreuve ces notions, nous pouvons faire l'exercice de la pharmacie, tiré de la banque d’exercices proposée au début de cette partie du tutoriel.

Maintenant, nous allons de plus en plus faire des petits projets en guise d’exercice, et l’avis d’autres programmeurs sera donc un très grand plus, et donc poster son code sur le forum pour obtenir des avis est vraiment une bonne chose.


Entre modules et classes, nous avons fort à faire. Et nous n’avons pas fini d’explorer tout ce que Ruby a à offrir ! Mais avant de continuer, voyons un peu ce que nous avons appris.

  • Les modules nous permettent de regrouper des méthodes, des constantes, et en gros du code d’une même thématique.
  • Les méthodes « dangereuses » (celles qui modifient l’objet) se terminent par un !. La méthode non dangereuse est généralement définie à l’aide de la méthode bang associée et de la dup (on appelle la méthode dangereuse sur une copie de l’objet et on renvoie cette copie).
  • Le mot-clé self représente l’objet courant. D’ailleurs qu’affiche puts self dans un programme (ou même dans IRB) et quelle est la classe de cet objet ?
  • Nous créons un fichier par classe et un fichier par module.