Licence CC BY-NC-ND

Les classes

Dans ce chapitre, nous allons compléter nos objets en les rendant moins statiques et nous allons découvrir un moyen de les créer plus facilement.

Attributs

Nos objets sont quand même bien triste pour le moment. Nous ne pouvons même pas changer le contenu d’un message sans redéfinir la méthode content. C’est parce que pour le moment, nous avons laissé de côté un aspect important des objets : un objet a des propriétés, des attributs. Si les méthodes servent à manipuler l’objet, ses propriétés le caractérisent. Par exemple, si nous reprenons l’exemple du chapitre précédent, l’auteur, le destinataire, et même le contenu d’un message ne servent pas à manipuler le message mais sont des attributs du message.

Ces propriétés sont représentées sous la forme de ce que l’on appelle des variables d’instances (nous verrons pourquoi elles portent ce nom dans la section suivante). Une variable d’instance est simple à déclarer, son nom est précédé du symbole @. Par exemple, on pourrait modifier notre objet message ainsi.

m = Object.new
def m.change_content(new_content)
  @content = new_content
end

def m.to_s
  @content
end

On peut alors modifier le contenu d’un message bien plus facilement.

m.change_content('Contenu du message.')
puts m
m.change_content('Nouveau contenu.')
puts m

Les attributs nous permettent donc d’avoir des objets moins statiques.

Accesseurs et mutateurs

Dans des langages comme Java, on écrit des méthodes pour obtenir la valeur d’un attribut et pour la modifier. Ces méthodes ont des noms suivant le modèle get_attribute et set_attribute. Pour notre message par exemple, plutôt que change_content, on écrirait set_content.

def m.set_content(content)
  @content = content
end

def m.get_content
  @content
end

# Et la même chose pour `author`, etc.

Les méthodes que nous venons de définir (pour obtenir un attribut ou donner une valeur à un attribut) sont ce que l’on appelle des accesseurs et des mutateurs. Par convention, on n’écrit pas l’accesseur get_content, mais plutôt content pour pouvoir écrire puts m.content. De même, on n’écrit pas le mutateur set_content, on fait en sorte de pouvoir écrire m.content = other_content.

Comment on fait pour écrire ce mutateur. La méthode content est déjà associée à l’accesseur, non ?

En fait, lorsqu’on écrit m.content = other_content, c’est la méthode content= de l’objet m qui est appelée avec le paramètre other_content. Il nous faut donc écrire la méthode content=.

def m.content
  @content
end

def m.content=(content)
  @content = content
end

# Et la même chose pour `author`, etc.
Conventions pour les méthodes

Nous allons maintenant nous pencher sur la conception des méthodes et sur quelques conventions de Ruby en la question.

Opérateurs

Nous avons déjà vu que nous pouvions définir les opérateurs de comparaison. Tous les autres opérateurs sont aussi des méthodes que l’on peut définir au besoin (et bien sûr a + b est équivalent à a.+(b)). Bien sûr, nous pouvons définir la méthode pour faire ce que l’on veut (on peut parfaitement définir la méthode + pour qu’elle affiche un message), mais nous allons rester à peu près cohérent et garder les opérateurs pour les opérations qu’elles semblent représenter. L’opérateur + sert alors à additionner pour des nombres, mais à concaténer pour les tableaux ou les chaînes de caractères.

S’il n’y en a pas besoin, nous n’allons pas définir ces méthodes (ainsi, les hachages n’ont pas de méthode +), mais quand nous en définissons un, nous appelons son paramètre other comme dans le chapitre précédent.

def message.==(other)
 message.content == other.content
end

Tout le long du tutoriel, nous verrons d’autres opérateurs, mais nous en connaissons déjà plusieurs. Listons-les ici.

Pour commencer, nous avons les opérateurs arithmétiques classiques (+, -, * et /) accompagné de % et **. Quand nous définissons par exemple l’opérateur + de l’objet a, nous pouvons écrire a += b, c’est la méthode a qui sera appelée. Attention, il ne faut pas oublier que le résultat de l’opération est ensuite affecté à a. Cela peut mener à des problèmes.

a = Object.new

def a.+(other)
  other
end

a += 3
# Maintenant, `a` vaut 3.

Puis, pour comparer, nous avons les opérateurs de comparaison que nous avons vu dans le chapitre précédent (==, ===) sans oublier <, >, <= et >=.

Et puis, il y a des opérateurs que nous utilisons pour manipuler les tableaux et les chaînes de caractères. Oui, [] et << sont bien des opérateurs. Si nous en avons besoin pour un objet, il ne faut pas hésiter à les définir. Le cas de [] est un peu spécial dans le sens où il sert à accéder à un élément, mais aussi à modifier un élément.

Ce problème se règle de la même manière que celui posé par les accesseurs et mutateurs, nous définissons la méthode [] et la méthode []=, []= étant appelé quand on écrit a[0] = . Le nombre entre crochets correspond au paramètre qui est passé à la méthode.

a = Object.new

def a.[](i)
  print "On a écrit « a[#{i}] »."
end

a[3]

Remarquons qu’ici, le paramètre n’a pas été nommé other. [] et << sont deux exceptions à cette règle car leur sémantique est différente de celle des autres opérateurs.

Méthodes retournant un booléen

Au niveau des méthodes renvoyant un booléen, la convention est très simple, elle se finit par un point d’interrogation. De plus, il est d’usage de ne pas utiliser de verbes inutiles à la compréhension. Par exemple, pour vérifier qu’un message est vide, plutôt que de définir une méthode is_empty, nous allons définir la méthode empty?.

def m.empty?
  @contenu == ''
end

Un modèle de construction

Pour le moment, créer des messages est assez rébarbatif. Nous devons à chaque fois créer les mêmes méthodes pour chaque objet créé. Ce serait bien de les avoir dès le départ, tout comme nous avons par exemple une méthode each dès que nous créons un tableau. Pour ce faire, nous allons créer une classe. Nous en avons déjà parlé dans le chapitre précédent en disant qu’elles étaient des modèles sur lesquelles les objets étaient construits.

Création de classe

Nous pouvons alors créer une classe Message et les objets de cette classe auraient déjà les méthodes nécessaires dès leur création.

La syntaxe pour créer une classe est aussi simple que le reste du langage. Créons notre classe Message.

class Message
end

On utilise le mot-clé class suivi du nom de la classe. Le mot-clé end sert à indiquer la fin de notre classe.

Les noms des classes sont par convention écrit en « Camel_Case » avec la première lettre en majuscule.

Ça y est, nous avons créé notre première classe. Elle est vide, mais nous pouvons déjà créer des objets avec eux. Cette fois, nous n’allons pas utiliser la méthode new de Object, mais la méthode new de Message pour indiquer que l’on veut créer un nouveau message.

m = Message.new
Un peu de vocabulaire

En Ruby, chaque objet a une classe (avec Object.new, on créait un objet de classe Object). Lorsque nous créons un objet d’une certaine classe, on dit que l’on crée une instance de la classe ou que l’on instancie la classe. L’objet créé est une instance de la classe, et l’opération de création s’appelle une instanciation.

C’est de cette opération que vient l’expression « variable d’instances » rencontrée plus haut, une variable d’instance étant une variable d’une instance d’une classe.

En fait, les classes peuvent être vu comme des modèles, mais aussi comme des types. Chaque objet a une classe, cette classe représente son type. Ainsi, le type d’un message est Message. Nous avons bien entendu la possibilité de savoir de quelle classe est un objet grâce à la méthode class.

m = Message.new
m.class # => Message

Nous pouvons aussi regarder la classe d’objets usuels.

10.class  # => Integer
[].class  # => Array
''.class  # => String
{}.class  # => Hash

Ceci renforce l’idée que les classes sont comme des types. Un tableau est de la classe Array, une chaîne de caractères de classe String, etc.

Des méthodes

Nous avons dit que les classes nous permettaient de ne pas réécrire les méthodes pour chaque objet. Pour cela, nous déclarons tout simplement la méthode à l’intérieur de la classe. Notre classe Message est donc la suivante.

class Message
  def content=(content)
    @content = content
  end

  def to_s
    @content
  end

  # And the other methods.
end

Et on peut maintenant utiliser nos objets comme on le souhaite.

m1 = Message.new
m2 = Message.new
m1.content = 'Voici un message.'
print m1
m1 == m2
m2.content = 'Voici un message.'
m1 == m2

Les méthodes que nous déclarons comme cela sont appelés méthodes d’instances. Chaque instance de la classe dispose de ces méthodes. Au contraire, lorsque nous définissons une méthode pour un objet seul, comme nous l’avons fait dans le chapitre précédent, on parle de méthode singleton.

Rajouter des méthodes à la volée

Tout comme nous pouvons rajouter des méthodes à un objet, nous pouvons rajouter des méthodes à une classe. Imaginons par exemple que nous avons déjà notre classe Message et que pour les messages qui suivent, on veut rajouter une méthode erase qui supprime le contenu. Il nous suffit d’écrire ça.

class Message
  def erase
    @content = ''
  end
end

En faisant ça, nous n’avons pas défini une nouvelle classe, nous avons modifié la classe en lui ajoutant la méthode supprimer. Lorsque nous faisons cette opération, nous ouvrons la classe. En Ruby, les classes sont ouvertes. Lorsque nous ouvrons une classe, les objets déclarés avant cette classe ne sont pas modifiés et ne disposent pas des méthodes créées lors de l’ouverture (normal, ils n’ont pas été créés avec la classe modifiée).

L’ouverture de classe n’est pas très utilisée sur les classes créées par le développeur, mais elle peut être très utile sur les classes pré-définies. Par exemple, nous pourrions définir une méthode sur la classe String pour mélanger une chaîne de caractères.

Méthodes pour les accesseurs et mutateurs

Ruby nous fournit des méthodes pour faciliter la définition d’accesseurs et de mutateurs. Elles prennent en paramètre des symboles (ou des chaînes de caractères), autant que l’on veut, et créent l’accesseur ou le mutateur associé à ce symbole. attr_writer (pour attribute writer) crée le mutateur, attr_reader (pour attribute reader) crée l’accesseur et àttr_accessor (pour attribute accessor) crée les deux. Notre classe Message va alors se réécrire ainsi.

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

  def to_s
    @contenu
  end

  # And the other methods.
end

Ici, tout a été placé en « lecture seule  », l’idée étant qu’une fois qu’un message a été écrit et envoyé, sa date, son destinataire, son contenu et son auteur sont fixés. Ils ne peuvent pas être modifiés. Bien sûr, ce n’est qu’une vision personnelle et on pourrait très bien imaginer une classe où tous ces attributs, ou une partie d’entre eux, sont modifiables.

Initialisation d’objets

Nous avons maintenant des objets plus personnalisables et nous savons le manipuler. Mais nous ne savons pas créer l’objet comme nous le voulons. Nous ne pouvons même pas donner de valeurs aux attributs dès sa création. Ceci pose de plus un gros problème, nous ne pouvons pas donner de valeurs aux attributs en lecture seule. Si nous créons un message avec la dernière classe Message que nous avons créé nous ne pouvons pas modifier ses attributs.

m1 = Message.new
print m1.content
m1.content = 'Nouveau contenu.' # Erreur content= n’est pas défini

Il nous faut initialiser notre objet. Cela se fait avec la méthode initialize. Elle est appelée automatiquement à la création de l’objet.

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

  def initialize
    @content = 'Contenu'
    @date = '19/19/1919'
    @recipient = 'Destinataire'
    @author = 'Auteur' 
  end
end

Testons la nouvelle classe.

m = Message.new
print "Écrit par #{m.auteur} pour #{m.destinataire} le #{m.date}."

La méthode initialize est une méthode qu’on appelle constructeur de la classe, parce qu’elle permet de construire l’objet tout simplement. Nous avons écrit un constructeur sans paramètre, mais il est tout à fait possible d’en créer un avec paramètres (et il est bien là l’intérêt).

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

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

m1 = Message.new('Un message', 'Moi', 'Le monde')
m2 = Message.new('Un autre message', 'Moi', 'Quelqu’un', '24/04/2024')

Du nouveau sur les classes ?

Travailler avec une classe

Nous pouvons créer des instances de classe, mais nous pouvons aussi travailler directement avec la classe.

Constantes de classe

Pour commencer, nous pouvons déclarer des constantes dans les classes, la portée de ces variables étant la classe. Cela permet de définir une constante qui sera utilisée dans le code. Nous pouvons par exemple ajouter une constante LIVREUR à notre classe Message. C’est Zeste de Savoir qui livre les messages.

class Message
  LIVREUR = 'ZdS'

  def livreur
    LIVREUR
  end
  
  # etc.
end

m = Message.new('Un message', 'Moi', 'Le monde')
m.livreur # => 'Zds'

Et, en fait, on peut avoir accès aux constantes de classe en dehors de la classe grâce à l’opérateur ::.

print "Le livreur est #{Message::LIVREUR}."
Variables de classes

Les constantes c’est bien, mais nous pourrions peut-être vouloir avoir des variables. Par exemple, pour ne pas mélanger les messages, nous allons leur ajouter un identifiant. Pour avoir des identifiants différents, nous allons utiliser une variable dans notre classe. Elle sera initialisée à 0 et incrémentée à chaque nouveau message. Il nous suffira alors de donner cette valeur à l’identifiant dans la méthode intialize.

Malheureusement, utiliser une variable normale ne fonctionnera pas, nous obtiendrons une erreur disant que la variable n’est pas définie. Pour faire ce travail, il nous faut utiliser une variable de classe. Si les variables d’instances ont leurs noms précédés du symbole @, les variables de classes ont leurs noms précédés du symbole @@.

class Message
  @@messages_number = 0

  attr_reader :author, :date, :recipient, :content, :id

  def initialize(content, author, recipient, date = '21/12/2112')
    @content = content
    @date = date
    @recipient = recipient
    @author = author
    @id = (@@messages_number += 1)
  end
  
  # etc.
end

m1 = Message.new('Contenu', 'Auteur', 'Destinataire')
Message.new
m2 = Message.new('Contenu', 'Auteur', 'Destinataire')
puts "m1 est le message #{m1.id}."  # => id = 1
puts "m2 est le message #{m2.id}."  # => id = 3

Les variables de classes sont à éviter car elles peuvent s’avérer dangereuses. Nous verrons pourquoi dans les chapitres suivants, mais il nous faut savoir que c’est en partie parce qu’elles peuvent être modifiées là où on ne s’y attend pas.

Nous allons donc les éviter dans la plupart des cas. Ici, son usage est simple et ne pose pas de problèmes

Méthodes de classes

Finalement, nous allons parler des méthodes de classes. Si les méthodes d’instance sont les méthodes que les instances possèdent, les méthodes de classe sont les méthodes de la classe (comme new). Pour déclarer une méthode de classe, nous allons utiliser le mot-clé self. Par exemple, définissons une méthode pour avoir accès au nombre de messages écrits.

class Message
  @@messages_number = 0

  attr_reader :author, :date, :recipient, :content, :id

  def self.messages_number
    @@messages_number
  end
  
  # etc.
end

Message.messages_number
m = Message.new
m.nb_messages  # => Erreur

La méthode de classe n’est définie que pour la classe et pas pour ses instances. De même, lorsque nous déclarons une méthode d’instance, elle ne peut pas être utilisée par la classe.

Une classe est aussi un objet, donc on peut déclarer une méthode de classe singleton pour une classe.

def Message.messages_number
  @@messages_number
end 
Visibilité

Tout comme nous avons vu que les variables avaient une portée, les méthodes ont ce qu’on appelle une visibilité. On ne les « voit » pas partout c’est-à-dire qu’on ne peut pas les utiliser partout. En Ruby, il y a trois types de visibilité, représenté par les méthodes public, private et protected.

L’utilisation d’une de ces méthodes sans argument change la visibilité de toutes les méthodes déclarées à partir de là (donc jusqu’à ce qu’on utilise une autre méthode qui change cela). L’utiliser dans la classe change donc la visibilité de toutes les méthodes déclarées à partir de là.

class Example
  private
  # Les méthodes sont maintenant privées.

  public
  # Les méthodes sont maintenant protégées.
end

On peut également utiliser ces méthodes après avoir écrit def nom. En agissant comme cela, nous agissons seulement sur la visibilité de la méthode nom.

class Example
  def example_method private
  end
end

Cette syntaxe n’est pas trop utilisée (et nous ne l’utiliserons pas), mais elle peut être croisée à l’occasion donc il faut l’avoir déjà vue.

Et finalement, il est possible d’utiliser ces méthodes de la même manière que attr_reader et compagnie, en leur donnant en argument les méthodes que l’on veut déclarer sous la forme de symboles ou de chaînes de caractères.

class Example
  def example_method
  end

  private :example_method
end

Maintenant, nous savons changer la visibilité des méthodes de trois manières différentes… Mais nous ne savons toujours pas à quoi correspondent ces visibilités. Il faudrait quand même le savoir.

Les méthodes publiques

Une méthode est dite publique si on peut l’utiliser partout. Par défaut, les méthodes que nous déclarons dans une classe sont publiques.

Les méthodes privées

Une méthode est dite privée si on peut l’utiliser uniquement dans les autres méthodes de l’objet. Cela permet d’empêcher d’accéder à cette méthode depuis l’extérieur. Par exemple, rendons privée la méthode erase de nos messages et rajoutons une méthode delete_content qui prend en paramètre une chaîne de caractère et efface le contenu du message uniquement si cette chaîne de caractère correspond à l’auteur.

class Message
  # Méthodes précédentes
  
  def delete_content(name)
    name == author ? erase : puts 'Ce n’est pas le bon auteur.'
  end
  
  private

  def erase
    @content = ''
  end
end

m = Message.new('Contenu', 'Auteur', 'Destinataire')
m.delete_content('Auteur')  => OK.
m.erase                  => Erreur.

Notre méthode delete_content peut appeler erase, mais on ne peut appeler erase en dehors de la classe. De plus, quand une méthode est privée, on ne peut l’appeler que pour l’objet courant. Ainsi, on ne peut pas appeler la méthode erase d’un message m1 depuis un autre message m2. On peut par exemple imaginer une méthode fantaisiste clean qui prend en paramètre un autre message et supprime son contenu s’ils sont de même auteur.

class Message
  # Méthodes précédentes

  def clean(other)
    other.erase if other.author == @author 
  end

  private

  def erase
    @content = ''
  end
end

m1 = Message.new('Contenu', 'Auteur', 'Destinataire')
m2 = Message.new('Autre contenu', 'Auteur', 'Destinataire')
m2.erase(m1)  # => Erreur.
Les méthodes protégées

Les méthodes protégées sont un peu moins strictes que les méthodes privées et permettent d’écrire la méthode erase. Si une méthode est protégée, n’importe quel objet de la classe peut appeler cette méthode. En faisant de erase une méthode protégée, on peut écrire ce code qui ne fonctionnait pas quand elle était privée.

m1 = Message.new('Contenu', 'Auteur', 'Destinataire')
m2 = Message.new('Autre contenu', 'Auteur', 'Destinataire')
m2.erase(m1)  # C'est OK.

Pour ne pas mélanger les différentes choses définies dans les classes, nous allons adopter la structure suivante. Les constantes, puis les accesseurs et mutateurs, puis les méthodes de classe publiques, puis la méthode initialize, puis les autres méthodes d’instances publiques et les méthodes privées et protégées viennent à la fin.

class ClassExample
  CONSTANT = 20

  attr_reader :variable

  def self.class_method
  end

  def initialize
  end

  def instance_method
  end

  protected

  def protected_method
  end

  private

  def private_method
  end
end
Interface

Parlons un peu de conception maintenant. Nous allons appeler « interface » l’ensemble des méthodes publiques d’un objet, c’est-à-dire la face que cet objet nous présente pour le manipuler. Nous pourrions d’abord penser à mettre toutes nos méthodes publiques et à créer au moins des accesseurs pour toutes nos variables d’instances. Pourtant c’est une mauvaise idée.

Ce que nous allons faire au contraire, c’est limiter les méthodes publiques et les accesseurs au strict minimum, donc à ce que l’utilisateur de la classe a besoin de savoir. Imaginons par exemple une classe Chat. Un chat sait faire le beau (comment ça, ce sont les chiens qui font le beau), dormir, et manger. Ça nous fait autant de méthodes.

Pour savoir s’il faut lui donner à manger, ou s’il doit dormir, nous allons lui attribuer des attributs estomac et énergie, des entiers de 0 à 10. Quand ces entiers sont inférieurs à 4, le chat mange ou dort. On a donc le code suivant.

class Cat
  attr_reader :, :energy, :name

  def initialize(nom)
    @estomac = 7
    @energy = 7
  end

  def eat
    puts 'Le chat mange.'
    @estomac += 6
    @energy -= 2
  end

  def sleep
    puts 'Le chat dort.'
    @energy += 6
    @estomac -= 2
  end

  def act
    puts 'Le chat fait le beau.'
  end
end

c = Cat.new('Sylvestre')
c.eat if c.estomac < 4

Le problème : estomac et energy font partie du fonctionnement interne de Chat, on ne devrait pas savoir combien d’énergie il a. De plus, imaginons que nous changeons de technique, nous décidons que le chat a faim s’il n’a pas mangé depuis un certain nombre d’heures. Il nous faudrait changer tous les endroits où nous utilisons estomac ; c’est ballot. Ici, ça va, on ne l’a utilisé qu’une fois, mais dans un vrai code, ça ferait mal.

La solution : définir une méthode hungry? et une méthode tired? et supprimer les accesseurs estomac et energy. L’utilisateur de la classe n’a pas à connaître ces informations.

class Cat 
  attre_reader :name

  def hungry?
    @estomac < 4
  end

  def tired?
    @energy < 4
  end

  # etc.
end

c = Cat.new('Tom')
c.sleep if c.tired?
c.eat if c.hungry?

Nous pouvons remarquer qu’il manque par exemple une méthode asleep? pour ne pas le faire manger pendant qu’il dort ce qui serait absurde (quoique, Garfield le fait bien, lui), mais c’est une autre histoire.

Remarquons que cela nous permet également de respecter la Loi de Démeter (l’histoire du gars qui donnait son pantalon au vendeur pour payer sa canette). Ici, les utilisateurs de la classe Cat ont juste besoin de connaître son état (s’il est fatigué ou affamé), mais ils n’ont pas du tout besoin de connaître son estomac !

Exercices

Comme exercice, nous allons créer une classe Duration pour représenter une durée. Nous pouvons additionner des durées, les multiplier ou les diviser par des nombres (par exemple, cinq fois une durée de 2 heures). Il nous faut bien sûr une manière de les afficher et de les comparer. Dans notre implémentation, nous pourrons choisir d’autoriser ou non les durées négatives. Il nous faudra alors faire attention à comment nous gérons les multiplications et divisions par un nombre négatif et la soustraction de durée.

Correction.

Ici, on a des méthodes pour :

  • additionner, soustraire, comparer des durées ;
  • multiplier et diviser des durées par des nombres ;
  • convertir une durée en secondes, minutes, heures ;
  • convertir une durée en chaîne de caractères ou en tableau.

On gère les durées négatives et pour continuer à illustrer le concept d’interfaces, on ne gère la durée qu’avec les secondes en attribut, mais on a accès à des méthodes pour connaître le nombre d’heures, le nombre de minutes et le nombre de secondes (et même le constructeur prend en paramètre ces trois choses. L’utilisateur de la classe, lui n’a pas besoin de savoir qu’en fait seul un nombre de secondes est stockée dans la classe. Ce qui l’intéresse, ce sont les services auxquels il a accès.

# Un petit exemple de classe.
class Duration
  def initialize(hours, minutes, seconds)
    @seconds = (seconds + minutes * 60 + hours * 3600).to_i
  end

  def +(other)
    Duration.new(0, 0, other.seconds + @seconds)
  end

  def -(other)
    Duration.new(0, 0, @seconds - other.seconds)
  end

  def -@
    Duration.new(0, 0, -@seconds)
  end

  def *(other)
    Duration.new(0, 0, @seconds * other)
  end

  def /(other)
    Duration.new(0, 0, @seconds / other)
  end

  def ==(other)
    @seconds == other.seconds
  end

  def >(other)
    @seconds > other.seconds
  end

  def >=(other)
    self > other || self == other
  end

  def <(other)
    self < other
  end

  def <=(other)
    self <= other
  end

  def positive?
    @seconds > 0
  end

  def h
    @seconds / 3600
  end

  def m
    (@seconds % 3600) / 60
  end

  def s
    (@seconds % 60)
  end

  def to_s
    "#{h}:#{m}:#{s}"
  end

  def to_a
    [h, m, s]
  end

  def to_second
    @seconds
  end

  def to_minute
    @seconds.to_f / 60
  end

  def to_hour
    @seconds.to_f / 3600
  end

  protected

  attr_reader :seconds
end

a = Duration.new(0, 0, 3686)
b = Duration.new(0, 0, 59)
c = 3
puts a
puts a + b
puts a - b
puts a * c
puts a / c

Bien sûr, ce n’est pas la seule implémentation possible et on pourrait penser à d’autres méthodes pour notre classe Duration.


C’est la fin d’un chapitre encore une fois long, et avec beaucoup de théories. Qu’avons-nous appris ?

  • Nos objets ont des attributs qu’on manipule sous la forme de variables d’instances.
  • Les méthodes pour les opérateurs et les conversions doivent être cohérentes.
  • Les classes permettent de créer des objets avec des méthodes prédéfinies. Ces objets peuvent être initialisés plus facilement.
  • L’interface de nos objets ne doit donner accès qu’au nécessaire. Pour cela, il ne faut définir que les accesseurs et les mutateurs nécessaires et donner aux méthodes la visibilité qui leur convient.

Ruby est souvent comparé à Python, mais sur le point de la visibilité, ils sont complètement différents. Là où en Python on suit la philosophie « nous sommes entre adultes consentants », en Ruby on cache ce qu’il y a à cacher.