Définir cette méthode dans la classe mère ou les filles

Cette question s'applique à tous les langages, pas qu'au Ruby !

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous !

Je continu ma grande aventure à la quête du Ruby. J'ai donc une question concernant la POO en général. J'ai actuellement une classe Fish qui ressemble à ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env ruby
# encoding: utf-8

class Fish
  attr_reader :life, :sex

  @@nb_fish = 0 # Number of fish since the simulation start

  def initialize
    @@nb_fish += 1

    @life = 10
    @sex = choose_sex
  end

  # Accessors
  def nb_fish
    @@nb_fish
  end

  # Methods
  def choose_sex # this one is private
    if Random.rand(1..2) == 1
      'm'
    else
      'f'
    end
  end

  def alive?
    @life > 0
  end

  private :choose_sex
end

Elle est on ne peut plus basique, je le sais. J'aimerais définir une méthode eat (car oui, un poisson ça mange) dont les classes filles (herbivore et carnivore) hériterais afin de la spécialiser (l'herbivore mange les algues et le carnivore mange les herbivores). D'où ma question :
Faut-il que je définisse cette méthode dans la classe Fish ou dans ses filles ?

J'imagine que l'idéologie de la POO voudrait que je la définisse dans la classe Fish mais si je le fais ça me fait 3 fonctions dont une inutile (celle de la classe Fish qui ne sera jamais instanciée) et si je ne le fais pas, ça ne colle pas aux principes de la POO car un poisson sait manger donc théoriquement je devrait définir cette méthode dans la classe Fish…

Bref, je suis un peu perdu, j'ai du pour et du contre. J'attends donc vos avis sur la question ! :)
Merci de votre aide et bonne journée !

+0 -0

Le fait de pouvoir manger, surtout de manière aussi généraliste (aussi bien herbivore que carnivore), est une caractéristique qui ne s'applique pas nécessairement qu'aux poissons. Du coup, dans une optique de permettre l'augmentation des possibilités du code, la solution idéale serait à mon avis de ne la définir ni dans la mère ni dans les filles, mais comme un objet externe : un trait, une classe abstraite ou un mixin, selon les besoins précis et les possibilités de ton langage.

+2 -0

En Ruby, je serais tenté de dire qu'on s'en fiche un peu de savoir ou la méthode eat est déclarée. Puisque tu ne peux pas forcer les classes filles à implémenter les méthodes de la classe mère. A la rigueur, tu peux faire ça :

1
2
3
4
5
class Fish
  def eat
    raise NotImplementedError
  end
end

Mais ça ne te forcera pas à spécialiser tes classes filles, ça plantera juste si la méthode n'est pas redéfinie.

Du coup, je partirais plus dans une méthode eat identique à chaque poisson. Celle ci récupérerais la liste des truc mangeable par le poisson concerné.

1
2
3
4
5
6
7
8
class Fish
  def eat(eatable_things)
    # tu boucles sur tous les poissons/algues
    if (eatable_things.include?(poisson.type))
      # On mange le poisson (ou l'algue)
    end
  end
end

ça te permet d'avoir une unique fonction qui marche pour tous les cas

  • Si tu pars du principe que le fait de manger est une action générique et commune à tous les poissons, alors définit la méthode dans la classe Mère, toutes les classes Filles y auront accès (ok c'est la base).
  • Si l'action de manger est spécifique à chaque catégorie de poisson, alors crée une méthode abstraite eat() pour la Mère et redéfinit la méthode pour chaque classe Fille.
  • Si l'action de manger est commune à un ensemble de poisson MAIS que des classes Filles peuvent avoir des spécificités, crée une méthode par défaut dans la classe Mère et redéfinit uniquement les cas spécifiques pour les classes Filles. Pour éviter la répétition de code dans le cas où certaines classes Filles mangeraient de la même manière spécifique, sans entrer dans les détails des fonctionnalités spécifiques des langages, tu peux utiliser la composition.
+0 -0

Le pattern stratégie est inutile ici, puisque si un poisson est carnivore, il le restera toutes sa vie, et donc son action de manger ne changera jamais.

Dans cet exercice, le pattern stratégie est intéressant lorsque que le poisson doit choisir son action : manger, se reproduire … Il choisira une action en fonction de ses points de vie.

Plutôt que créer des classes CarnivoreFish et HerbivoreFish, pourquoi ne pas juste avoir un attribut carnivore et écrire ta méthode eat ainsi :

1
2
3
4
5
6
7
8
9
class Fish
  def eat(food)
    if @carnivore
      # mange la barbaque
    else
      # mange l'algue
    end
  end
end

Sachant que Ruby a un typage dynamique, peu importe le type de food, si tu donne de la viande à un carnivore ou une algue à un herbivore, ça tournera correctement (à toi de sécuriser derrière). Puis, pour le coup, tu pourras t'amuser à créer un type Food avec ses méthodes bien à lui et le spécialiser par la suite (même si deux classes séparées avec une interface commune serait aussi bien grâce au Duck Typing).

Après, un idiome très Rubyesque mais qui s'applique à toute la pensée objet en général, c'est de préférer la composition (rajouter des attributs typiquement, inclure un module - Ruby only …) à l'héritage ; car même si l'héritage, d'un point de vue théorique ou même intuitif, c'est très attirant, ça résout un peu tous les problèmes magiquement, bah en pratique c'est souvent assez dégueulasse.

+0 -0

Je ne pense pas que ce soit une bonne pratique de faire ça avec des if / else (voir switch le jour où t'auras de nouvelles catégories) ;)

Gugelhupf

C'est pire encore de créer toute une hiérarchie de classes avec des fonctions "abstraites" (qui n'existent pas en Ruby, donc des fonctions vides et inutiles). Puis, comme je l'ai dit, mieux vaut partir sur de la composition que de l'héritage.

Puis dans le cas présent, c'est la bonne solution au contraire. Le régime alimentaire du poisson peut clairement être pris comme un attribut de l'objet et donc permettre de réagir différemment face à la nourriture (via des branchements conditionnels). Ce serait clairement alourdir le code que créer des classes inutilement.

Je sais bien mais par principe il faut se dire qu'un code est évolutif et qu'on peut avoir beaucoup de if/elseif/else si on place tout dans une méthode, on risque de se retrouver avec une très grosse classe. Si tu places tes classes de comportement dans un package, tu ne risques pas de t'y perdre.

+0 -0

Dans le cas présent, on est censé avoir un faible nombre de cas qui en plus, sont explicitement donnés dans l'énoncé. Du coup, pas besoin de se prendre la tête avec un système de packages pour résoudre le problème.

Par contre, pour un projet plus gros, oui clairement, je ne m'y prendrais pas comme ça non plus. Et heureusement. Dans le cas où il y aurait pléthore de régimes alimentaires, l'idée d'utiliser un pattern stratégie ne me parait pas idiote, en effet. On pourrait avoir un truc de ce goût :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Fish
  def on_eating(&block)
    @eating_policy = block
  end

  def eat(food)
    @eating_policy.call food
  end

  def Fish.new_carnivorous
    fish = Fish.new # avec les paramètres qui vont bien
    fish.on_eating { |food| food.consume if food.is_meat } # par exemple
    fish
  end
end

# Du coup, tu peux créer un poisson carnivore directement grâce à la classe
# On a une interface ressemblant à une hiérarchie sans avoir besoin d'héritage
piranha = Fish.new_carnivorous
piranha.eat :Gugelhupf # :p

Bah du coup j'ai suivit tout vos conseils. La méthode eat est dans la classe Fish, qui possède un attribut @diet qui prend les valeurs :carnivorous ou :herbivorous et un autre qui attribut @species qui prend l'espèce du poisson en valeur.
Voilà à quoi ressemble mon initialize :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def initialize(name, age, species, gender)
    @@nb_fish += 1

    @life = 10
    @name = name
    @age = age
    @species = species

    if @species == :carpe || @species == :thon
      @gender = gender
    elsif @species == :bar || @species == :merou
      @age >= 10 ? @gender = :female : @gender = :male
    else
      @gender = :male
    end

    if @species == :merou || @species == :thon || @species == :clown
      @diet = :carnivorous
    else
      @diet = :herbivorous
    end
end

Je ne suis pas sûr que ce soit comme ça que l'on utilise les symboles, mais j'avoue avoir toujours du mal à bien les comprendre (niveau utilité, niveau interne ça j'ai bien compris) ! Merci de votre aide, ça m'a bien aidé ! :)

PS : J'attend encore le jour où un langage permettra de faire ça :

1
2
3
4
5
if @species == (:merou || :thon || :clown)
   @diet = :carnivorous
else
   @diet = :herbivorous
end
+0 -0

Pour tes tests sur @species tu peux écrire le code suivant, qui sera plus propre :

1
2
3
4
5
6
# litéralement : est-ce que la liste donnée inclut @species ?
@diet = if [:merou, :thon, :clown].include? @species
  :carnivorous
else
  :herbivorous
end

Cela t'évite de répéter @species == trois fois de suite. Et comme le if...else...end est une expression comme une autre, tu peux assigner son résultat à ta variable "extérieurement".

PS : J'attend encore le jour où un langage permettra de faire ça :

1
2
3
4
5
if @species == (:merou || :thon || :clown)
   @diet = :carnivorous
else
   @diet = :herbivorous
end

Wizix

Python permet quelque chose de similaire, plus court :

1
if species in ('merou', 'thon', 'clown'):

En ruby, tu as effectivement la solution du Array#include?, mais plus verbeux.

Sinon, mon avis personnel est que tes conditions lors de l'initialisation sont difficiles à relire et à maintenir.

"plus verbeux", "je préfère partir plutôt que d'entendre ça plutôt que d'être sourd"… Ce sont deux constructions similaires, on gagne juste 4 caractères à tout casser en Python … C'est pas ce que j'appelle de la verbosité.

GaaH

Je peux aussi ajouter « moins intuitif » si tu le souhaites. En Python, il s'agit d'une construction du langage, un opérateur in permettant de savoir si un élément appartient à un itérable (le débutant ne se rend même pas forcément compte qu'il construit un tuple). Alors qu'en Ruby, on a la construction explicite d'un array et l'appel d'une méthode de cet objet, de plus, l'ordre de lecture est inversé.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte