09. Classes

Jusqu'à présent, nous avons vu de nombreux types d'objets différents, ou classes : chaînes, entiers, flottants, tableaux et quelques objets spéciaux (true, false et nil), sur lesquels nous reviendrons plus tard. En Ruby, toutes ces classes commencent toujours par une majuscule : String, Integer (Entiers), Float (Flottants), Array (Tableaux), etc. Généralement, si nous voulons créer un nouvel objet d'une certaine classe, nous utilisons new :

a = Array.new  + [12345]  # Addition de Tableaux.
b = String.new + 'bonjour'  # Addition avec Chaînes.
c = Time.new

puts 'a = '+a.to_s
puts 'b = '+b.to_s
puts 'c = '+c.to_s

Comme nous pouvons créer des tableaux et des chaînes en utilisant [...] et '...' respectivement, nous utilisons rarement new pour cela (De toute façon, il n'est pas très clair, dans l'exemple précédent, que String.new crée une chaîne vide et que Array.new crée un tableau vide). Les nombres, cependant, sont une exception : vous ne pouvez pas créer un entier en utilisant Integer.new. Vous devez simplement taper le nombre.

La classe Time

D'accord, et la classe Time ? Les objets Time représentent des moments dans le temps. Vous pouvez ajouter (ou soustraire) des nombres à (ou de) des temps pour obtenir de nouveaux instants : ajouter 1.5 à un temps renvoie un nouvel instant une seconde et demie plus tard :

temps  = Time.new    # L'instant où vous chargez cette page.
temps2 = temps + 60  # Une minute plus tard.

puts temps
puts temps2

Vous pouvez également créer un temps pour un moment spécifique en utilisant Time.mktime :

puts Time.mktime(2000, 1, 1)          # An 2000.
puts Time.mktime(1976, 8, 3, 10, 11)  # Année de ma naissance.

Note : quand je suis né, l'Heure Avancée du Pacifique (PDT, en anglais) était en vigueur. Quand l'an 2000 est arrivé, cependant, l'Heure Normale du Pacifique (PST, en anglais) était en vigueur, au moins pour nous sur la côte Ouest. Les parenthèses servent à grouper les paramètres pour mktime. Plus vous ajoutez de paramètres, plus votre instant deviendra précis.

Vous pouvez comparer deux temps en utilisant les méthodes de comparaison (un temps antérieur est plus petit qu'un temps postérieur).

Quelques Choses à Essayer

  • Un milliard de secondes... Trouvez la seconde exacte de votre naissance (si vous le pouvez). Découvrez quand vous aurez (ou quand vous avez eu ?) un milliard de secondes. Ensuite, allez le marquer sur votre calendrier.
  • Joyeux Anniversaire ! Demandez l'année de naissance d'une personne. Ensuite, demandez le mois et, enfin, le jour. Ensuite, découvrez l'âge de cette personne et donnez-lui une FESSÉE ! pour chaque anniversaire qu'elle a eu.

La Classe Hash

Une autre classe très utile est la classe Hash. Les hashs sont très similaires aux tableaux : ils ont un tas d'emplacements qui peuvent contenir divers objets. Cependant, dans un tableau, les emplacements sont alignés en ligne, et chacun est numéroté (en commençant par zéro). Dans un Hash, cependant, les emplacements ne sont pas alignés en ligne (ils sont juste en quelque sorte tous ensemble), et vous pouvez utiliser n'importe quel objet pour faire référence à un emplacement, pas seulement un nombre. Il est bon d'utiliser des hashs lorsque vous avez un tas de choses que vous voulez stocker, mais qui ne s'intègrent pas vraiment dans une liste ordonnée. Par exemple, les couleurs que j'utilise dans diverses parties de ce tutoriel :

tableauCouleurs = []  # comme Array.new
hashCouleurs    = {}  # comme Hash.new

tableauCouleurs[0]         = 'rouge'
tableauCouleurs[1]         = 'vert'
tableauCouleurs[2]         = 'bleu'
hashCouleurs['chaines']    = 'rouge'
hashCouleurs['nombres']    = 'vert'
hashCouleurs['mots_cles']  = 'bleu'

tableauCouleurs.each do |couleur|
  puts couleur
end
hashCouleurs.each do |typeCode, couleur|
  puts typeCode + ':  ' + couleur
end

Si j'utilise un tableau, je dois me rappeler que l'emplacement 0 est pour les chaînes, l'emplacement 1 est pour les nombres, etc. Mais si j'utilise un Hash, c'est facile ! L'emplacement 'chaines' stocke la couleur des chaînes, bien sûr. Rien à retenir. Vous avez peut-être remarqué que lorsque j'ai utilisé each, les objets dans le hash ne sont pas sortis dans le même ordre que je les ai mis (Du moins pas quand j'ai écrit ça. Peut-être que maintenant ils le font... on ne sait jamais avec les hashs). Les tableaux servent à mettre les choses en ordre, les Hashs non.

Bien que les gens utilisent généralement des chaînes pour nommer les emplacements dans un hash, vous pouvez utiliser n'importe quel type d'objet, même des tableaux et d'autres hashs (bien que je ne puisse pas trouver une raison pour laquelle vous voudriez faire ça...) :

hashBizarre = Hash.new

hashBizarre[12] = 'singes'
hashBizarre[[]] = 'vide'
hashBizarre[Time.new] = 'rien de mieux que le Présent'

Les hashs et les tableaux sont bons pour différentes choses : c'est à vous de décider lequel résout le mieux votre problème, et différent pour tous les problèmes que vous aurez.

Étendre les Classes

À la fin du dernier chapitre, vous avez écrit une méthode pour retourner un nombre en toutes lettres. Cependant, ce n'était pas une méthode d'entiers : c'était une méthode générique du programme. Ne serait-ce pas plus cool si vous pouviez écrire 22.en_lettres au lieu de nombreFrancais 22 ? Regardez comment vous pouvez faire ça :

class Integer
  def en_lettres
    if self == 5
      lettres = 'cinq'
    else
      lettres = 'cinquante-huit'
    end

    lettres
  end
end

# Je préfère toujours tester par paires...
puts 5.en_lettres
puts 58.en_lettres

Eh bien, j'ai testé ; et rien n'a explosé. :)

Nous avons défini une méthode d'entier en "sautant" simplement dans la classe Integer, en définissant la méthode là-dedans et en ressortant. Maintenant, tous les entiers ont cette méthode sensationnelle (incomplète). En fait, si vous n'aimez pas la façon dont la méthode native to_s fait les choses, vous pouvez simplement la redéfinir... mais je ne recommande pas de faire ça ! Il est préférable de laisser les anciennes méthodes tranquilles et d'en faire de nouvelles quand vous avez besoin de quelque chose de nouveau.

Encore confus ? Laissez-moi revenir un peu sur ce dernier programme. Jusqu'à présent, chaque fois que nous exécutions du code ou définissions une méthode, nous le faisions dans l'objet "programme" par défaut. Dans notre dernier programme, nous avons quitté cet objet pour la première fois et sommes allés dans la classe Integer. Nous avons défini une méthode là-bas (ce qui en a fait une méthode d'entier) et tous les entiers peuvent l'utiliser. À l'intérieur de cette méthode, nous utilisons self pour faire référence à l'objet (l'entier) qui utilise la méthode.

Créer des Classes

Nous avons déjà vu un tas d'objets de différentes classes. Cependant, il est facile de créer des types d'objets que Ruby n'a pas. Heureusement, créer une nouvelle classe est tout aussi facile que d'en étendre une existante. Disons que nous voulions lancer des dés en Ruby. Regardez comment nous pouvons faire une classe appelée De :

class De

  def rouler
    1 + rand(6)
  end

end

# Faisons deux dés...
des = [De.new, De.new]

# ...et roulons chacun d'eux.
des.each do |de|
  puts de.rouler
end

(Si vous avez sauté la section sur les nombres aléatoires, rand(6) renvoie simplement un nombre aléatoire entre 0 et 5).

C'est tout ! Des objets de notre propre création. Roulez les dés quelques fois (en utilisant le bouton "Actualiser" de votre navigateur) et voyez ce qui se passe.

Nous pouvons définir toutes sortes de méthodes pour nos objets... mais il manque quelque chose. Travailler avec ces objets n'a pas beaucoup changé depuis que nous avons appris à manipuler des variables. Regardez notre dé, par exemple. Chaque fois que nous le roulons, nous obtenons un nombre différent. Mais si nous voulions sauvegarder ce nombre, nous devrions créer une variable pour pointer vers lui. Et tout dé décent devrait avoir un nombre, et rouler le dé devrait changer ce nombre. Si nous gardons le dé, nous n'avons aucun moyen de savoir quel nombre il affiche.

Cependant, si nous essayons de stocker le nombre que nous avons obtenu dans une variable (locale) à l'intérieur de rouler, la valeur sera perdue dès que rouler sera terminé. Nous avons besoin de sauvegarder ce nombre dans un type différent de variable :

Variables d'Instance

Normalement, quand nous parlons de chaînes, nous les appelons simplement des chaînes. Cependant, nous pourrions les appeler des Objets de type Chaîne. Parfois, certains programmeurs peuvent les appeler des instances de la classe String, mais c'est une façon exagérée (et très longue) de dire chaîne. Une instance d'une classe est juste un objet de cette classe.

Par conséquent, les variables d'instance sont comme des variables d'objet. Une variable locale d'une méthode reste en vie jusqu'à ce que la méthode se termine. Une variable d'instance d'un objet, par contre, restera en vie tant que l'objet sera en vie. Pour différencier les variables d'instance des variables locales, elles ont un @ devant leurs noms :

class De

  def rouler
    @nombreAffiche = 1 + rand(6)
  end

  def affiche
    @nombreAffiche
  end

end

de = De.new
de.rouler
puts de.affiche
puts de.affiche
de.rouler
puts de.affiche
puts de.affiche

Très sympa ! Maintenant rouler roule le dé et affiche nous dit quel est le nombre qui est sorti. Mais et si nous voulions voir quel nombre est sorti avant de rouler le dé (avant d'avoir défini @nombreAffiche) ?

class De

  def rouler
    @nombreAffiche = 1 + rand(6)
  end

  def affiche
    @nombreAffiche
  end

end

# Puisque je ne vais plus utiliser ce dé,
# je n'ai pas besoin de le sauvegarder dans une variable.
puts De.new.affiche

Hum... Eh bien, au moins ça n'a pas donné d'erreur. Attendez, ça n'a pas beaucoup de sens un dé "non-roulé" ou quoi que nil signifie ici. Ce serait beaucoup plus cool si nous pouvions rouler le dé dès qu'il est créé. C'est ce que fait initialize :

class De

  def initialize
    # Je vais juste rouler le dé, bien que
    # nous puissions faire n'importe quoi que
    # nous voulions, comme mettre la face '6'
    # vers le haut
    rouler
  end

  def rouler
    @nombreAffiche = 1 + rand(6)
  end

  def affiche
    @nombreAffiche
  end

end

puts De.new.affiche

Quand un objet est créé, la méthode initialize (si elle a été définie) est toujours appelée.

Notre dé est presque parfait. La seule chose qui manque est un moyen de définir quel nombre est affiché... Pourquoi n'écririez-vous pas la méthode triche qui fait ça ? Revenez quand vous aurez fini (et quand vous l'aurez testé et que ça marchera, bien sûr). Assurez-vous simplement que personne ne puisse faire afficher un 7 au dé !

C'était très cool ce que nous avons fait jusqu'à présent. Mais c'était juste un jouet, quand même. Laissez-moi vous montrer un exemple plus intéressant. Faisons un animal virtuel, un bébé dragon. Comme tous les bébés, il doit pouvoir manger, dormir et "faire ses besoins", ce qui signifie que nous allons devoir pouvoir le nourrir, le mettre au lit et l'emmener dans le jardin. En interne, notre dragon a besoin de savoir s'il a faim, s'il est fatigué ou s'il a besoin de sortir, mais nous ne pourrons pas voir cela pendant que nous interagissons avec lui, tout comme vous ne pouvez pas demander à un bébé "as-tu faim ?". Donc nous allons ajouter quelques façons sympas d'interagir avec notre bébé dragon, et quand il naîtra nous lui donnerons un nom (Tout ce que vous passez comme paramètre à la méthode new sera passé à la méthode initialize pour vous). D'accord, essayons :

class Dragon

  def initialize nom
    @nom = nom
    @endormi = false
    @nourritureEstomac  = 10 # Il est plein
    @nourritureIntestin =  0 # Il n'a pas besoin d'aller au jardin

    puts @nom + ' est né.'
  end

  def nourrir
    puts 'Vous avez nourri ' + @nom + '.'
    @nourritureEstomac = 10
    passageDuTemps
  end

  def jardin
    puts 'Vous avez emmené ' + @nom + ' au jardin.'
    @nourritureIntestin = 0
    passageDuTemps
  end

  def mettreAuLit
    puts 'Vous avez mis ' + @nom + ' au lit.'
    @endormi = true
    3.times do
      if @endormi
        passageDuTemps
      end
      if @endormi
        puts @nom + ' ronfle et remplit la chambre de fumée.'
      end
    end
    if @endormi
      @endormi = false
      puts @nom + ' se réveille.'
    end
  end

  def lancer
    puts 'Vous lancez ' + @nom + ' en l\'air.'
    puts 'Il glousse et brûle vos sourcils.'
    passageDuTemps
  end

  def bercer
    puts 'Vous bercez ' + @nom + ' doucement.'
    @endormi = true
    puts 'Il commence à somnoler...'
    passageDuTemps
    if @endormi
      @endormi = false
      puts '...mais se réveille quand vous arrêtez.'
    end
  end

  private

  # "private" signifie que les méthodes définies ici
  # sont des méthodes internes de l'objet. (Vous pouvez
  # le nourrir, mais vous ne pouvez pas lui demander si
  # il a faim.)

  def aFaim?
    # Les noms de méthodes peuvent finir par "?".
    # Normalement, nous faisons cela seulement
    # si la méthode renvoie vrai ou faux,
    # comme celle-ci :
    @nourritureEstomac <= 2
  end

  def aBesoinDeSortir?
    @nourritureIntestin >= 8
  end

  def passageDuTemps
    if @nourritureEstomac > 0
      # Déplacer la nourriture de l'estomac vers l'intestin.
      @nourritureEstomac  = @nourritureEstomac  - 1
      @nourritureIntestin = @nourritureIntestin + 1
    else  # Notre dragon est affamé !
      if @endormi
        @endormi = false
        puts 'Il se réveille !'
      end
      puts @nom + ' est affamé ! En désespoir de cause, il vous a mangé VOUS !'
      exit  # Cela quitte le programme.
    end

    if @nourritureIntestin >= 10
      @nourritureIntestin = 0
      puts 'Oups !  ' + @nom + ' a eu un accident...'
    end

    if aFaim?
      if @endormi
        @endormi = false
        puts 'Il se réveille !'
      end
      puts 'L\'estomac de ' + @nom + ' gargouille...'
    end

    if aBesoinDeSortir?
      if @endormi
        @endormi = false
        puts 'Il se réveille !'
      end
      puts @nom + ' fait la danse pour aller au jardin...'
    end
  end

end

animal = Dragon.new 'Norbert'
animal.nourrir
animal.lancer
animal.jardin
animal.mettreAuLit
animal.bercer
animal.mettreAuLit
animal.mettreAuLit
animal.mettreAuLit
animal.mettreAuLit

WOUAH ! Bien sûr, ce serait beaucoup plus cool si c'était un programme interactif, mais vous pouvez faire cette partie plus tard. J'essayais juste de vous montrer les parties liées directement à la création d'une nouvelle classe de type Dragon.

Nous avons dit un tas de nouvelles choses dans cet exemple. La première est simple : exit termine le programme où qu'il soit. La deuxième est le mot private, que nous avons mis juste au milieu de notre classe. J'aurais pu le laisser de côté, mais je voulais juste renforcer l'idée que certaines méthodes vous pouviez faire avec un dragon, tandis que d'autres se produisaient avec le dragon. Vous pouvez penser à cela comme "des choses sous le capot" : à moins que vous ne soyez un mécanicien automobile, tout ce que vous avez besoin de savoir sur les voitures, c'est l'accélérateur, le frein et la direction. Un programmeur appelle cela l'interface publique.

Maintenant, pour un exemple plus concret dans cette ligne de raisonnement, parlons un peu de comment vous représenteriez une voiture dans un jeu (ce qui est mon domaine de travail). D'abord, vous devez décider à quoi ressemblera votre interface publique ; en d'autres termes, quelles méthodes les gens peuvent-ils appeler sur vos objets de type voiture ? Eh bien, ils doivent pouvoir accélérer et freiner, mais ils doivent aussi pouvoir définir la force qu'ils appliquent sur la pédale (Il y a une grande différence entre effleurer l'accélérateur et écraser le pied). Ils auront aussi besoin de conduire, et encore une fois, dire quelle force ils appliquent sur la direction. Je pense que vous pouvez aller encore plus loin et ajouter un embrayage, des clignotants, un lance-roquettes, un incinérateur arrière, un convecteur temporel, etc... cela dépend du type de jeu que vous faites.

Les objets internes à une voiture, cependant, sont plus complexes : d'autres choses dont une voiture a besoin sont la vitesse, la direction et la position (en restant basique). Ces attributs seront modifiés en appuyant sur la pédale d'accélérateur ou de frein et en tournant le volant, bien sûr, mais l'utilisateur ne devrait pas pouvoir modifier ces informations directement (ce qui serait une distorsion). Vous voudrez peut-être vérifier le dérapage ou les dommages, la résistance de l'air et ainsi de suite. Tout cela ne concerne que la voiture. Tout cela est interne à la voiture.

Quelques Choses à Essayer

  • Faites une classe de Oranger. Elle doit avoir une méthode hauteur qui renvoie sa hauteur, une méthode appelée passer_un_an qui, lorsqu'elle est appelée, fait compléter une année de plus à l'arbre. Chaque année, l'arbre grandit (peu importe combien grand vous pensez qu'un oranger peut grandir en un an), et après quelques années (encore une fois, vous décidez) l'arbre doit mourir. Les premières années, il ne doit pas produire de fruits, mais après un certain temps il doit, et je pense que les arbres plus vieux produisent beaucoup plus de fruits qu'un plus jeune avec le passage des années... ou ce que vous trouvez le plus logique. Et, bien sûr, vous devez pouvoir compter_les_oranges (le nombre d'oranges sur l'arbre), et prendre_une_orange (qui réduira le @nombre_d_oranges de un et renverra une chaîne disant combien l'orange était délicieuse, ou alors dira qu'il n'y a plus d'oranges cette année). Rappelez-vous que les oranges que vous ne prenez pas cette année doivent tomber avant l'année prochaine.
  • Écrivez un programme pour que vous puissiez interagir avec votre bébé dragon. Vous devez être capable d'entrer des commandes comme nourrir et jardin, et ces méthodes doivent être appelées sur votre dragon. Logiquement, comme toute l'entrée se fera par des chaînes, vous devez avoir un moyen de répartir les méthodes, où votre programme doit valider la chaîne tapée et appeler la méthode appropriée.

Et c'est tout ! Mais attendez un peu... Je ne vous ai rien dit sur les classes pour faire des choses comme envoyer un e-mail, ou enregistrer et charger des fichiers de votre ordinateur, ou comment créer des fenêtres et des boutons, ou des mondes en 3D ou quoi que ce soit ! Eh bien, il y a juste trop de classes que vous pouvez utiliser, et cela rend impossible que je vous les montre toutes ; même moi je ne les connais pas toutes. Ce que je peux vous dire, c'est où en trouver plus à leur sujet, afin que vous puissiez en apprendre plus sur celles que vous voulez utiliser. Mais avant de vous renvoyer, il y a une autre fonctionnalité de Ruby que vous devriez connaître, quelque chose que la plupart des autres langages n'ont pas, mais dont je ne peux tout simplement pas me passer : les blocs et les procs.