source: https://github.com/hackademy-io

SOMMAIRE

1- Les types primitifs String et Numeric
2- Les types primitifs Array et Hash
3- Les structures de controle
4- Variables et identifieurs
5- Créer une classe
6- Les accesseurs
7- L’héritage
8- La visibilité des méthodes
9- Les bases de la réflexion
10- Les symboles
11- La classe Time
12- Les classes Date et Datetime
13- La classe Array
14- Manipuler des chaînes de caractères
15- Les expressions rationnelles, partie 1
16- Les expressions rationnelles, partie 2
17- Les entrées sorties
18- Les attributs de fichier
19- Fichiers temporaires et répertoires
20- Sérialisation et persistance
21- Les threads en ruby
22- Synchroniser les threads
23- La programmation système
24- La programmation réseau
25- Les tests automatisés, partie 1
26- Les tests automatisés, partie 2
27- Utiliser le debugger




1- LES TYPES PRIMITIFS STRING ET NUMERIC

Bienvenue dans cette toute nouvelle série dédiée à la découverte du langage Ruby.

Nous allons commencer par des sujets très simples qui vont vous permettre d’acquérir des bases solides avant nous attaquer à des aspects plus complexes du langage.

Cette série sera découpée ne traitant que d’un sujet bien délimité.

Avant de démarrer assurez vous que vous possédez une version récente de Ruby, nous supposons dans cette vidéo que votre version de Ruby est supérieure ou égale à la 2.2.

Vous pouvez vérifier la version actuelle à l’aide de la commande ruby -v.

Si votre version de Ruby est trop ancienne, vous pouvez installer une version plus récente grâce à un gestionnaire de version tel que Rbenv ou encore RVM.

Dans ses vidéos nous utiliserons beaucoup un outil très utile livré avec Ruby qui s’appelle IRB. C’est une console interactive qui permet de saisir du code Ruby et de voir en retour le résultat de l’expression évaluée.

C’est donc notamment très pratique pour tester de petits morceaux de code, vérifier une syntaxe ou comparer deux implémentations.

Dans ces vidéos IRB vous permettra de voir en temps réel le résultat d’une expression sans avoir à tester de votre côté en parallèle.

Entrons maintenant dans le vif du sujet en parcourant les types “primitifs” disponibles en Ruby et que vous utiliserez au quotidien.

En Ruby, tout est objet, même ce que nous appelons ici les types primitifs. Quand nous utilisons ces types primitifs nous utilisons donc en fait une instance d’une classe.

Nous appelons ici la méthode times sur l’entier 2 ce qui va nous permettre d’afficher deux fois la chaîne “Hello”.

Nous appelons ensuite la méthode upto sur l’entier 3 avec pour paramètre 5 ce qui va nous permettre d’afficher les entiers de 3 à 5.

Nous créons maintenant une chaîne “Hackademy” sur laquelle nous appelons la méthode size qui nous permet de connaître sa taille.

On voit que ses types primitifs sont en fait des objets sur lesquels on peut appeler des méthodes.

La classe String

Voyons maintenant plus en détail la classe String.

Nous stockons ici une chaîne dans la variable s puis nous vérifions sa classe à l’aide de la méthode class. C’est bien une String qu’on a ici.

Nous appelons ensuite la méthode succ qui permet d’avoir le successeur de cette chaîne, ici le dernier caractère devient le “z”.

Nous changeons maintenant le contenu de cette chaîne par “hackademy” suivi d’un retour à la ligne. Et nous appelons la méthode chomp dessus qui permet de supprimer ce retour à la ligne.

Nous changeons à nouveau le contenu de la variable s pour y mettre “hackademy”. Nous allons appeler la méthode chop dessus qui permet simplement de supprimer le dernier caractère.

Nous vérifions maintenant la longueur de la chaîne s grâce à la méthode length.

Nous appelons maintenant la méthode capitalize qui n’agit que sur la première lettre. Puis la méthode upcase qui elle permet de transformer toute la chaîne.

Les numériques : Numeric, Fixnum, Bignum, Integer, Float

Passons maintenant aux Numeric, c’est à dire les entiers mais aussi les nombres à virgules flottante.

Nous allons d’abord stocker l’entier “20” dans la variable i, vérifier sa classe, c’est un Fixnum. Les Fixnum sont en fait une classe enfant de la classe Numeric.

Cette classe Numeric regroupe donc les entiers, les flottants, les très grands entiers.

Nous appelons la méthode succ qui permet d’avoir le successeur, c’est ici 21.

Nous faisons une simple addition, 2 + 3 et nous nous rendons compte que c’est en fait un sucre syntaxique pour un appel de méthode : 2.+(3).

Vérifions maintenant la classe d’un nombre à virgule flottante, c’est un Float. Les Float eux aussi sont des enfants de la classe Numeric.

Conclusion

Voilà pour cette présentation rapide des objets des classes String et Numeric que vous utiliserez au quotidien.

Nous n’avons ici qu’effleuré la surface des possibilités de ces classes. Je vous invite donc à en lire la documentation pour découvrir l’étendu des possibilités.

2- LES TYPES PRIMITIFS ARRAY ET HASH

Les tableaux : Array

Les tableaux sont très utilisés et vous permettent de stocker et d’ordonner un jeu d’objets. On peut stocker des objets de différents types au sein d’un même tableau.

On stocker un tableau dans la variable a. Ce tableau contient certain nombre d’entiers. Ici 2, 5 et 10.

On va ensuite ajouter un entier supplémentaire à la fin de ce tableau grâce au double chevron qui est un sucre syntaxique pour un vrai appel de méthode, la méthode push.

On va maintenant ajouter un entier en début de tableau grâce à la méthode unshift et on va essayer de rappeler l’une des valeurs grâce à son index que l’on passe entre crochets. C’est encore une fois un sucre syntaxique pour la méthode at.

Nous ajoutons maintenant en fin de tableau deux objet nil qui représentent le néant. Et nous allons compacter le tableau, c’est à dire supprimer l’ensemble de ses valeurs nil.

On appele donc pour ce faire la méthode compact!.

On ajoute maintenant un tableau en fin de tableau et on va donc se retrouver avec un tableau à deux dimensions. On va maintenant aplatir ce tableau grâce à la méthode flatten! puis on va rendre les valeurs uniques grâce à la méthode uniq!. On a donc ici chaîné deux appels de méthode.

On ajoute maintenant une chaîne “foo” en fin de tableau et on va chercher à connaître son index grâce à la méthode index.

On mélange ensuite le tableau grâce à la méthode shuffle.

On va maintenant créer un nouveau tableau qui va contenir les valeurs doublées de chaque valeur du premier tableau et pour ce faire on utilise la méthode map qui nous permet d’itérer sur chaque valeur du premier tableau et de stocker dans le nouveau tableau la valeur qui est retournée par notre morceau de code, ici la valeur multipliée par deux.

On se retrouve donc bien avec un tableau pour lequel l’ensemble des valeurs ont été doublées et notamment la chaîne qui est devenue “foofoo”.

On va maintenant soustraire au tableau double_a le tableau a, on a donc supprimé les valeurs communes.

On procède ensuite à une jointure entre le tableau double_a et le tableau a, grâce au |

Et on fait maintenant une intersection entre le tableau double_a et le tableau a. Ce qui nous retourne uniquement les valeurs communes.

Les tableaux associatifs : Hash

On passe maintenant aux tableaux associatifs représentés en Ruby par la classe Hash. On va donc stocker dans la variable h un tableau associatif. On utilise donc une accolade ouvrante et un ensemble de clés / valeurs puis on referma avec une accolade fermante.

Ici les clés sont un élément un peu spécial de Ruby qu’on appelle des symboles mais on reviendra sur ce point précis plus tard.

On va ensuite essayer d’afficher la valeur de l’une des clés.

On essaie de récupérer la valeur d’une clé qui n’existe pas ce qui nous retourne nil et on va modifier le comportement de ce hash pour qu’il ait une valeur par défaut sur les clés qui n’existent pas, ici la chaîne “Foo”.

Si on appelle à nouveau cette clé qui n’existe pas, on a maintenant la valeur par défaut qui nous est retournée.

On recherche maintenant une clé qui existe.

On crée un nouveau hash avec une clé commune au premier hash, le symbole :a. Et une clé qui n’existe pas, le symbole :z dans lequel on va stocker “baz”.

On note que la valeur pour la clé a est différente de la valeur pour la clé a du premier tableau.

On va maintenant procéder à un merge entre les deux tableaux et on voit donc que la valeur associée à la clé a a été modifiée pour prendre celle qui était disponible dans h2 et la clé z est tout simplement ajoutée.

Conclusion

Voilà pour cette présentation rapide des types Array et Hash que vous utiliserez trés souvent.

Bien évidemment ce n’est ici que approche très succincte et une fois encore je vous invite à aller lire la documentation relative à ces classes.

3- LES STRUCTURES DE CONTROLE

Les classiques

Voyons tout d’abord les structures classiques.

Comme vous pouvez vous en douter, il existe en Ruby le mot-clé if qui permet de créer une branche conditionnelle dans le code.

On peut donc écrire des choses comme :

if 1 == 1
	puts "OK"
end

On utilise dans cet exemple le double égal pour comparer deux valeurs entre elles. Si les valeurs sont égales, on utilise la méthode puts qui va nous permettre d’afficher une chaîne. On clôt notre condition avec le mot-clé end.

On peut aussi créer des conditions à deux branches qui permettent de couvrir le cas où les valeurs sont égales mais aussi le cas où elles ne le sont pas. Pour ce faire, on utilise le mot-clé else en coordination avec le mot-clé if :

if 1 > 2
	puts "1 est plus grand que 2"
else
	puts "1 est plus petit que 2"
end

Si la condition n’est pas égale à true alors on passera dans la branche else ce qui est le cas dans notre exemple.

On utilise ici le chevron pour vérifier si la valeur de gauche est plus grande que la valeur de droite.

Pour aller plus loin, on peut encore ajouter des branches à notre condition grâce au mot-clé elsif. C’est un cas plus rare mais c’est une possibilité du langage. On peut donc écrire des conditions du type :

if false
	puts "faux"
elsif true
	puts "vrai"
else
	puts "autre"
end

C’est donc la branche elsif qui remplie la condition et on affiche “vrai”.

Utilisation de unless

Très souvent on a besoin d’inverser une condition, on peut donc utiliser le mot-clé if avec un point d’exclamation devant la condition pour l’inverser :

if !false
	puts "OK"
end

mais il existe un idiome en Ruby pour exprimer cette condition inversée, c’est le mot clé unless. On peut donc ré-écrire notre condition de la façon suivante :

unless false
	puts "OK"
end

On gagne donc en lisibilité et les intentions sont plus claires.

Les conditions post-fixées

Dans bien des cas, on souhaite écrire une condition qui ne va concerner qu’une seule ligne de code. En Ruby, plutôt que d’ouvrir un bloc conditionnel pour une seule ligne, on peut utiliser des conditions post-fixées.

Elles consistent à être placées directement après la ligne de code concernée et sont une fois encore un moyen d’améliorer la lisibilité du code :

puts "Ok" if true

On peut également utiliser le unless en version post-fixée :

puts "Ok" unless false

Cas complexes

Pour les cas plus complexes, on utilisera souvent la structure de contrôle case qui dans d’autres langages s’exprime avec le mot-clé switch.

En Ruby cette structure de contrôle est bien plus puissante. Elle ne se cantonne pas à vérifier une égalité simple pour chaque branche, elle permet d’embarquer des conditions évoluées :

case "Ceci est une chaîne"
when "foo"
	puts "Branche 1"
when 1..10
	puts "Branche 2"
when /une/
	puts "Branche 3"
else
	puts "Branche 4"
end

Quand on exécute ce code, c’est la branche 3 qui ressort. Pourquoi ?

La première branche permet de vérifier si la valeur testée est égale à la chaîne “foo”.

La deuxième vérifie si cette valeur est un entier compris entre 1 et 10. C’est un Range, une classe livrée avec Ruby que nous découvrirons par la suite.

La troisième branche compare la valeur à l’expression rationnelle /une/ délimitée par les slashs. Notre valeur contient bien la sous-chaîne “une” c’est pourquoi la branche 3 ressort.

La branche else est le cas par défaut qui est appelé si aucune des conditions précédentes n’a été satisfaite. Cette branche est facultative.

Conclusion

Vous avez donc maintenant les bases pour mettre en place des structures de contrôle dans votre code Ruby. Ces structures constituent la base d’un programme et vous seront utiles au quotidien.

4- VARIABLES ET IDENTIFIEURS

La théorie

En Ruby, les variables et autres identifieurs commencent généralement par une lettre. Les régles de bases sont simples.

Les variables locales commencent par une lettre ou un underscore. Commencer le nom d’une variable par un underscore permet d’avertir l’interpréteur que vous n’allez pas utiliser cette variable mais que c’est intentionnel. Ça évite donc d’avoir un message d’avertissement à l’exécution.

Les conventions en Ruby veulent que si une variable contient plusieurs mots, on les sépare par un underscore.

Des exemples de variables locales pourraient donc être :

user = "Nico"
_unused = "Non utilisé"
some_var = 1234

Il faut également distinguer des pseudo-variables qui sont générées automatiquement par l’interpréteur. Il s’agit de self qui représente l’objet courant ou encore nil qui est une instance de la classe NilClass.

Il y a également __FILE__ et __dir__ qui utilisés dans un fichier Ruby permettent d’obtenir respectivement le chemin absolu vers ce fichier ainsi que le chemin absolu vers le répertoire qui contient ce fichier. Au sein d’IRB ces deux variables n’ont pas réellement de sens.

Viennent ensuite les variables globales qui commencent obligatoirement par un $. Ces variables une fois définies sont accessibles à travers tout le programme, quelque soit le fichier dans lequel on se trouve et celui où elles sont définies. Ces variables sont à utiliser avec parcimonie car elles “polluent” l’ensemble du programme et dénotent très souvent un problème de conception.

Voici quelques exemples de variables globales :

$version = "1.2.6"
$NOT_A_CONST = 42

Nous avons ensuite les variables d’instances qui sont un type de variable très utilisé. Elles sont utilisées au sein d’un objet et représentent des données qui lui sont directement liées. Seul l’objet y a accès.

Ces variables d’instances commencent toujours par un @ et suivent les mêmes conventions que les variables locales. Voici quelques exemples :

@foobar = "baz"
@some_var_123 = 123

Nous avons également les variables de classe qui contiennent des données accessibles à l’ensemble d’une classe et des ses instances. Ces variables sont partagées entre tous les objets issus de la classe en question. Les variables de classe commencent par deux @. Voici quelques exemples :

@@counter = 10
@@file_path = "/some/dir/file.txt"

Il reste pour finir les constantes qui doivent commencer par une lettre majuscule. Par convention, de nombreux rubyistes écrivent les constantes avec uniquement des lettres majuscules et séparent les éventuels mots par un underscore.

Les constantes sont destinées à stocker des informations qui ne sont pas appelées à être modifiées. Il faut tout de même être vigilant puisque pour des raisons techniques, il est possible en Ruby de modifier une constante existante. Ça n’empêchera pas le programme de fonctionner, l’interpréteur émettra simplement un message d’avertissement.

Voici quelques exemples de noms de constantes :

STATUSES = ["draft", "published", "pinned"]
API_URL = "http://something.com"

En pratique

Dans la pratique, en Ruby, les développeurs aiment encapsuler les logiques métier dans des classes et c’est dans ce contexte qu’on travaille le plus et que nous utilisons ces différents types de variables.

Voyons donc un exemple dans le contexte d’une classe. Ne vous inquiétez pas, nous aurons l’occasion de voir plus en détail le fonctionnement des classes dans les prochains épisodes :

class User
	MIN_AGE = 18
	MAX_AGE = 90

	@@count = 0

	def initialize(name)
		@name = name
		@@count += 1
	end

	def self.instances_count
		@@count
	end
end

Nous avons donc définie deux constantes, une variable de classe ainsi qu’une variable d’instance qui est initialisée dans le constructeur, la méthode initialize.

Cette classe contient deux méthodes, le constructeur qui permet de préparer l’objet lorsqu’on crée une instance et une méthode de classe qui sert ici à retourner le nombre d’instances qui ont été créées par cette classe.

Pour finir utilisons cette classe nouvellement crée :

User.new("nico")
User.new("martin")
User.new("jon")

User.instances_count

On a donc créé trois instances de la classes User et c’est bien ce que nous confirme l’appel à la méthode de classe instances_count.

Conclusion

Vous connaissez maintenant les différents types de variables et d’identifieurs à votre disposition ce qui vous permettra de stocker correctement les informations dans votre programme en fonction de leur contexte.

5- CREER UNE CLASSE

La théorie

Ruby propose de base de nombreuses classes qui vous faciliteront le travail quotidien mais dans la plupart des cas, vous aurez besoin de créer vos propres classes.

L’intérêt d’une classe est de permettre d’encapsuler les comportements spécifiques à un objet métier et d’isoler sa logique du reste du code. Cette encapsulation va faciliter la maintenance et l’évolution des fonctionnalités liées à cet objet métier.

Comme vous avez pu le voir dans les épisodes précédents, une classe en Ruby se construit grâce au mot-clé class qui permet d’ouvrir une classe pour y définir des attributs et des méthodes.

Ce mot-clé class sera suivi du nom de la classe que vous voulez créer. Le nom de la classe est une constante et à ce titre il doit commencer par une majuscule.

Dans une classe on pourra stocker des constantes, des variables de classe, des variables d’instance, des méthodes de classe et des méthodes d’instance.

Les données stockées dans des variables de classe sont disponibles dans tous les objets de cette classe. Les données stockées dans des variables d’instance seront disponibles uniquement dans l’objet qui les a définies.

Il est intéressant de noter que puisqu’en Ruby tout est objet, les classes elles même sont des objets. Ce sont en fait des instances de la classe Class.

La pratique

Créons une classe Animal qui permettra de gérer des animaux.

class Animal
  WILD = true

  @@counter = 0

  def initialize(name, sex, age)
    @@counter += 1
    @name, @sex, @age = name, sex, age
  end

  def description
    puts "#{@name} is #{@age} years old."
  end

  def self.instances_count
    puts "We created #{@@counter} animals."
  end
end

On a donc créé une classe dans laquelle on a définie une constante WILD, une variable de classe @@counter qui est initialisée à 0 directement dans la définition de la classe.

On a ensuite définie la méthode initialize qui sert de constructeur. Cette méthode est appelée automatiquement dès qu’une instance est allouée. On profite de cette méthode pour stocker nos différents paramètres dans des variables d’instance mais aussi pour incrémenter notre compteur. On aura donc un historique du nombre d’animaux qu’on a instancié.

On a également définie une méthode d’instance description qui aura pour but d’afficher des informations concernant l’animal, ici on ré-utilise nos variables d’instance @name et @age pour construire une phrase descriptive.

Finalement on a définie une méthode de classe instances_count qui est donc précédée par le mot-clé self. Cette méthode a pour but de nous informer sur le nombre d’instances qui ont été créées.

On va maintenant utiliser cette classe pour tester son comportement. On commence par charger notre fichier dans IRB grâce à la méthode load

load 'animal.rb'

On peut utiliser notre classe.

a1 = Animal.new('Simba', 'male', 5)
a2 = Animal.new('Cheetah', 'male', 20)

a1.description
a2.description

Vérifions maintenant le nombre d’instances créées :

Animal.instances_count

On peut également accéder à la constante depuis l’extérieur de la classe grâce à la notation :: :

Animal::WILD

C’est très souvent utilisé pour accéder à la valeur depuis une autre partie du code.

Conclusion

Vous connaissez maintenant les bases de la création d’une classe en Ruby. Vous allez donc pouvoir commencer à organiser votre code de façon plus modulaire. Il reste évidemment beaucoup d’autres choses relatives aux classes à découvrir pour prétendre les maitriser, c’est ce que nous verrons dans les prochains épisodes.

6- LES ACCESSEURS

La théorie

Dans la vidéo précédente nous avons créé une classe basique qui permet de gérer des animaux.

Bien souvent vous aurez besoin de pouvoir modifier les propriétés d’une instance existante. Le fait d’avoir créé des variables d’instances ne suffit pas à pouvoir les lire ou les modifier depuis l’extérieur de la classe. Pour pouvoir accéder à ces variables d’instances depuis l’extérieur, que ce soit en lecture ou en écriture, vous devez écrire ce qui dans beaucoup de langage s’appelle les “getter” et les “setter”.

Les accesseurs

En Ruby ce sont les accesseurs. Ce sont des méthodes dédiées à la lecture ou à la modification du contenu d’une variable d’instance. Nous allons améliorer notre classe existante pour pouvoir lire et modifier le contenu de la variable d’instance @age :

class Animal
  WILD = true

  @@counter = 0

  def initialize(name, sex, age)
    @@counter += 1
    @name, @sex, @age = name, sex, age
  end

  def age
    @age
  end

  def age=(age)
    @age = age
  end

  def description
    puts "#{@name} is #{@age} years old."
  end

  def self.instances_count
    puts "We created #{@@counter} animals."
  end
end

On a donc ajouter la méthode age qui fait office de “getter”, son but est simplement de retourner la valeur de la variable d’instance @age. On a également ajouté la méthode age= qui sert quant à elle de “setter”. Elle va permettre de modifier le contenu de la variable d’instance @age. On note la présence du = dans le nom de méthode. Ce n’est absolument pas obligatoire mais par convention et pour améliorer la lisibilité, les Rubyistes ont pour habitude d’ajouter un = au noms de méthodes servant de “setter”. Cette méthode prend un paramètre qui sera utilisé comme nouvelle valeur.

Testons cette classe améliorée :

load 'animal.rb'

a = Animal.new('Simba', 'male', 5)
a.age
a.description
a.age = 10
a.age
a.description

Ça fonctionne donc comme voulu, c’est satisfaisant sur le plan fonctionnel mais ce n’est pas du tout représentatif du code idiomatique à utiliser en Ruby. Ce besoin est tellement courant en Ruby qu’il met à notre disposition des macros permettant de faire ce travail à notre place.

On a donc à disposition trois méthodes qui sont attr_reader, attr_writer et attr_accessor qui permettent respectivement de créer un “getter”, un “setter” ou les deux à la fois. On peut donc modifier notre classe pour la simplifier :

class Animal
  WILD = true

  @@counter = 0

  attr_accessor :age

  def initialize(name, sex, age)
    @@counter += 1
    @name, @sex, @age = name, sex, age
  end

  def description
    puts "#{@name} is #{@age} years old."
  end

  def self.instances_count
    puts "We created #{@@counter} animals."
  end
end

On a donc supprimer nos deux méthodes dédiées à la gestion de l’âge pour les remplacer par un appel à attr_accessor suivi du nom de la variable d’instance pour laquelle les méthodes doivent être générées.

On peut recharger notre code dans IRB et vérifier que ça fonctionne toujours. On remarque un message d’avertissement qui nous dit que nous avons redéfinie une constante. Effectivement le fait de recharger notre fichier a écrasé la constante définie lors du premier chargement :

load 'animal.rb'

a = Animal.new('Simba', 'male', 5)
a.age
a.description
a.age = 10
a.age
a.description

Le comportement est identique mais le code est plus concis et les risques de bugs sont donc réduits.

Conclusion

Vous connaissez maintenant les bases de la création des accesseurs dans une classe en Ruby. Vous allez donc pouvoir améliorer vos classes existantes et mettre en place des méthodes permettant de manipuler vos attributs d’instances.

7- L'HERITAGE

La théorie

Dans la vidéo précédente nous avons amélioré une classe existante qui permet de gérer des animaux.

Nous allons aujourd’hui apprendre à nous servir de l’héritage en Ruby.

L’héritage permet de créer une classe sur la base d’une autre pour en modifier certains aspects. Ça permet donc de refléter des comportements spécifiques qui ne peuvent pas être mis en commun dans la classe parent.

Imaginez que sur la base de notre classe Animal nous voulions créer des chiens mais aussi des chats. Le cri de chaque animal est différent et il est donc impossible de le mutualiser dans la classe Animal. C’est une méthode que nous devrons mettre en place spécifiquement dans chaque classe enfant. Pour autant on souhaite garder tous les comportements communs pour ne pas avoir à les ré-écrire. C’est ce que permet l’héritage.

Mise en place de l’héritage

Nous allons donc créer notre première classe qui hérite de la classe Animal. Ce sera la classe Dog. Dans cette classe, nous allons créer une méthode qui permet à l’animal d’émettre son cri. En Ruby, l’héritage se fait grâce à l’opérateur < :

class Dog < Animal
  WILD = false

  def cry
    puts "Woof!"
  end
end

On a donc défini une classe Dog qui hérite de la classe Animal. De ce fait elle possède déjà tous les comportements de la classe Animal :

load 'animal.rb'
load 'dog.rb'

d = Dog.new('Snoopy', 'male', 10)
d.age
d.description

Mais cette classe possède également une nouvelle méthode qu’on peut appeler :

d.cry

Ce qui est impossible sur un objet de la classe Animal :

a = Animal.new('Simba', 'male', 5)
a.cry

Une exception NoMethodError est levée.

On voit également qu’on a redéfini la constante WILD parce que les chiens ne sont pas des animaux sauvages :

Dog::WILD

Redéfinition de méthode

L’héritage laisse aussi la liberté de ré-écrire entièrement ou en partie des méthodes de la classe parent. On va donc modifier la description pour qu’elle soit plus personnelle :

class Dog < Animal
  WILD = false

  def cry
    puts "Woof!"
  end

  def description
    puts "I'm #{@name} the dog and I'm #{@age} years old!"
  end
end

On a donc complètement écrasé la définition du parent pour avoir un comportement personnalisé :

load 'dog.rb'

d = Dog.new('Snoopy', 'male', 10)
d.description

On aurait pu vouloir garder le comportement par défaut mais simplement y ajouter du comportement additionnel. C’est possible grâce au mot-clé super qui permet d’appeler la méthode correspondante du parent. Modifions notre classe :

class Dog < Animal
  WILD = false

  def cry
    puts "Woof!"
  end

  def description
    super
    puts "He's a dog."
  end
end

Testons à nouveau :

load 'dog.rb'

d = Dog.new('Snoopy', 'male', 10)
d.description

On a donc la méthode description de la classe Animal qui génère la première ligne, puis le méthode description de notre classe Dog qui génère la deuxième ligne.

Conclusion

L’héritage est un moyen très flexible pour architecturer vos classes et éviter la redondance. C’est une notion qu’il est nécessaire de maîtriser si vous voulez vous attaquer à des projets ambitieux et bien encapsuler le comportement de chaque entité. Je vous invite donc à faire des essais de votre côté !

8- LA VISIBILITE DES METHODES

La théorie

Lorsque vous écrivez une classe en Ruby, il est possible de limiter la visibilité de ses méthodes. Certaines méthodes sont conçues pour être utilisées directement par la classe, elle n’ont pas vocation à être utilisé à l’extérieur de celle ci. En limitant la visibilité, vous empêchez leur utilisation depuis l’extérieur.

Par défaut, les méthodes que vous ajoutez à une classe sont publiques, elle peuvent donc être appelées depuis l’extérieur. Les mots-clés protected et private permettent de modifier la visibilité des méthodes qui sont définies ensuite.

On a donc trois visibilités à disposition : public, protected et private.

En pratique

Depuis la classe:

Écrivons une classe qui utilise les trois :

class Visibility
  def public_method
    puts "public"
  end

  protected

  def protected_method
    puts "protected"
  end

  private

  def private_method
    puts "private"
  end
end

Voyons maintenant comment ces trois méthodes se comportent au sein de la classe en ajoutant deux méthodes qui les utilisent :

def without_self
  public_method
  protected_method
  private_method
end

def with_self
  self.public_method
  self.protected_method
  self.private_method
end

Testons maintenant dans IRB :

load 'visibility.rb'
visibility = Visibility.new
visibility.without_self

Les trois méthodes sont donc appelées sans le moindre problème avec un receveur implicite.

Testons maintenant la version utilisant les self

visibility.with_self

Dans ce cas les méthodes publique et protégée sont bien appelées mais la méthode privée lève une exception NoMethodError.

Il est donc impossible d’appeler une méthode privé avec un receveur explicite, elle ne peut être appelée que sur l’objet courant.

À l’extérieur de la classe

Voyons maintenant comment ces méthodes se comportent depuis l’extérieur de la classe :

visibility.public_method

La méthode publique peut, sans surprise, être appelée de l’extérieur.

visibility.protected_method
visibility.private_method

Les méthodes protégée et privée lèvent quant à elles une exception NoMethodError. Il est donc impossible d’y faire appelle depuis l’extérieur. Elles ont une visibilité limitée et ne font pas partie de l’interface publique de la classe.

Différence entre protectedet private

Vous devez sûrement vous demander quelle est la différence entre les visibilités protected et private. Elle est subtile.

Si l’objet qui envoie le message, autrement dit qui appelle la méthode, est du même type que l’objet qui le reçoit alors il peut appeler une méthode protégée. Il reste impossible d’appeler une méthode privée même dans ce cas.

Voyons un exemple :

class Extended < Visibility
  def call_methods(other)
    other.public_method
    other.protected_method
    other.private_method
  end
end

class NotRelated
  def public_method
    puts "public"
  end

  protected

  def protected_method
    puts "protected"
  end

  private

  def private_method
    puts "private"
  end
end

On ajoute une classe Extended qui hérite de Visibility. Cette classe implémente une méthode call_methods qui permet d’appeler nos trois méthodes mais sur un autre objet passé en paramètre.

Nous ajoutons également une classe NotRelated qui implémente nos trois méthodes mais qui n’a aucun lien avec nos deux classes précédentes.

On recharge notre fichier et on teste le comportement :

e = Extended.new
e.call_methods(Visibility.new)

Les méthodes publique et protégée de l’objet Visibility ont pu être appelées depuis l’objet Extended. C’est possible car ces deux objets proviennent de la même classe parent :

e.is_a?(Visibility)

Seule la méthode privée ne peut être appelée.

Essayons maintenant avec un objet qui n’a aucun lien :

e.call_methods(NotRelated.new)

Ici seule la méthode publique peut être appelée, l’appel à la méthode protégée lève une exception NoMethodError car les deux objets ne font pas partie de la même hiérarchie :

e.is_a?(NotRelated)

L’utilisation la plus fréquente pour les méthodes protégées est de permettre à deux objets du même type de coopérer. Par exemple pour écrire une méthode qui permet de comparer deux objets, disons des personnes et savoir qui est le plus âgé sans pour autant avoir accès publiquement à l’âge dans l’interface.

Conclusion

Il est important de maîtriser le concept de visibilité des méthodes pour être en mesure d’écrire des classes avec une interface propre. Le plus souvent vous utiliserez des méthodes publiques et privées mais il reste essentiel de savoir utiliser les méthodes protégées pour permettre à vos objets de communiquer entre eux sans polluer l’interface publique.

9- LES BASES DE LA REFLEXION

La théorie

La réflexion est la possibilité pour un programme d’analyser son environnement pendant l’exécution. Elle lui permet d’interroger les objets qui le compose et de les étendre ou de les modifier à la volée.

Les possibilités de réflexion en Ruby sont très avancées. Il est par exemple possible de créer des méthodes ou savoir si une variable existe pendant l’exécution. Ces possibilités ouvrent de nombreuses perspectives du point de vue du programmeur qui peut de ce fait créer des programmes très dynamiques qui réagiront de manière spécifique en fonction des conditions dans lesquels ils sont exécutés.

Un programme peut par exemple définir une méthode avec une implémentation complètement différente en fonction du système sur lequel il est exécuté.

Il sera également possible de définir des méthodes relatives à une structure de données qu’on ne connaît pas à l’avance mais qui est découverte au moment de l’exécution.

En pratique

On peut par exemple utiliser le mot-clé defined? qui permet de savoir si un identifieur, une variable par exemple, existe dans le contexte courant :

if defined?(some_var)
  puts "some_var = #{some_var}"
else
  puts "some_var n'existe pas"
end

De la même façon, la méthode respond_to? permet de savoir si un objet répond à une méthode donnée :

s = "test"
s.respond_to? :size
s.respond_to? :foo

Il devient donc possible de mettre en place des mécanismes évolués dans le code puisqu’on peut interroger les objets à la volée pour savoir s’ils implémentent tel ou tel comportement. Nous n’avons plus besoin de connaître le type de l’objet à l’avance. Il nous suffit de nous renseigner à son sujet.

On peut, à l’exécution, obtenir la classe d’un objet :

s.class
123.class
Array.new.class

On pourra également savoir si un objet est une instance d’une classe donnée :

s.instance_of?(String)
s.instance_of?(Object)

On peut étendre cette demande à l’objet ainsi que ses ancêtres :

s.kind_of?(String)
s.kind_of?(Object)
s.kind_of?(Array)

Pour poursuivre, on peut récupérer une liste complète des méthodes qui peuvent être appelées sur l’objet :

s.public_methods
s.protected_methods
s.private_methods

On peut aussi manipuler les variables d’instance de l’objet :

s.instance_variables
s.instance_variable_set :@foo, "test"
s.instance_variables_get :@foo

Au niveau d’une classe, il est possible de connaître ses ancêtres et modules inclus :

Array.ancestors
Array.included_modules

Le module ObjectSpace contient toutes les informations concernant les objets actuellement disponibles dans l’interpréteur :

ObjectSpace.each_object { |o| p o }
ObjectSpace.count_objects

C’est notamment ce module qui permet de forcer le lancement du garbage collector, c’est à dire, le nettoyage en mémoire des objets non-utilisés.

Il permet aussi de définir un comportement à mettre en place lorsqu’un objet est détruit.

Finalement l’un des mécanismes les plus utilisés est la définition à la volée de méthodes.

Quand l’interpréteur recherche une méthode, il regarde si elle existe sur l’objet, puis sur la classe de l’objet et finalement si l’un de ses ancêtres la définie. Si elle n’est pas trouvée, Ruby va exécuter la méthode method_missing de l’objet si elle existe sinon, une exception NoMethodError sera levée :

"test".foo

class String
  def method_missing(name, *args, &block)
    puts "La méthode #{name} n'existe pas"
  end
end

"test".foo

Conclusion

Comme vous pouvez le voir, la réflexion est une fonctionnalité extrêmement puissante. On peut, en peu de lignes de code, rendre un programme très dynamique, auto-évolutif et faire en sorte qu’il sache prendre en charge des cas complexes ou des éléments du contexte d’exécution qui ne peuvent pas être connus à l’avance.

Je vous invite toutefois à faire attention à ne pas en abuser. Quand vous pouvez développer une fonctionnalité sans recourir à la méta-programmation, il faut faire sans. Le code qui est évalué et généré au moment de l’exécution sera forcément plus lent que le même code “statique”.

Sachez que cette possibilité existe et qu’elle a beaucoup à offrir mais utilisez la avec parcimonie.

10- LES SYMBOLES

La théorie

Les symboles sont l’un des éléments du langage les plus utilisés.

Un symbole est une instance de la classe Symbol. Pour en déclarer un il suffit d’utiliser les deux-points suivi par un identifieur.

Un symbole ressemble à une chaîne dans son fonctionnement et son usage. Mais contrairement aux chaînes, chaque symbole n’a qu’une seule instance et n’existe donc qu’une seule fois en mémoire :

:foo.class

:foo.object_id
:foo.object_id

"foo".object_id
"foo".object_id

Il y a donc une différence importante entre ces deux types d’objets qui peut impacter la consommation mémoire ou les performances de votre programme.

Si on crée un tableau comme suit :

array = ["foo", :foo, "foo", :foo, :foo]

Les deux chaînes “foo” seront des objets distincts en mémoire. Les symboles :foo sont quand à eux plusieurs références vers le même objet en mémoire. On peut voir ça comme un objet unique qui a un nom.

Utilisation

Identifieur

L’utilisation la plus commune pour les symboles est de s’en servir pour représenter le nom d’une variable ou d’une méthode. Rappelez-vous dans les épisodes précédents nous avons vu comment ajouter des accesseurs à une classe, nous nous sommes servi de symboles à cette occasion :

class Foo
  attr_accessor :something
end

Nous aurions très bien pu utiliser une chaîne à la place du symbole, ça aurait tout de même fonctionné. À vrai dire, la plupart des méthodes livrées avec Ruby qui attendent un symbole en argument savent aussi se débrouiller avec un chaîne.

Un symbole est finalement très proche d’un chaîne, il est composé par une suite de caractères. On peut considérer qu’un symbole est un peu comme une chaîne immuable. On peut d’ailleurs simuler le comportement d’un symbole à l’aide d’une chaîne :

"foo".freeze.object_id
"foo".freeze.object_id

Le fait de geler la chaîne fait qu’elle n’est instanciée qu’une fois et représente toujours le même objet en mémoire.

Mais malgré ces similitudes, il ne faut pas s’y méprendre, les symboles et les chaînes sont des objets différents avec une interface publique différente. Les symboles n’héritent pas de la classe String.

:foo.kind_of?(String)

Il est à noter qu’on peut créer des symboles contenant des caractères spéciaux en entourant l’identifieur de guillemets :

:"identifieur peu commun !"

On pourrait donc créer des méthodes avec des noms barbares mais ce n’est pas une bonne idée.

Énumération

Une autre utilisation fréquente des symboles est de s’en servir pour créer des énumérations :

STATUSES = [:draft, :published, :pinned]
NORTH, SOUTH, EAST, WEST = :north, :south, :east, :west

C’est plus expressif que d’utiliser, par exemple, des entiers pour définir les points cardinaux.

Méta-valeur

Un autre usage assez commun est d’utiliser les symboles comme des méta-valeurs. Ils vont donc pouvoir être utilisés comme code de retour de méthodes qui n’ont pas vocation à être des chaînes qui seront utilisées en tant que tel. C’est une fois encore plus expressif que de retourner des entiers.

Conversion des symboles en chaîne et vice-versa

Il est très facile de convertir des symboles en chaîne et vice-versa. C’est pratique pour permettre à vos méthodes d’utiliser indépendamment l’un ou l’autre en tant que paramètre par exemple :

"foo".to_sym
:foo.to_s

def hello_dude(name)
  puts "Hello #{name.to_s}!"
end

hello_dude("Nico")
hello_dude(:Nico)

Génération de proc

La dernière utilisation commune pour les symboles permet de raccourcir les appels à des méthodes qui acceptent un bloc en paramètre. Disons par exemple qu’on veut capitaliser chaque mot d’un tableau, on pourrait faire :

array = ["one", "two", "three"]
array.map { |word| word.capitalize }

mais Ruby permet de raccourcir cet appel très commun grâce à l’utilisation d’un symbole qui va permettre de générer le bloc à la volée :

array.map(&:capitalize)

Conclusion

Les symboles sont un des piliers de Ruby qu’il est important de maîtriser pour améliorer votre code mais aussi pour comprendre celui des autres. Désormais quand vous créerez une chaîne, demandez vous si c’est vraiment ce dont vous avez besoin. Allez-vous la manipuler en tant que tel ? Si la réponse est non, vous avez certainement besoin d’un symbole pour représenter cette valeur.

11- LA CLASSE TIME

La théorie

La manipulation de dates et heures est une nécessité fréquente en programmation. Fort heureusement, en Ruby il est facile de manipuler ce type d’information.

Ruby met à notre disposition la classe Time qui permet de faire la majorité des manipulations inhérentes à ce type de données.

En interne, les objets de la classe Time sont stockés sous forme d’un entier qui représente le nombre de nano-secondes écoulées depuis ce qu’on appelle “Epoch” qui correspond au 1 janvier 1970 à minuit UTC.

Nous verrons dans une prochaine vidéo que les classes Date et DateTime apportent quelques outils supplémentaires qui améliorent la flexibilité.

Création d’objets Time

La chose la plus fondamentale qu’on puisse vouloir faire est de connaître la date et l’heure courante :

Time.new

Nous n’avons pas passé de paramètre à new c’est donc l’heure courante qui nous est retournée. Pour ce cas fréquent, il existe une méthode plus explicite :

Time.now

Il existe plusieurs façon de créer des objets Time. Pour le fuseau horaire local, pour le fuseau horaire UTC et finalement pour un fuseau horaire arbitraire :

Time.local(2015, 8, 12, 8, 15, 20)
Time.utc(2015, 8, 12, 8, 15, 20)
Time.new(2015, 8, 12, 8, 15, 20, "-05:00")

Les paramètres à passer vont de celui ayant la plus grande unité, l’année, à celui ayant la plus petite, la seconde. Seul l’année est un paramètre obligatoire :

Time.local(2015, 8, 12, 8)
Time.local(2015, 8, 12)

Si vous tentez de créer une date invalide, disons avec un mois qui n’existe pas, vous auriez une exception ArgumentError qui serait levée :

Time.local(2015, 13)

Affichage des objets Time

De nombreuses méthodes sont mises à notre disposition pour manipuler ces dates :

t = Time.local(2015, 8, 12, 8, 15, 20)
t.to_s

to_s nous retourne une version textuel de la date facilement compréhensible par un humain mais également simple à analyser avec un programme.

Il est également possible de formater la représentation à votre guise grâce à la méthode strftime :

t.strftime("Printed on %d/%m/%Y")
t.strftime("at %H:%M")  

Les options de formatage sont très complètes et je vous invite à lire la documentation de strftime pour en avoir une liste exhaustive.

On peut par exemple utiliser le formatage suivant pour des dates compatibles ISO 8601 pour nos JSON :

t.strftime("%FT%R")

Les méthodes utilitaires

La classe Time nous livre aussi une panoplie de méthodes qui nous permettent de savoir si une instance correspond à un jour donné :

t.monday?
t.tuesday?
t.wednesday?
t.thursday?
t.friday?
t.saturday?
t.sunday?

Nous avons aussi quelques méthodes qui nous permettent de récupérer le jour, le mois etc :

t.day
t.month
t.year
t.hour
t.min
t.sec
t.usec

On peut également connaître le jour dans l’année que représente notre instance :

t.yday

Les fuseaux horaire

Nous avons également de quoi manipuler les fuseaux horaire. Quel est le fuseau horaire ?

t.zone

Est-ce que c’est une heure d’été ?

t.dst?

Est-ce que c’est une heure UTC ?

t.utc?

Convertir l’heure en heure locale

t.getlocal

Convertir l’heure en heure UTC

t.getutc

Quelle est le nombre de secondes de décalage entre mon heure et l’heure UTC ?

t.utc_offset

Arithmétique

Pour finir, il est également possible de faire de l’arithmétique sur les objets Time :

t1 = Time.now
t2 = t1 + 60

On voit que t2 est exactement une minute plus tard que t1, soit 60 secondes.

t3 = t1 + 120

t1 != t2
t1 < t2

t2 - t1

t2.between?(t1, t3)

Conclusion

La gestion des dates et heures en Ruby est donc quelque chose de très simple grâce à une panoplie de méthodes qui nous facilitent la vie.

Il devient facile d’écrire des méthodes de plus haut niveau pour manipuler de façon avancée ce type de données dans nos programmes. La librairie propose d’ailleurs les extensions Date et DateTime qui apportent encore plus d’aisance dans la manipulation.

D’autres librairies comme ActiveSupport vont encore plus loin et ajoutent encore plus de naturel dans la manipulation des dates.

12- LES CLASSES DATE ET DATETIME

Les classe Date et DateTime et leurs motivations

La classe Date a été créée pour manipuler spécifiquement des dates sans tenir compte de l’heure. La classe DateTime quant à elle a été écrite pour apporter quelques méthodes manquantes à la classe Time, pour mieux cadrer la validation des dates et heures, gérer les différents types de calendriers et faciliter les analyses.

Il faut savoir qu’historiquement, avant Ruby 1.9, la classe Time était plus pauvre et limitée dans les valeurs possibles. Il n’était possible de gérer que des dates allant approximativement de 1901 à 2038.

Depuis la gestion des dates a été revue et le fossé entre la classe Time et DateTime a largement été réduit. Toutefois DateTime apporte encore quelques méthodes supplémentaires. Il est en parti possible de les reproduire dans la classe Time grâce à un require 'time'.

Les classes Date et DateTime partagent de nombreuses fonctionnalités et on va donc les présenter en parallèle.

Pratique

Avant toute chose, il faut charger les classes Date et DateTime :

require 'date'

Création de dates

On peut maintenant les utiliser :

d = Date.today
dt = DateTime.now

On peut bien évidemment créer des dates arbitraires :

Date.new(2015, 9, 15)
DateTime.new(2015, 9, 15, 8, 10, 30, '+7')

On peut aussi créer des dates sur la base de la semaine calendaire et le jour de la semaine en lieux et place des jour et mois absolus :

Date.commercial(2015, 5, 6)

On peut également créer une date sur la base du jour de l’année :

Date.ordinal(2015, 34)

Il nous est également possible de faire de la validation de date :

Date.valid_date?(2015,2,3)
Date.valid_date?(2015,2,29)

Analyse des dates sous forme de chaînes

L’analyse de dates sous forme de chaînes est également facilité.

Là où la classe Time sera perdue :

Time.new("2015-08-05")

La classe Date sera elle capable d’une analyse assez avancée :

Date.parse("2015-08-05")
Date.parse("05/08/2015")
DateTime.parse("01/12/2015 09:43")
```ruby

La classe va même nous apporter quelques méthodes permettant d'analyser des formats de date répandus :

```ruby
DateTime.httpdate("Wed, 26 Aug 2015 15:13:02 GMT")
DateTime.iso8601('2015-02-03T08:10:30+07:00')
DateTime.rfc2822('Sat, 3 Feb 2015 04:05:06 +0700')

Si nous devons aller plus loin en supportant nos propres formats, c’est également possible :

Date.strptime('03-02-2015', '%d-%m-%Y')
Date.strptime('2015!034', '%Y!%j')

Le template de format utilise ici la même syntaxe que strftime que nous avons déja vu.

Arithmétique sur les dates

Comme pour la classe Time, les classes Date et DateTime nous permettent de faire de l’arithmétique :

d + 1
d.next_day

d - 5
d.prev_day(5)

d << 2
d.prev_month(2)
d >> 2
d.next_month(2)

d.prev_year(2)
d.next_year(2)

other_date = Date.new(2015, 8, 2)
d - other_date
d <=> other_date

Affichage et formattage des dates

Pour ce qui est de l’affichage des dates, nous avons également tout le nécessaire :

dt.asctime
dt.to_s
dt.httpdate
dt.iso8601
dt.rfc2822
dt.strftime("%T%:z")
```ruby

Il nous est possible de récupérer le détail des éléments qui composent une date :

```ruby
dt.day
dt.month
dt.year
dt.hour
dt.min
dt.sec
```ruby

```ruby
d.cwday
d.cweek
d.yday

Une fois encore il nous est possible de savoir si la date correspond à un jour donné de la semaine :

d.monday?
d.wednesday?

Au besoin, il est également possible de convertir un objet d’un type vers un autre :

d.to_datetime
dt.to_date
dt.to_time

Finalement il nous est possible de parcourir des périodes pour y faire des traitements divers et variés :

other_date.upto(d).count { |d| d.sunday? } 
d.downto(other_date).count { |d| d.sunday? } 
other_date.step(d, 2).count { |d| d.sunday? }

Conclusion

Comme vous avez pu le voir ici, les classes Date et DateTime apportent les dernières briques qui permettent de manipuler très naturellement des dates et heures.

C’est une base solide pour l’écriture de méthodes de manipulation de plus haut niveau. Rails, par exemple, les utilise très largement pour manipuler les dates et heures en base de données et bien plus encore.

13- LA CLASSE ARRAY

Les déclarations

Nous avons vu qu’il est possible de créer un tableau avec la notation [] :

a = []

Mais il est également possible d’utiliser la syntaxe classique Array.new qui peut prendre de zéro à deux arguments :

a = Array.new(3)
a = Array.new(3, "foo")

Dans le premier cas notre tableau est initialisé à la taille spécifiée avec des éléments vides. Dans le deuxième cas, chaque élément représente ce qui est passé en deuxième argument.

Attention au piège, le tableau contient en fait des références à cet élément, pas des copies. Si vous en modifiez un, vous les modifierez tous :

a[0].capitalize!
a

Si vous souhaitez que chaque élément soit un objet unique plutôt que de multiples références au même objet, il faut utiliser la syntaxe prenant un bloc :

a = Array.new(3) { "foo" }
a[0].capitalize!
a

Sélection et remplacement d’éléments

Il est courant de vouloir récupérer un ou plusieurs éléments en début ou fin de tableau, pour ce faire des méthodes nous facilitent la tâche :

a.first
a.first(2)
a.last
a.last(2)

On sait qu’on peut également récupérer un élément à un index donné grâce à la notation [index] :

a[0]

Mais on peut également récupérer un jeu d’éléments grâce à deux autres notations :

a[1, 2]
a[1..2]

La première demande de commencer la récupération à l’index “1” sur une longueur de deux éléments. La seconde utilise un Range pour demander de récupérer les éléments des index 1 à 2 compris.

On peut se servir de cette même notation pour faire de l’affectation :

a[1, 2] = ["bar", "baz"]
a

a[1..2] = [2, 3]
a

On peut également récupérer plusieurs éléments éparpillés sur des index non-contiguës en une seule opération :

a = [10, 20, 30, 40, 50, 60]
a.values_at(0, 1, 4)
a.values_at(0..2, 5)

Une fois encore, on a utilisé un Range.

Il est également possible de récupérer le premier ou dernier éléments du tableau en le supprimant à la volée ce qui transforme notre tableau en pile :

a_dup = a.dup
a_dup.shift
a_dup
a_dup.pop
a_dup

Compter les éléments

Une autre opération commune est de vouloir compter le nombre d’éléments d’un tableau ou d’un sous-ensemble, plusieurs possibilités s’offrent à nous :

a.size
a.count
a.count { |el| el > 30 }

La méthode size est la plus simple mais aussi la plus rapide puisqu’elle vérifie simplement la taille en mémoire du tableau.

La méthode count quant à elle est plus puissante puisqu’elle autorise un bloc qui va servir de filtre. Cette méthode est par contre plus lente puisqu’elle parcourt l’ensemble du tableau pour donner le résultat.

On va également pouvoir savoir si un tableau est vide ou s’il contient des éléments :

a.empty?
a.any?
a.any? { |el| el > 100 }

Là encore, la méthode any? autorise un bloc de filtrage.

Tri et recherche

La classe Array propose aussi de nombreuses méthodes permettant de faire du tri ou de la recherche :

a.shuffle!
a.sort
a.sort { |a, b| b <=> a }
a.sort.reverse

b = ['Nico', 'Victor', 'Jon', 'Martin']
b.sort_by { |el| el.length }
b.sort_by { |el| [el.length, el] }

On peut rechercher le premier élément du tableau qui rempli une condition :

a.find { |el| el > 20 }

ou tous les éléments qui remplissent la condition :

a.find_all { |el| el > 20 }

Il existe également la méthode permettant de retourner tous les éléments qui ne répondent pas à la condition :

a.reject { |el| el > 20 }

Dans la même veine nous avons la méthode grep qui permet de faire des recherches sur la base d’expressions rationnelles :

b.grep(/ic/)

On peut éventuellement lui passer un bloc de transformation :

b.grep(/ic/) { |el| el.length } 

On peut également rechercher les valeurs minimales et maximales :

b.min
b.max
b.max { |a, b| a[1..-1] <=> b[1..-1] }

Manipulation diverses

Finalement vous pourriez vouloir regrouper les éléments de deux tableaux, deux par deux, c’est ce que propose la méthode zip :

c = a.zip(b)

et construire une chaîne en concaténant ces éléments :

c.join("-")

Conclusion

Nous avons vu ici la majeur partie des possibilités livrées par la classe Array, l’ensemble de ses méthodes devraient sans nul doute vous donner toutes les clés pour pouvoir effectuer vos manipulations. Nous avons volontairement omis quelques méthodes plus exotiques. Jetez un œil à la documentation si vous êtes curieux.

Quelques méthodes supplémentaires très pratiques sont mises à disposition par le module Enumerable que nous découvrirons plus tard.

14- MANIPULER DES CHAINES DE CARACTERES

Les déclarations

Les chaînes peuvent être déclarées de différentes façons. Tout d’abord avec des guillemets simples ce qui créera la chaîne telle quelle sans transformation, il faudra simplement penser à échapper les guillemets simples :

puts 'Ceci est une chaîne'
puts 'Aujourd\'hui'

On peut ensuite créer des chaînes en utilisant des guillemets doubles qui apportent plus de puissance puisqu’on pourra utiliser d’autres séquences d’échappement, des tabulations, retours à la ligne, etc. On pourra également interpoler des variables au sein de la chaîne :

puts "Une tabulation (\t)"

s = "Nico"
puts "De l'interpolation #{s}"

Il existe également des moyens alternatifs de représenter les chaînes qui deviennent pratique quand la chaîne contient beaucoup de guillemets ou de caractères d’échappement :

puts %q(Une chaîne avec des '', des "" et des "\t" et #{s})
puts %Q(Une chaîne avec des '', des "" et des "\t" et #{s})

La version minuscule correspond donc à une chaîne à guillemets simples alors que la version majuscule correspond à une chaîne à guillemets doubles.

Pour déclarer les très longues chaînes, les “Here-Documents” sont là pour nous aider. C’est très utile lorsqu’on souhaite créer une chaîne qui s’étend sur plusieurs lignes :

puts <<EOF
une chaîne sur
plusieurs lignes
EOF

La chaîne doit donc commencer par deux chevrons suivi d’un identifieur. Cet identifieur sera ré-utilisé pour signifier la fin de la chaîne.

Les utilitaires de la classe String

Voyons maintenant les méthodes utiles de la classe String.

Souvent vous aurez besoin de connaître la longueur d’une chaîne :

Comptage

s.length

Ou connaître le nombre d’occurrences d’une sous-chaîne dans la chaîne :

s.count("a")
s.count("a-i")
s.count("^a-i")

Parcourir

On pourra également vouloir parcourir du texte, ligne par ligne :

s = <<EOF
Première ligne
Deuxième ligne
Troisième ligne
EOF

s.each_line do |line|
  puts line.chomp + " !"
end

Ou encore caractère par caractère :

s = "Ruby is awesome!"
s.each_char do |char|
  print char + "-"
end

Il sera assez fréquent de vouloir comparer deux chaînes pour savoir laquelle est la plus “grande”, on pourra le faire en tenant compte de la casse ou non :

"ab" <=> "AB"
"ab".casecmp("AB")

Découpage

On peut également découper notre chaîne sur l’espace, un caractère quelconque ou encore une expression rationnelle :

s.split
s.split("e")
s.split(/\W+/)

On pourra même limiter le nombre de champs retournés :

s.split(/\W+/, 2)

La méthode scan quant à elle va permettre de nous retourner un tableau des éléments qui répondent à une expression rationnelle donnée :

s.scan(/\w+/)
s.scan(/\w+/) { |w| print "<<#{w}>> " }

Formater

Ceux qui ont fait du C doivent sans nul doute connaître la fonction sprintf qui existe aussi en Ruby et permet un formatage puissant des chaînes :

name = "Nico"
age = 34

sprintf("Salut %s, tu as déjà %d ans…", name, age)

Jusque là rien de bien épatant, une simple chaîne avec interpolation aurait fait l’affaire. Mais sprintf va bien plus loin dans ses possibilités :

sprintf("Salut %-20s, tu as déjà %03d ans…", name, age)

On peut également ajuster notre texte à l’affichage :

name.ljust(15)
name.center(15)
name.rjust(15)
name.center(15, "-")

ou encore gérer la casse :

s.downcase
s.upcase
s.capitalize
s.swapcase

Accéder et assigner des sous-chaînes

En Ruby, il est également très facile de travailler avec des sous-chaînes. On peut à la fois accéder à des sous-chaînes mais également les modifier.

D’abord on y accède avec la notation [index de départ, longueur] :

s[0, 4]
s[-8, 7]

Puis à l’aide de Range :

s[0..3]
s[-8..-2]

L’utilisation d’expressions rationnelles est également possible :

s[/a.*e/]

On peut également utiliser l’ensemble de ces notations pour accéder à une sous-chaîne et la modifier :

s[/a.*e/] = "yummy"
s

Rechercher dans une chaîne

Il y d’autres façon de faire des recherches dans une chaîne qui conviendront mieux en fonction du cas d’utilisation :

s.index("m")
s.index(/.y/)

s.rindex("m")
s.rindex(/.y/)

s.include?("a")
s.include?("uby")

On peut aussi obtenir un tableau des occurrences grâce à la méthode scan :

s.scan("m")
s.scan(/.y/)

Manipulation diverses

Dans la série des inclassables, il nous reste quelque méthodes à connaître.

D’abord la possibilité de supprimer un caractère donné dans une chaîne :

s.delete("u")

Il y a également la possibilité d’avoir la suite logique d’une chaîne :

"R2D2".succ

Et finalement la possibilité de répéter une chaîne :

etc = "Etc. " * 3

Conclusion

Nous avons vu ici la majeur partie des possibilités livrées par la classe String qui, de base, est très bien fournie. Ce n’est pas un tour exhaustif des méthodes et de leurs possibilités mais le principal est là. Si vous êtes curieux, je vous conseille de jeter un œil à la documentation.

15- LES EXPRESSIONS RATIONNELLES, PARTIE 1

La théorie

Les expressions rationnelles peuvent être déclarées grâce une paire de slashes, avec la notation %r ou encore grâce à Regexp.new :

s = "Ruby"
/Ruby/ =~ s
/[Rr]uby/ =~ s
%r(^abc) =~ s
Regexp.new("xyz%") =~ s

Il est possible d’utiliser des modifieurs pour modifier le comportement de l’expression rationnelles :

/ruby/i =~ "Ruby"

Il existe beaucoup d’autres ancres et modifieurs utilisables dans les expressions rationnelles. Une bonne compréhension vous apportera beaucoup de bénéfices dans vos développements.

Faire le tour des possibilités prendrait bien plus de temps qu’on en a pour cette vidéo mais heureusement la documentation officielle de la classe Regexp est particulièrement exhaustive.

C’est un sujet tellement vaste que des livres entiers sont consacrés au sujet.

En pratique

Les échappements

Vous voudrez parfois rechercher des caratères qui correspondent à des caractères spéciaux dans les expressions rationnelles. Il faudra donc les échapper.

Vous pourrez les échapper à la main :

/./ =~ s
/\./ =~ s

mais dans les cas plus complexes, il est préférable d’utiliser la méthode escape :

Regexp.escape("[*?]")

Début et fin de chaînes, de lignes et de mots

Les expressions rationnelles sont un bon moyen de détecter les débuts et fin de chaînes, de lignes et de mots.

Chercher un motif en début de ligne utilisera l’ancre “^”, pour la fin de ligne on utilisera “$” :

s = "abc def ghi"
/abc/ =~ s
/def/ =~ s
/^def/ =~ s
/def$/ =~ s
/ghi$/ =~ s

Si on remplace les espaces par des retours à la ligne, les résultats seront différents :

s = "abc\ndef\nghi"
/abc/ =~ s
/def/ =~ s
/^def/ =~ s
/def$/ =~ s
/ghi$/ =~ s

D’autres ancres nous permettent de rechercher en début et fin de chaîne sans prendre en compte les retours à la ligne :

s = "abc\ndef\nghi"
/\Aabc/ =~ s
/\Adef/ =~ s
/def\Z/ =~ s
/ghi\Z/ =~ s

On ne cherche donc plus en début ou fin de ligne mais bien en début et fin de chaîne ce qui est tout à fait différent.

De la même façon, on peut repérer les limites des mots ou au contraire tout ce qui n’en est pas :

s = "Ruby is awesome"
s.gsub(/\b/, "|")
s.gsub(/\B/, "-")

Conclusion

Les expressions rationnelles sont donc un moyen très puissant d’analyser du texte mais c’est aussi un outil assez difficile à prendre en main. Nous n’avons qu’effleuré les possibilités dans cette vidéo.

La vidéo suivante nous permettra d’en apprendre un peu plus.

16- LES EXPRESSIONS RATIONNELLES, PARTIE 2

Bienvenue dans cette vidéo qui fait suite à la découverte de l’utilisation des expressions rationnelles en Ruby.

Nous n’avons la dernière fois pu voir que les bases et nous allons donc essayer d’aller plus loin dans cette nouvelle vidéo.

Continuons donc notre découverte dans IRB.

Les quantificateurs

Vous aurez souvent besoin de gérer des motifs optionnels ou répétés. Les quantificateurs vous permettent de gérer ça.

On peut par exemple définir un ou plusieurs caractères optionnels, il pourront donc apparaître une fois ou pas du tout :

re1 = /ax?b/
re2 = /a[xy]?b/

re1 =~ "ab"
re1 =~ "azb"
re1 =~ "axb"

re2 =~ "ayb"
re2 =~ "acb"

On peut également rechercher un caractère qui apparaît au moins une fois :

re = /[0-9]+/
re =~ "1"
re =~ "8765432"

On peut simplifier cette expression avec l’ancre dédiée à la recherche de numériques :

/\d+/ =~ "123"

Il est très fréquent de vouloir définir un motif qui peut apparaître de zéro ou une infinité de fois :

re = /Yeah!*/
re =~ "Yeah"
re =~ "Yeah!!!!"

On peut préciser le nombre d’occurrences qu’on attend :

re = /\d{3}-\d{2}-\d{2}/
re =~ "123-45-67"
re =~ "1-45-67"

On pourra aussi définir un nombre d’occurrences variable :

re = /\d{1,3}-\d{2}-\d{2}/
re =~ "123-45-67"
re =~ "1-45-67"

Il faut savoir que les quantificateurs *, + et {} sont gourmands par défaut, ils vont essayer de récupérer la plus grande partie de la chaîne qui correspond :

s = "Il est à la fois le plus petit et le plus grand"
/.*le/.match(s)

Pour n’attraper que la plus petite occurrence, il faut demander à l’opérateur de ne pas être gourmand grâce au “?” :

/.*?le/.match(s)

Les groupes

Dans une expression rationnelle, il est possible de créer des groupes ré-utilisables. Chaque groupe pour lequel on a une correspondance sera donc accessible ensuite pour utilisation.

Les groupes simples

Un groupe est créé en utilisant les parenthèses :

"Ruby".sub(/(.)(.)(.)(.)/, '<\4> <\3> <\2> <\1>')

Pour chaque groupe qui aura une correspondance, on obtient un identifieur spécial. On a dans notre exemple quatre jeux de parenthèses et donc les identifieurs de 1 à 4.

La méthode match permet d’aller plus loin en retournant un objet MatchData :

s = "alpha beta gamma delta epsilon"
re = /(b[^ ]+) (g[^ ]+) (d[^ ]+)/
md = re.match(s)
md[1]
md[2]
md[3]
md[0]
md.begin(2)
md.pre_match
md.post_match

Les groupes nommés

Vous aurez sûrement remarqué qu’il faut compter les groupes pour savoir qui correspond à quoi. Ce n’est pas toujours pratique. Heureusement, Ruby nous permet de mettre en place des groupes nommés :

re = /(?<beta>b[^ ]+) (?<gamma>g[^ ]+) (?<delta>d[^ ]+)/
md = re.match(s)
md[:beta]

Il suffit donc de commencer le groupe par un point d’interrogation suivi d’un identifieur entre chevrons. On peut ensuite faire référence au groupe grâce à cet identifieur.

L’écriture est plus longue mais la lisibilité est considérablement augmentée.

Recherche en avant

Dans les expressions rationnelles il est possible de faire de la recherche en avant (lookahead) pour ne les valider que si elles sont suivies ou non par un motif précis. Ce motif ne sera pas retourné dans notre résultat :

s1 = "J'aime le Ruby"
s2 = "J'aime le PHP"
s3 = "J'aime le Javascript"

re = /J'aime le (?=Ruby|Javascript)/
re.match(s1)
re.match(s2)
re.match(s3)

On voit donc que l’expression n’est vérifiée que si elle est suivi par le mot “Ruby” ou “Javascript”.

On peut également faire l’inverse, à savoir ne valider que si ce qui suit ne correspond pas :

re = /J'aime le (?!Ruby|Javascript)/
re.match(s1)
re.match(s2)
re.match(s3)

Ici seule les correspondances n’étant pas suivies par les mots “Ruby” ou “Javascript” seront validées.

Recherche en arrière

Il est également possible de faire le même type d’opérations mais en revenant en arrière (lookbehind).

L’utilisation de cette technique est plus difficile à décrire par un cas pratique. Ça pourrait être pour modifier du contenu dans un langage balisé après analyse de ses balises ou encore pour analyser des séquences ADN.

Je vais donc reprendre un exemple d’utilisation connu pour nécessiter les recherches en arrières.

Imaginons que vous vouliez retrouver dans une séquence ADN, toutes les séquences nucléotidiques, de quatre éléments donc, qui suivent un “T”. On ne pourrait pas simplement chercher le “T” puisqu’on ne serait plus capable de détecter deux séquences contiguës si la première finie par un “T”. Pour illustrer cela utilisons la séquence suivante :

adn = "GATTACAAACTGCCTGACATACGAA"
adn.scan(/T(\w{4})/)

Il manque donc la séquence “GACA” qui suit la séquence “GCCT” qui est elle même une des correspondances.

En utilisant la recherche arrière, on pourrait palier à ce problème en demandant de vérifier également le caractère précédent :

adn.scan(/(?<=T)(\w{4})/)

Conclusion

Les expressions rationnelles permettent donc des recherches très avancées. Ça sera parfois la seule solution à votre problématique.

Il faut par contre avoir conscience qu’elles sont des opérations coûteuses. Si vous pouvez vous en passer votre programme n’en sera que plus réactif.

Pour continuer l’exploration, je vous invite à tester des expressions dans votre console et à lire attentivement la documentation de la classe Regexp.

17- LES ENTREES SORTIES

Bienvenue dans cette vidéo consacrée à la découverte des entrées / sorties en Ruby. Les entrées / sorties sont souvent appelées I/O de l’anglais Input / Output.

La gestion des entrées / sorties est une des bases de la programmation. Même si vous n’en avez pas conscience parce que vous utilisez un framework, vous les utilisez sans cesse. Les usages sont d’une variété quasi infinie.

Aujourd’hui, nous nous pencherons uniquement sur les fichiers classiques, stockés sur un support. Ça nous permettra de creuser le sujet dans les vidéos suivantes.

Théorie

En Ruby, toute la gestion des entrées / sorties repose sur la classe IO. C’est la classe qui définie les comportements communs à toutes les entrées / sorties.

De cette classe découle la classe File qui encapsule tous les détails de la gestion de fichiers tels que les permissions, les dates de création, etc.

C’est sur ces fondements que sont réalisées d’autres classes et librairies qui vont permettre de persister des éléments dans des fichiers ou dans des bases de données.

Ouvrir / fermer des fichiers

Commençons donc à travailler avec les fichiers qui sont le moyen le plus simple et le plus commun pour stocker de l’information persistante.

Les opérations les plus communes sont l’ouverture et la fermeture des fichiers. Entre deux on lira ou écrira des informations.

Pour ouvrir un fichier on utilisera la méthode File#new qui prend en premier paramètre le chemin vers le fichier à ouvrir et en deuxième paramètre optionnel le mode d’ouverture.

Le mode d’ouverture sert à indiquer si on ouvre le fichier en lecture, en écriture, en lecture et écriture, etc.

On peut donc ouvrir un fichier en écriture :

file1 = File.new("one", "w")

ou en lecture:

file2 = File.new("one")

Quand vous ouvrez des fichiers, il est de bon ton de les fermer une fois que vous avez fini de les utiliser. Si vous ouvrez de nombreux fichiers et que vous les laissez tous ouverts, le système finira par se plaindre.

D’ailleurs si vous ne fermez pas un fichier dans lequel vous avez écrit, il est fort probable que vous perdiez une partie des données.

Fermons donc nos deux fichiers précédemment ouverts :

file1.close
file2.close

Pour ouvrir des fichiers, vous pouvez également utiliser la méthode File#open qui est plus ou moins un synonyme de File#new à ceci près que vous pouvez lui passer un bloc et le fichier sera automatiquement fermer à la fin de ce bloc :

File.open("test.txt", "w") do |file|
  file.puts "Line 1"
  file.puts "Line 2"
  file.puts "Line 3"
end

C’est sans aucun doute la façon la plus élégante de procéder. Nous avons donc ouvert en écriture le fichier “test.txt”, qui a été créé s’il n’existait pas.

Nous avons ensuite appelé la méthode puts. Nous l’avons déjà largement utilisé jusqu’ici pour afficher des valeurs à l’écran. Par défaut puts s’applique sur la sortie standard, ce qui dans la plupart des cas est la console dans laquelle le script est exécuté.

Ici c’est différent, nous l’appelons explicitement sur la variable locale file qui représente le fichier que nous avons ouvert ce qui a pour effet d’écrire les chaînes dans ce fichier.

Mettre à jour un fichier

Si vous veniez à ouvrir à nouveau le même fichier, toujours avec le mode “w”, vous écraseriez le contenu existant.

Pour mettre à jour un fichier existant, il faut utiliser le mode adéquat. Pour pouvoir lire et écrire dans un fichier, il suffit d’ajouter “+” derrière le mode utilisé. Il reste maintenant à choisir le mode en fonction de ce que vous souhaitez faire.

Pour mettre à jour un fichier, en partant du début, on utilisera le mode “r+” :

f1 = File.new("one", "r+")

Pour écraser le contenu du fichier existant ou en créer un nouveau, on utilisera “w+” :

f2 = File.new("one", "w+")

Finalement pour compléter le fichier, en partant donc de la fin de ce dernier, on utilisera le mode “a+” :

f3 = File.new("one", "a+")

Notez que les “+” ne sont pas nécessaire à la mise à jour, ils ne sont là que pour indiquer qu’on souhaite pouvoir lire et écrire. On pourrait très bien se cantonner à un simple “a” pour faire de la mise à jour de fichier.

Accès aléatoires

Naturellement quand vous lirez un fichier, la lecture se fera de manière séquentielle. Ce sera donc caractère par caractère ou ligne par ligne.

File.open("test.txt", "r") do |file|
  puts file.getc
  puts file.gets
end

Pour vous déplacer à un endroit précis, il faudra utiliser la méthode IO#seek qui attend qu’on lui passe un entier représentant le nombre de bytes dont on veut avancer dans le fichier :

File.open("test.txt", "r") do |file|
  file.seek 7
  puts file.gets
end

On a donc avancé de 7 bytes pour passer la première ligne qui contient la chaîne “Line 1” constituée de 6 caractères plus un retour à la ligne ce qui nous permet d’arriver directement à la deuxième ligne.

Par défaut la méthode IO#seek commence toujours en début de fichier, on peut aussi lui demander de commencer à la position actuelle en lui passant un deuxième paramètre :

File.open("test.txt", "r") do |file|
  file.seek 7
  puts file.gets

  file.seek 0
  puts file.gets

  file.seek 7
  puts file.gets

  file.seek 0, IO::SEEK_CUR
  puts file.gets
end

On peut également commencer depuis la fin de fichier grâce à la constante IO::SEEK_END.

Entrée / sortie par défaut

Comme on l’a expliqué un peu plus tôt, par défaut, les méthodes Kernel.puts et Kernel.gets si elles sont appelées sans receveur vont tenter d’écrire sur la sortie standard et de lire depuis l’entrée standard.

Ce phénomène s’explique par le fait que l’interpréteur lorsqu’il est lancé met à disposition trois constantes STDIN, STDOUT et STDERR. Ces constantes sont automatiquement affectées à trois variables globales qui sont $stdin, $stdout et $stderr.

Quand vous appelez puts, vous appelez en fait implicitement $stdout.puts. De la même manière en faisant gets, vous faites en réalité un $stdin.gets.

En ré-assignant ces variables globales, vous pouvez modifier le comportement par défaut de puts, gets et autres méthodes similaires ce qui peut se révéler très pratique.

Créons un script dans notre éditeur :

file = File.new("io.txt", "w")
puts "Hello"

$stdout = file
puts "Bye"

$stdout = STDOUT
puts "Done"

Nous pouvons maintenant l’exécuter en console : ruby io.rb

On voit que sur la sortie standard apparaissent les chaînes “Hello” et “Done”. Si on consulte le contenu du fichier “io.txt”, on voit qu’il contient la ligne “Bye” : cat io.txt

Gestion des arguments

Une autre constante très utile lorsqu’on écrit des scripts est ARGF. Elle contient le contenu des fichiers qui ont été passés au script lors de son lancement.

On pourrait par exemple écrire un script qui affiche le contenu de ces fichiers :

puts ARGF.read

Puis le lancer : ruby cat.rb test.txt io.txt

Plutôt simple n’est-ce pas ?

Une autre constante spéciale existe et permet de lister les arguments passés au script :

ARGV.each do |arg|
  puts arg
end

On le lance : ruby args.rb foo bar baz

C’est un élément indispensable pour quiconque souhaite écrire des scripts.

Lire l’entrée standard

Une autre tâche dont vous aurez souvent besoin si vous écrivez des scripts sera de pouvoir lire des entrées utilisateur. Pour ce faire, il nous suffira d’écouter ce qu’il se passe sur $stdin :

puts "Il y a de l'écho ici !"

while line = $stdin.gets
  puts "echo: " + line.chomp
end

On le lance : ruby echo.rb

On a donc créé une boucle infinie qui attend une entrée utilisateur puis la répète.

Conclusion

Vous connaissez donc maintenant les bases de la manipulation des entrées / sorties. Ces quelques bases vous permettent d’aller déjà assez loin dans l’interaction entre votre script et le système ou l’utilisateur.

Dans les prochaines vidéos nous approfondirons ces concepts pour apprendre à manipuler les attributs des fichiers, travailler avec les fichiers temporaires, les répertoires ainsi que la persistance d’objets.

18- LES ATTRIBUTS DE FICHIER

Bienvenue dans cette vidéo consacrée à la manipulation des attributs avancés des fichiers. Les usages que nous verrons ici sont particulièrement destinés à des scripts s’exécutant sur des systèmes de type Unix.

Bloquer les fichiers

Dans vos scripts système il sera parfois nécessaire de bloquer l’accès à un fichier pour éviter qu’un autre processus tente par exemple de le modifier pendant que vous vous en servez.

Plusieurs modes de blocage sont disponibles, ils correspondent à ce qui est mis à disposition par la commande Unix flock.

On peut donc bloquer le fichier en mode partagé (LOCK_SH) pour lequel plusieurs processus pourront accéder au fichier en parallèle. Aussi longtemps qu’il existera des processus accédant au fichier dans ce mode, il sera impossible d’obtenir un blocage exclusif (LOCK_EX).

Le blocage exclusif va lui permettre d’obtenir un accès unique au ficher, souvent pour y écrire. Si d’autres processus veulent accéder au fichier en mode partagé, ils devront attendre que le blocage exclusif soit levé.

On peut en plus ajouter le drapeau LOCK_NB aux deux premiers modes. Dans ce cas si un processus tente d’accéder au fichier, une exception sera levé, ça sera impossible. Il n’y aura pas de système de mise en attente de la levée du blocage.

Finalement lorsqu’un processus a fini d’utiliser un fichier, il est de son devoir de lever le blocage grâce au drapeau LOCK_UN.

En Ruby, on utilisera ces concepts de la manière suivante :

file = File.new("foo", "w")

file.flock(File::LOCK_EX)
file.flock(File::LOCK_UN)

file.flock(File::LOCK_SH)
file.flock(File::LOCK_UN)

file.close

Pour faire une analogie, vous pouvez imaginer un prof devant son tableau. Le tableau correspond à notre fichier.

Pendant que le prof écrit au tableau, il y pose un blocage exclusif. Pendant ce temps, personne ne peut le lire. Aucun autre blocage partagé ou exclusif ne peut être obtenu.

Lorsqu’il a fini, le prof s’écarte du tableau et lève le blocage exclusif. Les étudiants peuvent maintenant lire le tableau, c’est à dire accéder au fichier, en y posant un blocage partagé. On peut poser plusieurs blocages partagés en parallèle.

Pendant que des élèves lisent le tableau, il est impossible pour le professeur de le modifier en y posant un blocage exclusif.

Appartenance et permissions

Il sera également courant de vouloir manipuler l’appartenance des fichiers et les permissions associées.

On pourra tout d’abord vouloir vérifier qui est le propriétaire ou le groupe d’un fichier :

s = File.stat("foo")
s.uid
s.gid

C’est en fait ici l’identifiant de l’utilisateur et du groupe qu’on obtient. Pour obtenir le nom correspondant il faut passer par le module Etc :

Etc.getpwuid(s.uid).name
Etc.getgrgid(s.gid).name

On va pouvoir, de la même façon, récupérer les permissions associées au fichier :

s.mode

Par défaut, les permissions sont affichées en mode octal, on peut obtenir un affichage plus classique à l’aide de sprintf :

sprintf("%o", s.mode)

Bien que très standard, cet affichage n’est pas le plus pratique à exploiter, on a donc un ensemble de méthodes qui nous permettent d’obtenir les information plus clairement :

s.readable?
s.writable?
s.executable?

Bien évidemment il existe aussi des commandes pour modifier les permissions, le propriétaire et le groupe. Vous serez toutefois limité par les permissions de l’utilisateur courant :

file = File.new("foo")
file.chmod(0444)
file.chown(502, 20)

Gestion des informations d’horodatage

Lorsque vous utilisez un fichier des horodatages sont mis-à-jour. Depuis Ruby vous pouvez récupérer trois données, la date de dernier accès qu’on appelle “access time”, la date de dernière modification appelée “modify time” et finalement la date de la dernière modification, changement de propriétaire ou de permissions inclus, c’est ce qu’on appelle le “change time”.

Ces informations peuvent être obtenues directement depuis la classe “File”, depuis une instance de cette même classe via des méthodes dédiées, ou encore à travers les informations de la méthode stat :

File.mtime("foo")

file.mtime
file.atime
file.ctime

s = file.stat
s.atime

En plus de la consultation, il est possible de définir les heures de modification et changement en passant par la méthode utime :

today = Time.now
yesterday = today - 86400
File.utime(today, yesterday, "foo")

Existence et taille des fichiers

Une autre tâche fréquente en administration système que vous voudrez pouvoir automatiser au travers de vos script Ruby est la vérification de l’existence de fichiers et de leur taille.

Vérifions donc l’existence de quelques fichiers :

File.exist?("foo")
File.exist?("bar")

Bien que notre fichier existe, il est peut-être vide et nous sera donc tout aussi inutile. Ruby met à notre disposition une méthode qui permet de vérifier si le fichier existe et s’il n’est pas vide :

File.size?("foo")
File.size?("args.rb")

Si le fichier existe et qu’il n’est pas vide, sa taille nous est retournée.

Dans la même veine, la méthode zero? nous permettra de savoir si le fichier existe et qu’il est vide :

File.zero?("foo")
File.zero?("args.rb")
File.zero?("bar")

Il est à noter que l’objet stat disponible sur l’instance d’un fichier dispose lui aussi des méthodes size? et zero?.

Il existe bien évidemment la méthode plus directe size qui se contentera de donner la taille du fichier. Si le fichier passé en argument n’existe pas, une exception sera levée :

file.size
File.size("foo")
File.size("bar")

Caractéristiques spéciales des fichiers

Finalement vous voudrez peut-être pouvoir consulter les caractéristiques spéciales des fichiers.

Vous pourrez par exemple vouloir savoir si un fichier est un terminal :

$stdin.tty?
file.tty?

Comme tout est fichier sous Unix, il est intéressant de connaître son type :

File.file?("foo")
File.directory?("/tmp")
File.pipe?("foo")
File.socket?("foo")

Plutôt que de tirer à l’aveuglette quand vous ne connaissez pas le type probable du fichier, vous pouvez passer par la méthode ftype :

File.ftype("/dev/disk0s1")

Conclusion

Ces quelques pointeurs vous permettront sans aucun doute de vous lancer dans l’écriture de scripts d’administration et de gestion du système de fichier.

Pour compléter vos compétences, nous verrons dans le prochain épisode comment manipuler les fichiers temporaires ainsi que les chemins et répertoires.

19- FICHIERS TEMPORAIRES ET REPERTOIRES

Les fichiers temporaires

Dans bien des cas, vous serez amené à manipuler des fichiers temporaires pour stocker de l’information. C’est par exemple nécessaire lorsqu’on écrit une librairie qui gére les envois de fichier.

Quand on crée un fichier temporaire, on veut s’assurer que son nom est unique et qu’il sera bien effacé lorsqu’on aura fini de s’en servir. Biensûr il est possible de gérer ça soit même en écrivant le code nécessaire mais ça peut être fastidieux et sujet à erreur surtout si vous souhaitez avoir un code portable.

Heureusement, Ruby met à notre disposition la classe Tempfile dédiée à la gestion de cette opération courante :

temp = Tempfile.new("foo")
temp.path

Un fichier avec un nom unique, commençant par “foo” a donc été créé dans le répertoire temporaire du système. Ce nom est garanti unique à travers les threads et le processus.

temp.puts "On ajoute une ligne"
temp.close

temp.open
temp.gets
temp.close!

Si nous n’avions pas détruit le fichier, il l’aurait été automatiquement en fin de processus. Il est recommandé de le faire explicitement pour éviter que le fichier temporaire reste disponible sur le système de fichier jusqu’à ce qu’il soit collecté alors qu’il n’est plus utilisé par notre programme.

Si votre programme stocke des données sensibles qui ne doivent pas être accessibles aux autres processus, vous pouvez supprimer le fichier juste après sa création. Sur les systèmes POSIX, tant que le descripteur du fichier n’est pas clos, vous pouvez toujours vous en servir même s’il n’est plus visible sur le système de fichier :

file = Tempfile.new("bar")
file.unlink
file.puts "Fichier toujours accessible"
file.close

Manipuler les chemins

Un autre besoin récurrent est de manipuler les chemins de fichiers. On pourra vouloir connaître le répertoire correspondant :

f = File.open("args.rb")
path = f.path

File.dirname(path)

Ou simplement le nom du fichier avec ou sans son extension :

File.basename(path)
File.basename(path, ".rb")

On peut donc préciser l’extension à occulter, quand on ne la connaît pas à l’avance, on pourra utiliser l’étoile.

On va également pouvoir obtenir un chemin absolu depuis un chemin relatif :

File.expand_path("~")

ou recomposer un chemin depuis ses différents composants en respectant le séparateur du système courant :

File.join("usr", "local", "bin")

La classe Pathname a pour but de regrouper toutes ces fonctionnalités et permet également d’aller un peu plus loin :

pn = Pathname.new(path)
pn.directory?
pn.file?
pn.split
pn.extname
pn.size

Cette classe offre beaucoup d’autres méthodes très utiles et je vous invite à lire sa documentation. Vous pourrez par exemple pour un répertoire connaître son parent et ses enfants.

Manipuler les répertoires

Finalement, en plus d’analyser les chemins, on voudra pouvoir se déplacer dans les répertoires, en créer, en supprimer, etc.

Tout d’abord on pourra se renseigner sur le répertoire courant :

Dir.pwd

puis se déplacer dans un autre :

Dir.chdir("Desktop")
Dir.pwd

Cette méthode peut prendre un bloc bien pratique puisque le changement de répertoire ne sera effectif qu’à l’intérieur du bloc :

Dir.chdir("..") do
  puts Dir.pwd
end
puts Dir.pwd

On pourra également lister l’ensemble des entrées d’un répertoire :

Dir.chdir("..")
Dir.foreach(".") do |item|
  puts item
end

Dans certains cas, on préférera simplement récupérer cette liste sous forme d’un tableau :

Dir.entries(".")

On va maintenant s’attacher à gérer les répertoires en Ruby en commençant par en créer un :

Dir.mkdir("rep_1")

Plutôt simple mais comment créer une chaîne de répertoire ? Comme vous le savez sûrement, il est impossible de créer un répertoire si son parent n’existe pas. Il faudrait donc créer les répertoires de la chaîne un par un pour assurer un bon déroulé. C’est un peu fastidieux et Ruby met donc à notre disposition une méthode qui peut le faire pour nous :

FileUtils.mkdir_p("rep_2/rep_3/foo/bar")

Parfait on peut maintenant passer au renommage d’un fichier et par extension d’un répertoire :

FileUtils.mv("rep_1", "rep_10")

Pour finir, on voudrait pouvoir supprimer un répertoire. Avec la méthode de base on ne pourra supprimer qu’un seul répertoire à la fois et seulement s’il est vide. S’il n’est pas vide, une exception sera levée :

Dir.delete("rep_10")

Bien souvent c’est un répertoire et l’ensemble de son contenu que vous voudrez supprimer récursivement. Ruby met à disposition une méthode nous évitant d’avoir à écrire un code fastidieux :

FileUtils.rm_r("rep_2")

Conclusion

Vous savez donc maintenant vous déplacer à travers le système de fichier, lister les entrées, en créer et en supprimer.

Vous avez pu voir en bonus comment mettre en place des fichiers temporaires de manière robuste ce qui vous permettra de stocker de l’information qui n’a pas vocation à être conservée et qui ne doit pas être accessible par un autre processus.

Le prochain épisode vous donnera les clés pour sérialiser et persister des objets Ruby dans des fichiers.

20- SERIALISATION ET PERSISTANCE

Nous verrons aujourd’hui comment persister des objets Ruby sur le disque pour pouvoir les ré-utiliser plus tard.

Plusieurs possibilités s’offrent à nous pour arriver à nos fins.

La brique la plus simple mise à notre disposition est la classe Marshal.

Persistence simple avec Marshal

Dans la plupart des cas, le besoin sera de pouvoir sauver de manière permanente des objets que vous avez créé pendant le cycle de vie de votre programme. Vous pourrez donc, en le relançant plus tard, récupérer ses informations pour les restituer à l’utilisateur ou continuer un traitement.

On pourrait par exemple vouloir gérer une structure consistant en un tableau à deux dimensions. Disons un tableau de chansons, pour chaque chanson on aurait un tableau contenant, l’auteur, le titre et la durée.

Une fois ce tableau construit on voudra le sauver sur le disque pour y accéder à nouveau plus tard.

Mettons cet exemple en pratique. On crée donc notre tableau de musiques :

songs = [
  ['Metallica', 'Nothing else matters', 389],
  ['Metallica', 'Sad but true', 325],
  ['Born of Osiris', 'The other half of me', 212]
]

On va maintenant créer un fichier pour stocker les informations :

File.open("songs", "w") do |f|
  # Et y sérialiser les informations
  Marshal.dump(songs, f)
end

On pourra ensuite récupérer ses informations pour s’en servir :

File.open("songs") do |f|
  songs = Marshal.load(f)
end

Comment faire plus simple ?

Malheureusement cette méthode ne permettra pas de sérialiser tout et n’importe quoi. Les classes de bas niveau ne sont pas sérialisables. On ne pourra pas, par exemple, sérialiser des objets issus des classes IO ou Proc. Les singletons, classes anonymes ou encore les modules ne peuvent pas non plus être sérialisés.

Vous pouvez par contre sérialisé sans souci vos classes maisons pour peu qu’elles n’embarquent pas un objet de bas niveau.

Persistence améliorée avec PStore

Avec la classe Marshal, vous devez gérer un certain nombre de détails pour vous assurer que vos données persistées restent dans un état cohérent. Si un problème survient en milieu d’exécution, vous risquez d’avoir un fichier corrompu.

Pour faciliter la gestion de ce type de problématiques, Ruby met à disposition la classe PStore. Un objet PStore peut gérer une hiérarchie d’objets Ruby sous forme de clés / valeurs. La grande différence avec la classe Marshal est qu’un objet PStore va écrire ses changements sur le disque de façon transactionnel.

Lorsqu’un jeu de données sera modifié tout sera donc modifié avec succès ou sinon rien ne sera modifié. On évite donc de se retrouver avec des données dans un état transitoire sur le disque.

PStore se basant sur la classe Marshal, il est à noter que les mêmes limitations s’appliquent en ce qui concerne ce qui peut être sérialisé ou pas.

Voyons un exemple d’utilisation :

require "pstore"

db = PStore.new("users")
db.transaction do
  db[123] = { name: "Nico", age: 34 }
end

Plus tard, nous pourrons récupérer ces informations :

db2 = PStore.new("users")
db2.transaction { puts db2[123] }

Il est à noter qu’il est possible d’interrompre une transaction à tout moment, soit en conservant les changements effectués jusque là grâce à la méthode commit, soit en annulant les changements en utilisant la méthode abort.

Tous les changement qui suivront dans la transaction seront ignorés :

db2.transaction do
  db2[1] = "test"
  db2.commit
  db2[2] = "foo"
end

db2.transaction do
  p db2[1]
  p db2[2]
end

On a donc enregistré les changements qui précédaient le commit mais pas les suivants. On aurait pu tout annuler avec abort :

db2.transaction do
  db2[3] = "foo"
  db2.abort
  db2[4] = "bar"
end

db2.transaction do
  p db2[3]
  p db2[4]
end

On voit donc qu’ici rien n’a été persisté.

Persistance avec YAML

Un autre format de persistance dont vous avez peut-être déjà entendu parler est YAML. L’avantage de ce format de stockage est qu’il est parfaitement lisible par une personne.

Le fait de charger la librairie “yaml” aura pour effet d’apporter une méthode to_yaml à chaque objet. Il devient alors possible de sérialiser des instances des classes de bases mais aussi de vos propres classes dans un format facilement compréhensible et exploitable.

Créons une classe simple pour illustrer l’utilisation :

class Foo
  def initialize(a, b, c)
    @a, @b, @c = a, b, c
  end
end

foo = Foo.new("test", 123, {a: "foo", b: "bar", c: "baz"})
foo.to_yaml

On peut donc sauver ces données dans un fichier comme on le ferait pour stocker n’importe quelle autre chaîne :

File.open("data.yml", "w") do |f|
  f.write(foo.to_yaml)
end

Voyons à quoi ressemble ce fichier :

cat data.yml

On pourra recharger ces informations plus tard de la manière suivante :

file = File.new("data.yml")
YAML.load(file)

On a donc à nouveau une instance de la classe Foo à disposition. Elle contient les mêmes informations que lors de la sauvegarde.

Ce format de stockage est très apprécié des Rubyistes, notamment pour sa facilité d’édition.

Stockage clés / valeurs avec DBM

Finalement, une autre option intéressante est la librairie DBM qui est un système de stockage sous forme de ficher basé sur des paires de clé / valeur. Ce sera donc un très bon moyen de persister vos Hash.

Il est à noter que les clés et les valeurs doivent être des chaînes. Hormis cela, un objet DBM se comporte de la même manière qu’un Hash et répond à quasiment toutes ses méthodes :

require "dbm"

d = DBM.new("dbm")
d["foo"] = "bar"
d.close

d["foo"]

d2 = DBM.open("dbm")
d2["foo"]
d2.to_hash
d2.close

Conclusion

Vous avez donc maintenant toutes les clés pour travailler avec les entrées sorties et vous pourrez largement en tirer partie dans vos programmes pour améliorer leurs qualités fonctionnelles.

21- LES THREADS EN RUBY

Introduction aux threads

Les threads aussi appelés «processus légers» sont un moyen très pratique d’exécuter du code en parallèle sans avoir à lancer un nouveau processus qui serait plus coûteux.

Il faut savoir que les threads en Ruby ne sont pas des threads système mais des threads utilisateur qui fonctionnent indépendamment du système hôte. C’est bien sûr moins performant mais aussi plus portable.

Les threads vous seront particulièrement utiles lorsque vous avez des morceaux de code qui peuvent fonctionner de manière indépendantes ou pour gérer une partie du code qui passe la plupart de son temps à attendre des événements.

Si votre logique métier fonctionne de manière sérialisée alors les threads ne vous aideront pas. Ils ne seront également pas très pratiques si vous devez synchroniser l’accès à de nombreuses données globales.

Sachez qu’un code multi-threadé sera toujours plus sujets aux bugs qu’une version sans threads et qu’il compliquera le débogage.

Il faut donc savoir analyser le besoin, le contexte et le gain potentiel avant de choisir de multi-threader un programme.

Création et manipulation de threads

Les bases de l’utilisation des threads consiste à en créer, y faire transiter de l’information puis les stopper. On pourra évidemment obtenir une liste des threads, leur état actuel, etc.

Créer un thread

Pour créer un thread, il suffit d’utiliser la classe dédiée :

thread = Thread.new do
  # something
end

On obtient donc en retour une instance de la classe Thread qui peut être manipulée depuis le thread principal.

On pourra passer des arguments à un thread lors de sa création :

Thread.new("foo", "bar") do |a, b|
  puts a
end

Il faut savoir qu’un thread a accès au contexte courant, il peut donc modifier le contenu des variables qui lui sont accessibles au moment de la création. C’est un vrai piège auquel il faut faire attention :

a = "foo"
b = "bar"

Thread.new(a) do |a|
 a = "not changed"
 b = "changed"
end

a
b

La variable locale a qui est passée au thread sous le nom de a n’est impactée que localement, par contre la variable b qui n’a pas été passée localement est accessible depuis le contexte global. En la modifiant, on modifie sa valeur au niveau global.

Accéder aux variable locales des threads

Il est donc clair qu’il est dangereux d’utiliser des variables du contexte extérieur depuis un thread. On voudra pourtant parfois pouvoir passer des données au contexte global depuis notre thread. Heureusement des mécanismes sont mis à notre disposition pour pouvoir faire ça proprement :

thread = Thread.new do
  t = Thread.current
  t[:foo] = "Here is foo"
  t[:bar] = 42

  baz = "Not available outside"
end

thread[:foo]
thread[:bar]
thread.key?(:foo)
thread.key?(:baz)

On peut donc depuis notre thread, communiquer de l’information à l’extérieur sans polluer le contexte global. C’est beaucoup plus propre ! Ne passez jamais par les variables du contexte global pour communiquer de l’information ou vous vous exposerez à de gros soucis.

Connaître et changer le statut d’un thread

La classe Thread met à notre disposition des méthodes qui nous permettent d’interroger et de modifier le statut d’un thread donné.

On peut récupérer la liste des threads vivants grâce à la méthode de classe list ou encore obtenir une référence vers le thread principal via la méthode de classe main. La méthode de classe current nous retournera une référence vers le thread en cours d’exécution comme on a pu le voir avant :

Thread.current == Thread.main

Thread.new do
  puts Thread.list.size
  puts Thread.current == Thread.main
end

Thread.list.include?(Thread.main)

D’autres méthodes vont nous permettent de modifier ou de connaître le statut d’un thread :

t1 = Thread.new { loop {} }

t1.status
t1.kill
t1.alive?
t1.stop?

On voit que si le thread est en cours d’exécution son statut sera run, s’il est en attente à cause d’un sleep ou de l’attente d’un retour d’une entrée / sortie alors il sera en statut sleep. S’il a fini son travail avec succès le statut sera false et nil si une exception a été levée.

On est donc en mesure de savoir où en est le déroulement de nos différents thread et donc, par extension, de notre programme principal.

Pour les cas plus complexe, il sera possible de forcer un thread à passer la main à un moment précis de son exécution avec la méthode pass, de lui demander d’arrêter son exécution grâce à la méthode stop puis de la reprendre plus tard en utilisant la méthode run ou encore wakeup qui réveillera le thread sans forcer son exécution immédiate.

Pour autant, il ne faudra pas se servir de ces mécanismes pour tenter de faire de la synchronisation. D’autres techniques sont mises à notre disposition spécialement pour ça.

Attendre la fin d’exécution d’un thread

Dans bien des cas, vous aurez besoin de faire en sorte que votre programme attende que vos threads aient fini leur travail pour rendre la main. Vous pourriez mettre en place cette vérification dans votre thread principal grâce aux méthodes vues précédemment mais ça deviendrait vite fastidieux.

Fort heureusement, une méthode prête à l’emploi nous est fournie, elle s’appelle join. Si cette méthode est appelée sur un thread, le programme principal ne rendra pas la main tant que le thread en question n’aura pas fini son exécution.

Il est donc assez commun, avant la fin de son programme principal de mettre en place un idiome qui permet de s’assurer que tous les threads de l’application ont fini leur travail :

Thread.list.each { |t| t.join if t != Thread.main }

On a donc listé tous les threads disponibles dans l’application et pour chacun d’entre eux, on a appelé join qui garantit que le programme ne peut pas rendre la main avant la fin de leur exécution.

On a simplement ajouté un test qui n’appelle pas join sur le thread principal qui serait alors incapable de se terminer.

Conclusion

Nous avons pu voir dans cet épisode que Ruby met à notre disposition une base solide d’outils qui nous permettent de mettre en place de la concurrence et donc de gagner du temps précieux de traitement lorsqu’une autre partie du programme est en attente d’éléments pour continuer.

Dans l’épisode suivant, nous verrons comment synchroniser les threads entre eux pour assurer la cohérence des données.

22- SYCHRONISER LES THREADS

Nous verrons cette fois ci comment synchroniser la manipulation des données entre les différents threads. En effet, si aucune synchronisation n’est mise en place il se pourrait que les différents threads se marchent sur les pieds en modifiant une même variable. Les modifications de l’un serait donc écrasées et perdues par l’autre.

C’est ce qu’on appelle avoir un code “thread-safe” qui assure qu’un code qui va être manipuler par plusieurs threads en même temps se comportera comme attendu.

Démonstration par l’exemple

Voyons un exemple que j’ai déjà écrit pour gagner du temps :

x = 0

10.times.map do |i|
  Thread.new do
    puts "avant ajout (#{i}): #{x}"
    x += 1
    puts "après ajout (#{i}): #{x}"
    puts
  end
end.each(&:join)

puts "valeur finale : #{x}"

Comme vous le voyez, quand chaque thread démarre, il récupère la valeur courante de x pour l’afficher. Ensuite il tente d’ajouter 1 à cette valeur mais le résultat obtenu est variable du fait du traitement en parallèle de cette même variable par plusieurs threads concurrents.

Sans synchronisation, il devient impossible de prédire l’état de la variable x et par extension, le comportement de notre méthode. Le résultat retourné sera parfois correct et parfois erroné…

Il faut donc porter une attention particulière aux variables ayant une portée plus large que le thread dans lequel elles sont utilisées.

Atomicité

Pour obtenir un code thread-safe, il va falloir travailler de manière atomique. Si nos opérations de modifications des valeurs sont faîtes de telle façon que les autres threads ne peuvent ni les lire, ni les écrire pendant qu’on travaille dessus, alors notre code sera thread-safe.

Pour gérer un ensemble d’opération de façon atomique, il nous faut utiliser les Mutex dont le rôle est de garantir que les opérations sont traitées d’une traite, comme un ensemble indivisible.

Notre code thread-safe

Modifions donc notre code pour le rendre thread-safe. Pour cela il suffit d’encapsuler l’ensemble du code à jouer de manière atomique dans un bloc dédié.

Voyons le code :

x, mutex = 0, Mutex.new

10.times.map do |i|
  Thread.new do
    mutex.synchronize do
      puts "avant ajout (#{i}): #{x}"
      x += 1
      puts "après ajout (#{i}): #{x}"
      puts
    end
  end
end.each(&:join)

puts "valeur finale : #{x}"

Ce simple ajout nous suffit à éviter bien des problèmes et à assurer que notre bloc d’instruction est joué en une seule fois sans être interrompu par un autre thread.

De l’utilisation d’un code thread-safe

Il ne suffit pas d’utiliser des librairies thread-safe pour que le code qui les utilise le soit aussi. En fait, vous pouvez tout à fait écrire un code non thread-safe lorsque vous utiliser une librairie ou une classe qui l’est.

Prenons par exemple la classe suivante :

class Counter
  attr_reader :count

  def initialize
    @count = 0
    @mutex = Mutex.new

    puts 'Counter created'
  end

  def increment!
    @mutex.synchronize { @count += 1 }
  end
end

Cette classe est thread-safe puisqu’elle utilise un Mutex pour incrémenter atomiquement la valeur.

Créons maintenant une application qui l’utilise :

class Application
  def increment!
    counter.increment!
  end

  def counter
    @counter ||= Counter.new
  end

  def count
    counter.count
  end
end

app = Application.new

10.times.map do |i|
  Thread.new { app.increment! }
end.each(&:join)

puts app.count

Si on lance plusieurs fois cette application, on notera que parfois son résultat est erroné. Elle devrait toujours renvoyer 10.

Cette application n’est pas thread-safe pour une seule raison, elle initialise l’instance de counter grâce à l’opérateur ||=. Cet opérateur n’est pas atomique. Il va d’abord lire la valeur puis la modifier après coup si nécessaire. Cette valeur a donc pu changer entre temps.

Dans les cas où notre application se comporte anormalement, c’est parce que plusieurs threads ont vu le contenu de la variable counter égale à nil. Ils ont donc voulu l’initialiser.

En réalité l’un des threads s’occupait déjà de l’initialisation, l’autre thread a ré-initialiser cette variable et écrasé l’existante.

On a donc créé une application qui n’est pas thread-safe sur la base d’une classe qui l’est. Il faut donc être vigilent.

La meilleure façon d’écrire notre application aurait été la suivante :

class Application
  attr_reader :counter

  def initialize
    @counter = Counter.new
  end

  def increment!
    counter.increment!
  end

  def count
    counter.count
  end
end

L’idée est donc d’éviter que notre instance de compteur puisse être modifiée. Le plus simple est de l’initialiser au démarrage de l’application puis d’utiliser cet instance partout ailleurs.

Ça règle notre souci et ça semble plus performant puisqu’on s’évite un test d’existence inutile à chaque appel à la méthode counter.

Quelque soit le contexte dans lequel vous utilisez l’idiome ||=, essayez d’abord de de voir s’il est possible d’initialiser la variable en amont. Vous y gagnerez un comportement plus sain.

Pour aller plus loin

D’autres classes livrées avec Ruby permettent de gérer facilement des cas typiques d’utilisation des threads avec notamment les classes Queue et SizedQueue qui vous permettent de gérer des files de threads avec communication synchronisée. Les threads qui utiliseront la même file d’attente n’auront pas à se soucier des problèmes de synchronisation des données.

Il peut également être intéressant de jeter un œil à la classe ConditionVariable qui permet d’interrompre l’exécution d’un thread pour qu’il attende la disponibilité d’une autre ressource avant de continuer son traitement.

Conclusion

Nous avons donc vu grâce à ces deux épisodes les principales fonctionnalités mises à disposition par les threads, leurs utilité ainsi que les pièges à éviter.

Les threads sont un sujet difficile à appréhender au début mais qui peut vous ouvrir de nombreuses portent vers l’optimisation de portions de code ayant pour résultat des améliorations notables des performances de votre application.

23- LA PROGRAMMATION SYSTEME

Dans le monde Unix, il n’est pas rare d’exécuter des tâches depuis le terminal, que ce soit pour lancer un ensemble de commandes ou encore faire de l’administration système. Quand ces tâches deviennent répétitives, on a tendance à les encapsuler dans un script pour les automatiser.

La plupart du temps, ces script seront des scripts shell écrit en Bash par exemple. Pourtant, quand on fait du Ruby, il peut très vite devenir intéressant de l’utiliser pour avoir accès à tout notre arsenal habituel.

Lancer des programmes externes

Le besoin le plus courant pour l’utilisation d’un shell script est de vouloir faire le pont entre deux programmes existants.

Il y a plusieurs façons de lancer une commande externe en Ruby.

Équivalent de la librairie standard C

On peut tout d’abord utiliser la commande system qui va lancer un sous-shell et exécuter la commande :

system("whoami")

La sortie de cette commande sera affichée sur la sortie standard. N’importe quelle commande, même évoluée, que vous pourriez entrer directement dans le shell peut être utilisée directement avec la méthode system.

On pourrait également utiliser la méthode exec mais elle a pour effet de remplacer le processus courant par celui nouvellement créé. Notre programme perdrait donc la main :

exec("whoami")

Récupération de la sortie

Ces deux méthodes sont très pratiques pour une utilisation simple mais il sera fréquent de vouloir récupérer la sortie de la commande pour la stocker dans une variable qu’on va manipuler ensuite. Ruby nous propose l’opérateur backtick pour ça :

name = `whoami`
now = `date`

Il existe une autre notation pour faire la même chose qui évite d’avoir à se soucier des caractère spéciaux par exemple :

%x(uptime)
%x(ls -l).split("\n").size

Manipuler les processus

Maintenant qu’on sait lancer des commandes externes et récupérer leur sortie penchons nous sur la manipulons des processus.

Créer des sous-processus

Depuis un programme Ruby, il est possible de lancer un autre processus dans lequel nous exécuterons du code de manière indépendante du processus principal.

C’est possible grâce à la méthode fork directement calquée sur la fonction Unix du même nom :

fork do
 puts "Je suis un enfant"
end

Process.pid

On a donc bien deux processus différents confirmés par les PID différents.

On peut demander à notre programme principal d’attendre qu’un enfant ait fini son travail avant de reprendre la main grâce à la méthode wait, on peut même attendre la fin d’un processus donné grâce à la méthode waitpid :

pid = fork { sleep 10; exit 1 }
Process.waitpid pid

Au sein d’un processus, il est possible de connaître le pid du processus parent :

parent_pid = Process.pid

fork { p Process.ppid == parent_pid }

Tuer des processus

Maintenant que nous savons comment créer de nouveaux processus, il pourrait être intéressant de voir comment tuer des existants.

Comme pour le reste, la méthode proposée par Ruby est très similaire à la fonction unix kill. Cette méthode est Process.kill et prend deux paramètres. En premier, le signal à envoyer, le second étant le PID du processus à tuer.

On peut donc simplement faire un programme qui tue d’autres programmes sur le système en fonction de leur état, de leur consommation de ressource ou sur n’importe quelle autre base mais on peut aussi mettre en place des systèmes interne de dialogue entre processus dans notre code Ruby :

pid = fork do
   Signal.trap("SIGTERM") { puts "Parent asked me to quit!"; exit }
   loop { } # Do something
end

Process.kill("SIGTERM", pid)

On peut donc depuis un processus, envoyer un signal à un autre processus pour le faire réagir en conséquence.

Arguments et options en ligne de commande

OptParse

Si vous écrivez des scripts en Ruby, il y a de fortes chances que vous vouliez pouvoir passer des arguments et des options à votre script lors de son lancement.

C’est souvent le meilleur moyen de le rendre flexible. Fort heureusement, c’est facile à faire et d’ailleurs Ruby propose même des librairies pour simplifier le travail. Ma préférée étant OptParse, voici un exemple d’utilisation :

$ ruby options_parser.rb
$ ruby options_parser.rb -v
$ ruby options_parser.rb -d --upcase
$ ruby options_parser.rb -d --upcase file1 file2 other
$ ruby options_parser.rb -unknown

On peut donc très facilement et de façon élégante gérer les options passées à notre programme, récupérer les arguments, afficher une aide à l’utilisation ou encore gérer le cas des options inconnues.

ARGF et ARGV

Si vous le souhaitez, vous pouvez descendre à plus bas niveau grâce à deux constantes pré-remplies au moment du lancement de votre script.

Vous avez tout d’abord ARGF qui représente un pseudo fichier concaténant tout le contenu des différents fichiers passés en arguments au lancement du programme. On pourra donc écrire par exemple :

puts ARGF.readline

qui reviendrait ni plus ni moins à cloner le fonctionnement basique de la commande unix cat :

ruby argf_reader.rb file1 file2

ARGV permet quant à elle de connaître la liste des arguments qui ont été passés sous la forme d’un simple tableau:

puts "count: #{ARGV.size}"
puts "args: #{ARGV.join(", ")}"
$ ruby argv_reader.rb
$ ruby argv_reader.rb --help foo bar

Accéder aux variables d’environnement

Pour finir, on voit assez régulièrement des scripts qui tirent parti des variables d’environnement mises à leur disposition. C’est également possible d’y avoir accès depuis Ruby :

ENV.keys
ENV["RBENV_VERSION"]

Vos programmes pourraient donc récupérer de l’information extérieure de cette manière et même proposer des variables d’environnement spécifiques qui pourraient être ajustées par l’utilisateur au moment du lancement de votre programme.

Conclusion

Nous avons donc ici toutes les bases nécessaires pour nous lancer dans l’écriture de scripts d’administration. Ajouter à cela tout ce qu’on a pu voir précédemment avec la vidéo sur les entrées / sorties et la disponibilités de classes dédiées à la gestion de systèmes Unix. Je pense par exemple à la classe Etc, nous voilà prêt à automatiser nos tâches de gestion du système.

24- LA PROGRAMMATION RESEAU

Dans de nombreux projets, on souhaite pouvoir faire communiquer plusieurs programmes entre eux à travers le réseau. On peut vouloir écrire un client pour communiquer avec un service existant. On peut aussi vouloir créer un serveur qui fournira des services à des clients externes (chat, jeu, ferme de calcul, peer to peer, etc). Finalement on voudra parfois écrire le client et le serveur.

Voyons donc quelques exemples de cas concrets pour vous permettre d’appréhender ce qu’il est possible de faire.

Écrire un serveur

Un serveur passe son temps à attendre l’arrivée d’une requête pour renvoyer une réponse. Ce qu’il fait entre deux peut être divers et varié. Ça peut être un traitement très simple ou des opérations très lourdes.

Pour ce faire le serveur pourra ne répondre qu’à une réponse à la fois ce qui est le plus simple à programmer. Il pourra aussi utiliser des threads pour être capable de répondre à plusieurs clients en simultané.

Cas simple

Commençons avec quelque chose de très simple, un serveur de temps dont le but sera simplement de renvoyer l’heure courante au client.

La version simple, sans thread, et qui donc ne pourra répondre qu’à un client à la fois pourrait ressembler à ça :

require "socket"

server = TCPServer.new(4567)

loop do
  client = server.accept
  client.puts Time.now
  client.close
end

On voit donc qu’on charge la librairie socket pour pouvoir écrire facilement notre serveur. Puis on crée une instance d’un serveur TCP sur le port 4567.

On implémente finalement la logique de notre serveur qui consiste à écouter en boucle les connexions entrantes. Lorsqu’une connexion est détectée, on y envoie une chaîne qui correspond à l’heure courante sur le serveur. Pour finir, on ferme la connexion avec le client.

On peut maintenant tester notre serveur :

$ ruby time_server.rb

Un simple appel à telnet suffirait à effectuer le test :

$ telnet localhost 4567

Serveur multi-threads

C’est satisfaisant mais pour avoir un serveur réellement utilisable, il faudrait qu’il puisse pouvoir répondre à plusieurs requêtes en simultané. Modifions donc notre code :

require "socket"

server = TCPServer.new(4567)

loop do
  Thread.start(server.accept) do |client|
    client.puts Time.now
    client.close
  end
end

On a donc simplement enrobé notre logique de réponse dans un thread ce qui suffit à ne pas bloquer la boucle principale lorsqu’on répond à un client.

Une fois encore on peut tester notre serveur avec telnet :

$ ruby threaded_time_server.rb
$ telnet localhost 4567

Écrire un client

Un client pour notre serveur de temps

Pour le plaisir, on pourrait aussi écrire le client plutôt que de passer par telnet :

require "socket"

HOST = ARGV[0] || "localhost"

session = TCPSocket.new(HOST, 4567)
puts session.gets
session.close

Une fois encore on charge la librairie socket pour nous faciliter la tâche.

On définie ensuite une constante qui sera remplie avec le premier argument fourni lors de l’appel de notre client ou “localhost” si aucun argument n’est passé.

On ouvre une connexion sur le serveur, puis on lui demande de nous envoyer du contenu qu’on affiche en local par la même occasion.

Finalement, on clos la connexion.

Difficile de faire plus simple !

Un client de nombres aléatoires

On a parfois besoin de générer des nombres aléatoires. Comme vous le savez sûrement, les librairies de génération de nombres aléatoires sont souvent pseudo-aléatoire. Dans certaines situation vous voudrez pouvoir générer des nombres réellement aléatoires et non-prédictibles. Il existe des services en ligne pour faire ça et notamment random.org qui est un service qui utilise le bruit atmosphérique pour générer des nombres aléatoires.

Créons un client qui nous permet de simuler un jet de 5 dès qui pourrait servir de base pour un jeu de hasard :

require "net/http"
require "openssl"

uri = URI.parse("https://www.random.org/integers/?num=5&min=1&max=6&col=1&base=10&format=plain")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

resp = http.get(uri.request_uri)

puts resp.body if resp.is_a?(Net::HTTPSuccess)

On a donc chargé les librairies net/http pour ouvrir un connexion HTTP et openssl pour pouvoir accéder à une URL en HTTPS, ce qui est le cas du service que nous voulons utiliser.

On parse notre URL pour obtenir un objet URI plus pratique à manipuler. Dans notre URL, on voit qu’on demande 5 entiers, compris entre 1 et 6, affichés avec un résultat par ligne. Les nombres attendus devront être en base 10, rendus au format texte.

On crée ensuite un objet Net::HTTP auquel on passe en paramètre l’hôte et le port à contacter. On autorise ensuite l’utilisation de SSL.

On lance ensuite une requête en GET sur notre URI puis on vérifie si la réponse a été un succès. Si c’est le cas, on peut afficher nos résultats.

ruby random_numbers.rb

Alors biensûr, on peut faire plus simple et plus concis en Ruby grâce à la librairie open-uri mais ça aurait été moins drôle et instructif :

require "open-uri"

puts open("https://www.random.org/integers/?num=5&min=1&max=6&col=1&base=10&format=plain").read
ruby random_numbers_2.rb

Conclusion

Nous avons donc vu ici les bases des accès réseaux tout d’abord avec des sockets TCP puis ensuite un peu plus spécialisé avec net/http. Il y a encore beaucoup à dire sur le sujet, on aurait pu par exemple écrire un client pour un vrai serveur de temps en utilisant net/telnet, ou encore accéder à un serveur IMAP avec net/imap puis discuter avec un serveur SMTP pour envoyer des emails via net/smtp. On pourrait imaginer monter un pont entre un canal d’un serveur IRC et un newsgroup, les possibilités sont infinies.

Ce qu’il faudra retenir c’est que vous avez à disposition les outils bas niveau pour créer vos propres protocoles client / serveur mais que si vous souhaitez utiliser un protocole établi, alors Ruby met certainement déjà à votre disposition un lib pour vous faciliter le travail. Si ce n’est pas le cas, vous trouverez sans aucun doute un gem qui le fait.

25- LES TESTS AUTOMATISES, PARTIE 1

La mise en place de tests automatisés pour votre code peut sembler rédhibitoire si vous n’y êtes pas habitués mais s’avère vite indispensable sur des projets voués à évoluer et dépassant les quelques dizaines de ligne de code.

C’est l’assurance pour vous et les autres développeurs du projet que les modifications que vous apportez ne cassent pas le comportement d’une autre partie de l’application. Vous gagnez donc en confort et vous vous évitez des suées à chaque modification d’une portion de code que vous connaissez mal.

Les outils à disposition

Plusieurs librairies dédiées à la mise en place de tests sont disponibles pour Ruby bien que deux options majeures sortent du lot. Si vous regardez les tests d’un projet libre écrit en Ruby, ils seront sûrement écrit à l’aide de RSpec qui a longtemps largement dominé la place grâce à ses nombreuses fonctionnalités et surtout parce qu’il a été le premier à proposer une écriture façon “spec” qui rend l’écriture et la lecture des tests plus naturelle.

Toutefois, depuis quelques temps, la librairie Minitest gagne beaucoup de terrain. Elle plaît de plus en plus aux développeurs car elle est très simple à prendre en main, utilise peu de magie pour fonctionner et reste très proche du langage Ruby de base. Pour ne rien gâcher, elle est très rapide à l’exécution et est livrée de base avec l’interpréteur Ruby !

C’est donc Minitest, dans sa version Minitest::Spec que nous allons utiliser pour profiter de l’écriture des tests à la manière “spec”.

Premiers tests

Supposons que nous avons une classe Person qui peut prendre en entrée un nom et un prénom et que nous voulions mettre en place une méthode d’instance qui permettra d’avoir le nom complet :

class Person
  def initialize(first_name:, last_name:)
    @first_name, @last_name = first_name, last_name
  end

  def full_name
    [@first_name, @last_name].join(" ")
  end
end

On a donc le constructeur qui prend deux paramètres nommés. Ces valeurs sont stockées dans des variables d’instance pour être ré-utilisables.

On a ensuite une méthode qui permet de générer le nom complet en utilisant un tableau contenant le prénom et le nom. Ces éléments sont ensuite joins par un espace.

Pour s’assurer que notre classe fonctionne correctement, nous devons écrire les tests adéquats :

require "minitest/autorun"

require_relative "person"

describe Person do
  it "should return person full name" do
    Person.new(first_name: "foo", last_name: "bar").full_name.must_equal "foo bar"
  end
end

C’est un bon début. Nous chargeons minitest/autorun qui permet de lancer automatiquement les tests lorsque le fichier est exécuté.

On charge ensuite le code de notre classe Person.

C’est maintenant le moment de passer au vif du sujet, à savoir l’écriture des tests.

La première chose remarquable est l’instruction describe qui permet de créer un contexte. Chaque fois qu’on souhaite grouper de manière logique un ensemble de tests, on peut utiliser un contexte. On peut d’ailleurs imbriquer les contextes.

Ici notre contexte est tout simplement la classe Person.

Une fois dans un contexte, on peut écrire des tests grâce au mot-clé it suivi d’une chaîne qui décrit ce qu’on va tester. Ici on souhaite vérifier qu’on peut obtenir le nom complet de la personne.

On va donc instancier un objet Person puis appeler la méthode qu’on souhaite tester, ici full_name. Il ne nous reste qu’à valider que la valeur retournée correspond à ce qu’on attend. C’est ce qu’on appelle une assertion.

Ici notre assertion consiste à dire que le résultat de la méthode doit être égale à la chaîne “foo bar”.

Plutôt simple n’est-ce pas ?

Lançons nos tests pour voir si tout fonctionne comme attendu :

ruby person_spec.rb

Minitest met à notre disposition tout un tas d’assertions qui permettent de vérifier des choses diverses et variées comme l’égalité, la différente, la nullité, l’inclusion ou encore le fait qu’on objet réponde ou non à une méthode.

Toutes les assertions disponibles sont listées dans la documentation de Minitest. Il y a aussi l’aide-mémoire de Matt Sears qui est très bien fait.

L’idée est de tester tous les comportements de votre classe pour vous assurer que vous ne passez pas à côté de quelque chose et que vos tests garantirons le bon fonctionnement de votre classe lorsque vous la modifierez.

TDD

L’une des grandes tendances dans le domaine du test est d’écrire les tests avant même d’écrire le code fonctionnel. Ça peut paraître étrange au premier abord mais c’est en fait un très bon moyen de bien réfléchir ses besoins et d’architecturer correctement son code.

Bien souvent quand on écrit les tests après le code fonctionnel, on a tendance à ne tester que l’implémentation présente plutôt que de tester réellement le comportement attendus pour l’ensemble des cas qu’on souhaite gérer.

On va donc procéder de cette manière pour la suite. Disons qu’à l’initialisation de notre objet, on veut pouvoir passer un deuxième prénom facultatif. Il faudrait modifier nos tests pour signifier ce besoin :

require "minitest/autorun"

require_relative "person"

describe Person do
  describe "#full_name" do
    it "should return first name and last name when there's no middle name available" do
      Person.new(first_name: "foo", last_name: "bar").full_name.must_equal "foo bar"
    end

    it "should return first name, middle name and last name when all available" do
      Person.new(first_name: "foo", last_name: "bar", middle_name: "baz").full_name.must_equal "foo baz bar"
    end
  end
end

On a ajouter un contexte full_name pour regrouper les tests relatifs à cette méthode.

Le premier test est le même qu’avant, nous avons simplement changé son nom pour être plus explicite sur ce qui est testé.

Nous avons finalement ajouté un test pour vérifier que si le second prénom est disponible, il sera utilisé dans la chaîne générée.

L’idée est donc de lancer notre test pour voir s’il passe ou non. Il ne doit logiquement pas passer puisque nous n’avons jamais prévu ce cas.

ruby person_spec.rb

On a bien une erreur qui nous explique que le paramètre middle_name est inconnu de la méthode initialize. Ajoutons le :

def initialize(first_name:, last_name:, middle_name:)
  @first_name, @last_name, @middle_name = first_name, last_name, middle_name
end

puis relançons nos tests:

ruby person_spec.rb

Nous avons maintenant deux erreurs… L’une d’entre elle nous dit qu’on a essayé d’initialiser l’objet sans passer de middle_name. Corrigeons déjà ça puisque effectivement ce paramètre est facultatif:

def initialize(first_name:, last_name:, middle_name: nil)
  @first_name, @last_name, @middle_name = first_name, last_name, middle_name
end

Si on relance nos tests:

ruby person_spec.rb

On voit qu’il ne nous reste qu’une erreur qui nous dit que la chaîne obtenue ne correspond pas à la chaîne attendu. C’est donc à nous de jouer et de modifier notre code pour qu’il réponde au besoin :

def full_name
  [@first_name, @middle_name, @last_name].compact.join(" ")
end

Lançons à nouveau nos tests :

ruby person_spec.rb

Et tous nos tests passent ! Voilà un exemple parfait de développement basé sur les tests plus connus sous le nom de “TDD”.

Conclusion

Cette introduction rapide vous permettra de commencer à mettre en place des tests pour garantir le bon fonctionnement de votre code et éviter l’introduction de régression. Elle vous montre également comment mettre un pied à l’étrier pour utiliser le TDD qui est une pratique qui devient indispensable lorsqu’on la maîtrise.

Dans le prochain épisode nous verrons comment éviter la redondance à travers les différents tests mais aussi comment maîtriser le contexte d’exécution des tests pour pouvoir tester des cas complexes dépendant d’éléments extérieur à priori non maîtrisables.

26- LES TESTS AUTOMATISES, PARTIE 2

Maintenant que nous avons vu les bases de la mise en place de tests à l’aide de Minitest::Spec, nous allons aller un peu plus loin et découvrir les outils que Minitest met à notre disposition pour éviter la redondance et gérer des cas complexes dans lequels le contexte doit être maitrisé pour pouvoir écrire des tests robustes.

Callbacks et accesseurs paresseux

before / after

Vous aurez souvent besoin, pour plusieurs tests d’un même contexte, d’initialiser pour chaque test un objet ou un environnement identique.

Plutôt que refaire cette initialisation dans chaque test ou d’écrire une méthode privée que vous appellerez manuellement dans chaque test, Minitest met à votre disposition des callbacks qui sont before et after et qui seront respectivement appelés avant et après chaque test du contexte.

require "minitest/spec"
require "minitest/autorun"

require_relative "person"

describe Person do
  before do
    puts "Avant le test"
  end

  after do
    puts "Après le test"
  end

  it "should do something" do
    puts "dans le test"
  end

  it "should do something else" do
    puts "dans l'autre test"
  end
end

On voit donc que les blocs before et after sont bien exécutés avant et après chaque test.

C’est donc l’endroit parfait pour initialiser des données qui seront utilisés dans chaque test ou nettoyer une base de données après chaque test.

require "minitest/spec"
require "minitest/autorun"

require_relative "person"

describe Person do
  describe "#full_name" do
    before do
      @person = Person.new(first_name: "Nico", last_name: "C.")
    end

    after do
      puts "Cleaning DB"
    end

    it "should include first name" do
      @person.full_name.must_include("Nico")
    end

    it "should include last name" do
      @person.full_name.must_include("C.")
    end
  end
end

Ces tests sont bizarrement constitués je vous l’accorde mais permettent de bien mettre en évidence l’utilisation de before et after.

On a donc profité du bloc before pour initialiser une instance de la classe Person qu’on stocke dans une variable d’instance. On va donc pouvoir ré-utiliser cette variable à travers tous nos tests du contexte. On s’épargne donc l’initialisation dans chaque test.

Dans notre exemple, le bloc after fait prétendument un nettoyage de la base de donnée après chaque test.

Lazy accessors

Un autre besoin encore plus courant est de mutualiser le contenu d’une variable entre plusieurs tests d’un même contexte. C’est ce que nous avons fait avant avec le before mais en pratique on le réservera plutôt à de la configuration d’environnement.

Bien souvent, on utilisera les accesseurs paresseux pour gérer les variables à partager entre plusieurs tests. Ces accesseurs paresseux sont déclarée à l’aide de let qui attend un bloc de code qui ne sera exécuté que lors du premier appel dans un test. Le bloc n’est donc pas exécuté s’il la variable n’est pas utilisée. De plus ce bloc ne sera exécuté qu’une fois par test. C’est donc la méthode la plus efficace et performante pour gérer des variables partagées.

On pourrait donc remplacer notre code de test précédent par :

require "minitest/spec"
require "minitest/autorun"

require_relative "person"

describe Person do
  describe "#full_name" do

    after do
      puts "Cleaning DB"
    end

    let(:person) { Person.new(first_name: "Nico", last_name: "C.") }

    it "should include first name" do
      person.full_name.must_include("Nico")
    end

    it "should include last name" do
      person.full_name.must_include("C.")
    end
  end
end

On a donc supprimé le bloc before pour le remplacer par un appel à let qui définie un accesseur paresseux qui sera générée à son premier appel. Ce n’est pas une variable mais plutôt une méthode qui nous retourne le résultat d’un bloc au premier appel puis sa version cachée ensuite.

Dans nos tests, on appelle donc l’accesseur person plutôt que la variable d’instance @person.

Stubs

Lorsqu’on teste, on a aussi régulièrement besoin de pouvoir forcer certains objets à répondre de manière attendu. C’est particulièrement vrai quand l’une de vos méthodes manipule des heures et dates ou des données aléatoires.

Dans ce cas il est particulièrement utile de pouvoir demander à une méthode de toujours répondre de la même manière lorsqu’on l’appel à un certain endroit ou avec certains paramètres.

Disons que notre classe Person peut retourner l’âge de la personne en secondes, on serait tenté d’écrire le test comme suit:

describe "#age_in_seconds" do
  it "should return person age in second from now" do
    thirty_years_ago = 30 * 365 * 24 * 60 * 60
    borned_at = Time.now - thirty_years_ago

    p = Person.new(first_name: "foo", last_name: "bar", birthday: borned_at)
    p.age_in_seconds.must_equal Time.now - borned_at
  end
end

On crée une date qui correspond à un age de 30 ans, puis on utilise cette date pour définir l’âge de notre personne.

On appelle ensuite la méthode age_in_seconds et on la compare à l’heure courante moins 30 ans.

On ajoute ensuite la méthode à notre classe ainsi que l’attribut @birthday:

def age_in_seconds
  Time.now - @birthday
end

Si on lance notre test on a une erreur. Le souci ici est qu’on va avoir un décalage de quelques millisecondes entre la création de la date anniversaire et le retour de notre méthode, il s’est passé quelques millisecondes dans le programmes.

Le plus simple est donc de faire en sorte que la méthode Time.now réponde avec une valeur fixe qui nous permet de tester dans des conditions connues :

current_time = Time.now

Time.stub :now, current_time do
  # …
end

On a donc stubber la méthode now de la classe Time pour que la valeur retournée soit toujours current_time. Tous les appels à Time.now dans le bloc retourneront donc la valeur de current_time y compris celui fait par notre méthode age_in_seconds.

Il ne faut évidemment pas en abuser et ne l’utiliser que dans le cas où contrôler la valeur de retour d’une méthode est une nécessité.

MiniTest::Mock

Le dernière fonctionnalité de MiniTest que je souhaite vous présenter est MiniTest::Mock dont le but est de créer un objet factice qui sera capable de recevoir des appels de méthode et de retourner des valeurs. Il permettra également de vérifier que les méthodes attendues ont bien été appelées pendant notre test. On pourra même vérifier qu’elles ont été appelées avec les bons arguments.

C’est particulièrement utile lorsque vous souhaitez simuler les appels à un service externe que vous ne maîtrisez pas.

De manière générale, c’est une bonne pratique de “mocker” tous les appels à des services externes. Ça permet de lancer les tests sans avoir de connexion réseau ou même encore si le service externe ne répond pas. Ça permettra d’ailleurs de tester le cas où le service est indisponible. Dans tous les cas, vos tests seront plus rapides.

On pourra donc simuler des appels à un serveur IMAP, à une base de données, une API, etc.

Créons donc une classe dédiée à l’envoie de message sur des plate-formes variées, Twitter par exemple.

Nous pourrions mettre en place une classe qui utilisera une interface commune pour envoyer les messages, il suffira donc de passer à l’initialisation de notre objet un autre objet qui implémente l’interface en question.

Ici les objets passés devront au moins répondre à la méthode post pour pouvoir être utilisés. Le reste de leur interface et le fonctionnement interne nous est égale :

class SocialMedia
  def initialize(media)
    @media = media
  end

  def post(message)
    @media.post("#{message} from SocialMedia class")
  end
end

On a donc une classe SocialMedia qui lors de son instanciation attend un paramètre qui est en fait le client pour l’API visée. Cette instance du clent sera donc capable de s’identifier, poster un message, etc.

Notre classe déclare ensuite une méthode post dont le but est d’utiliser le client fourni et d’appeler sa méthode post avec un message retravaillé.

L’implémentation est fantaisiste, personne n’aurait d’intérêt à utiliser ça mais l’idée est là. On isole les responsabilités pour avoir une architecture propre.

On écrit maintenant les tests pour notre classe :

require 'minitest/autorun'

require_relative 'social_media'

describe SocialMedia do
  before do
    @twitter  = MiniTest::Mock.new
  end

  let(:social_media) { SocialMedia.new(@twitter) }
  let(:content) { "I'm social!"}

  it "should append a watermark from the class" do
    @twitter.expect :post, true, ["#{content} from SocialMedia class"]
    social_media.post(content)

    assert @twitter.verify # verifies tweet and hashtag was passed to `@twitter.update`
  end
end

Dans le bloc before nous avons créé un mock de l’objet servant à communiquer avec l’API de tweeter puisque dans nos test nous ne souhaitons pas réellement contacter l’API de twitter mais simplement simuler son comportement.

Nous déclarons ensuite deux accesseurs paresseux, l’un instanciant notre classe avec en paramètre le mock de l’API Twitter puis un autre qui contient simplement le contenu à envoyer sur le réseau social.

Ensuite on déclare notre test qui va s’assurer que notre classe ajoute bien une chaîne l’identifiant à la fin du message original.

On fait savoir à notre mock qu’il doit normalement être appelé via sa méthode post avec en paramètre la chaîne “#{content} from SocialMedia class” et que dans ce cas il retournera true.

On appelle ensuite la méthode de notre classe qui est censée déclencher l’appel à la méthode post de l’API en lui passant le contenu d’origine.

Finalement, on s’assure avec assert que notre mock a bien été appelé comme prévu.

Si le test passe, on a donc l’assurance que les objets utilisés en interne dans notre méthode on bien été appelés avec les paramètres attendus. Dans notre cas, ça revient à confirmer que notre classe appelle bien la méthode post de l’objet API fourni et lui passe en argument la chaîne modifiée.

Conclusion

Vous connaissez maintenant toutes les bases vous permettant d’écrire des tests automatisés. Vous verrez que très rapidement l’écriture de tests deviendra naturelle et que vous ferez de plus en plus de TDD sans vous en rendre compte.

Les tests sont sans aucun doute possible l’un de vos meilleurs alliés pour écrire un code de qualité et que vous pourrez faire évoluer sur le long terme sans vous tirer les cheveux.

27- UTILISER LE DEBUGGER

Quelque soit l’attention que vous portez à l’architecture et l’écriture de votre code, vous finirez un jour par être confronté à un bug. Parfois les bugs sont très simples à trouver et vous n’aurez besoin d’aucun outil pour le repérer et le corriger. Le reste du temps, il vous faudra comprendre ce qui se passe, analyser ce que vous avez en entrée et pourquoi ce que vous avez en sortie n’est pas conforme à ce qui est attendu.

Vous devrez alors suivre l’exécution du code, passer de méthodes en méthodes pour analyser ce qu’il se passe. Beaucoup de développeurs se cantonnent à l’utilisation de messages de débogage pour vérifier les valeurs des éléments clés à différents endroits. Il ne sera donc pas rare de voir un développeur mettre des puts dans son code ou de façon un peu plus élégante en utilisant un logger avec quelque chose comme logger.debug.

Quand on sait dans quelle méthode se trouve le problème et que cette méthode ne fait pas des dizaines de lignes, ce qui ne devrait d’ailleurs jamais être le cas, ces méthodes d’analyse peuvent suffir. Pour les cas plus complexes, il existe un outil bien plus adapté qu’on retrouve dans tous les langages, le debbuger.

En Ruby plusieurs outils sont disponibles pour faire du debbugage. Bien évidemment, Ruby propose de base un outil qui permet de poser un point d’arrêt dans le code et de lancer la console interactive pour analyser l’état courant puis continuer à avancer dans le code. Bien que tout à fait honorable, il existe des alternatives à cet outil qui permettent d’accéder à plus de flexibilité.

Aujourd’hui on a principalement pry et byebug. Nous allons utiliser ce dernier dans cette vidéo mais gardez à l’esprit que quelque soit l’outil utilisé, ils partagent tous les mêmes fonctionnalités de base.

Installation de byebug

byebug n’est pas livré de base avec Ruby, il faut donc l’installer. On va utiliser la commande gem :

$ gem install byebug

Placer un point d’arrêt

Pour l’utiliser, il nous suffit maintenant de charger byebug dans notre code et d’appeler la méthode byebug à l’endroit où l’on souhaite que l’exécution s’arrête.

Voici un exemple de script que j’ai préparé pour démontrer l’utilisation :

require 'byebug'

class Computer
  def initialize(a, b)
    @a = a
    @b = b
  end

  def sum_and_double
    sum = sum(@a, @b)
    double(sum)
  end

  private

  def sum(a, b)
    a + b
  end

  def double(a)
    a * a
  end
end

computer = Computer.new(2, 3)
res = computer.sum_and_double

puts res

On a écrit une classe qui prend deux paramètres. Son but est de faire la somme des deux paramètres puis de doubler ce résultat et le retourner.

Évidemment l’exemple est extrêmement simple, difficile de faire une erreur, on peut même se demander pourquoi faire une classe. C’est uniquement pour vous montrer l’utilisation du debugger dans un contexte très simple à comprendre.

Comme vous pouvez le voir, j’ai le constructeur qui stocke nos valeurs initiales. J’ai ensuite une méthode principale qui permet de faire le traitement attendu.

Cette méthode principale délégue les deux parties du traitement (l’addition et la multiplication) à des méthodes privées. Avec cette structuration découpée, on se retrouve avec une architecture assez proche d’un cas réel.

Comme vous l’aurez remarqué, ce code comporte un bug grossier. La multiplication ne double pas la valeur mais retourne son carré. Faites comme si vous ne l’aviez pas vu et que ce bug est très difficile à détecter.

Si on lance notre programme, on a bien un résultat erroné :

$ ruby buggy.rb

Ajoutons donc un point d’arrêt pour suivre le déroulé et comprendre ce qu’il se passe.

Et relançons notre script.

On se retrouve dans une console interactive qui nous présente l’instruction sur laquelle on s’est arrêté. Juste après notre instruction byebug, donc juste avant la première instruction de la méthode sum_and_double.

Comme c’est notre première session byebug, on peut commencer par afficher la liste des commandes avec la commande help puis l’aide d’une commande donnée, ici la commande next.

Une commande intéressante est la commande list qui comme son aide l’indique (taper help list) permet de lister les lignes de code autour d’une ligne donnée. Par défaut la ligne courante. Essayons:

list 3-5

On a donc les lignes 3 à 5 de notre fichier courant, on peut également afficher le contexte autour de la ligne en cours d’exécution :

list=

On peut évidemment lister les lignes qui précédent :

list-

Analyser l’état courant

Passons maintenant à l’analyse des variables de notre script. On peut simplement appeler une variable par son nom, si elle existe dans le contexte, son contenu sera affiché :

@a

mais on peut aussi afficher toutes les variables d’un type donné pour le contexte courant:

var
var instance

Nos variables d’instance sont donc bien conformes à ce qu’on attendait. On va donc demander l’exécution de l’instruction suivante et voir où ça nous mène. Les debuggers nous offrent deux possibilités pour avancer dans le code, on peut soit exécuter l’instruction et s’arrêter à la suivante ou alors exécuter en entrant dedans pour exécuter son code ligne par ligne. C’est ce que nous allons faire ici :

step

On se retrouve donc dans la définition de la méthode sum. C’est l’occasion de découvrir une autre fonctionnalité intéressante qui permet de savoir comment on est arrivé là où on se trouve :

backtrace

On sait donc qu’on se trouve dans la méthode Computer.sum qui a été appelée par Computer.sum_and_double elle-même appelée par le programme principal. C’est particulièrement pratique dans de grosses applications où les points d’entrée sont nombreux comme une application Rails par exemple.

On pourrait se déplacer dans l’un des appels parents et obtenir le contexte de cet appel parent :

frame 1

On peut donc voir l’état des variables et autres éléments comme si nous venions juste de passer dans le parent. C’est l’une des grandes force des debuggers. Vous avez une machine à voyager dans le temps qui vous permet d’analyser le comportement qu’a eu votre programme tout au long de son exécution.

Continuons notre inspection à la recherche de notre bug:

next

On est sortie de notre méthode sum, on peut vérifier le contenu de notre variable avant de continuer :

sum

Notre variable contient la valeur attendue, notre bug est donc plus loin, continuons :

next
res

La valeur obtenue en sortie de la méthode double est erronée, on peut donc supposer que l’erreur se trouve dans cette méthode. En y regardant, on comprendra que la multiplication devrait en fait être une addition.

Voici à quoi peut ressembler une chasse au bug avec un debugger. Son utilisation prend tout son sens avec des programmes plus complexes.

Encore plus

byebug a encore plus à offrir, je vous invite à décortiquer son aide, vous pouvez par exemple :

Conclusion

Le debugger, quelque soit le langage utilisé, est l’arme par excellence pour résoudre des cas complexes. Par expérience je sais que cet outil est trop sous-estimé, particulièrement par les développeurs qui n’ont jamais utilisé des langages compilés ou qui n’ont jamais dû gérer eux-mêmes l’utilisation de la mémoire dans leurs programmes.

Le prochaine fois que vous rencontrez un bug, plutôt que de mettre des puts un peu partout, essayer de placer un ou plusieurs points d’arrêts aux endroits stratégiques et lancer votre debugger. Même si ce n’est pas naturel au début, vous deviendrez vite accro à cet outil indispensable.