Javaquarium

4 ans et 3 mois après, il est toujours là, et toujours aussi difficile !

a marqué ce sujet comme résolu.

En fait le plus casse-pied c'est pas tant l'héritage, le stratégies (régime alimentaire, reproduction) ou quoi, c'est d'arriver à trouver un moyen élégant d'éviter les concurrent modification exceptions ^^

On a envie de juste parcourir une liste d'être-vivants et de leur dire "vivez votre vie". Dans le même temps, on aimerait bien que quand ils font qqch en rapport avec l'aquarium, ça mette à jour l'aquarium en conséquence (typiquement qu'il se nettoie des cadavres tout seul, que les petits enfants s'ajoutent instantanément).

Ça oblige à "cloner" les listes de poissons/plantes au début pour travailler sur la liste telle qu'elle était lorsque le tour a démarré.

J'trouve ça un peu moche

EDIT : mais en même temps ça a du sens… On fait évoluer les poissons qu'on avait au départ. Mais bon c'est juste en termes de code que fishes.each {} c'est plus joli que fishes.findAll{}.each{}

EDIT² : Autre truc extrêmement casse-pied, c'est l'aspect aléatoire qui ne permet pas d'écrire de petits tests unitaires. Du coup on est obligé de mettre la partie tirage aléatoire à part pour pouvoir écrire des tests déterministes (style : un mérou ne peut pas manger une sole).

+1 -0

J'ai démarré hier puis trouvé un peu de temps pour avoir un truc à peu près présentable ce midi.

https://github.com/aesteve/groovyquarium

Les trucs à noter :

Traits

Le projet utilise les traits pour tout ce qui est "comportement" (méthode de reproduction, régime alimentaire).

Ni plus ni moins qu'un design pattern strategy, simplement un trait peut hériter d'un autre trait. Ce qui permet de décrire des comportement (comme dans cet exemple) de façon assez simple, puis de composer une classe à partir de plusieurs traits.

Du coup, contrairement à pas mal d'exemple en Java où on va stocker le type de poisson en tant qu'énumération par exemple, on peut directement écrire ça.

On aurait aimé avoir l'union de types comme ci-dessus (ou en Ceylon) ça serait encore plus élégant, mais c'est déjà un peu mieux que la PoissonFactory qui contient les règles de "si c'est une carpe alors son régime alimentaire c'est végétérien"

Des trucs bien pratiques

Méta-programmation

Un truc qu'on écrit plusieurs fois dans cet exercice : list.get(rand.nextInt(list.size())). ce qui est pas très joli à se trimbaler dans le code, d'où cette idée.

On ajoute une méthode getRandomMember sur toutes les listes, histoire de pas avoir à le réécrire à chaque fois.

Surcharge d'opérateurs

void add(Plant p) { this.plants.add(p) }. Finalement c'est quoi ? Un ajout ? Bon bah on n'a qu'à écrire aquarium + p ou aquarium += p si ça vous plaît mieux. Pour cela, il suffire de redéfinir l'opérateur plus

Même chose pour ++ (méthode next)

Des conventions utiles

A chaque fois qu'on tire au sort, on va éviter de tirer un poisson mort, au cas où le ménage ait mal été fait dans notre aquarium (tant qu'à faire on fera le ménage à la fin du tour d'ailleurs, même si c'est pas glop de laisser des cadavres au fond du bassin, c'est plus pratique que d'aller chercher son épuisette à chaque fois).

Du coup, on va demander la liste des poissons/plantes vivants. Invoquer une méthode à chaque fois, c'est un peu lourd à écrire, alors qu'au final, ça ressemble à un attribut de l'aquarium (on pourrait d'ailleurs mettre cette liste en cache par exemple).

Du coup, il suffit d'écrire ça pour que, par convention du langage, aquarium.livingPlants fasse appel à cette méthode (si il ne trouve pas la propriété, il appelle ce getter).

Comme tous les getter instrumentés, c'est à utiliser avec prudence quand même… Ici c'était essentiellement pour présenter cette convention.

EDIT : au départ j'avais profité d'avoir ce seul point d'accès au tirage aléatoire d'un poisson pour rajouter :

1
2
3
4
Fish randomFish(Fish self) {
  def list = livingFishes.findAll { !it.is(self) } as List
  list.randomMember
}

Parce que ça me semblait logique mais ça ne respectait pas l'énoncé.

Le "tour" d'aquarium est écrit en quasiment une ligne

https://github.com/aesteve/groovyquarium/blob/master/src/main/groovy/com/github/aesteve/groovyquarium/Aquarium.groovy#L65

L'opérateur plus des listes nous permet de passer outre la fameuse ConcurrentModificationException, puisqu'il renvoie une nouvelle liste. Et on fait un "next" sur chacun des être vivants de l'aquarium. Easy.

Des tests unitaires

Non pas qu'il y en ait vraiment besoin dans ce projet, mais ça m'a permis de vérifier point par point que je respectais l'énoncé (même si j'ai du zapper quelques trucs).

Le petite démo est elle-même un (faux) test unitaire. Plus rapide qu'écrire un main et tout le tralala.

Voilà, c'est à peu près tout. Il manque sans doute plein de trucs (notamment les I/O mais c'est pas franchement ce qui fait le charme de l'exercice) et y'a sans doute moyen de modéliser ça encore mieux, j'ai pas réfléchi des masses.

+4 -0

Petit déterrage oklm. J'pense écrire le Valaquarium dans la semaine, Vala et son système de signaux me semble être un bon langage pour cet exercice, puis j'en ai marre d'écrire du code de merde pas propre, ça sera une bonne occasion de me fixer des objectifs de qualité.

+1 l'auteur!

+2 -0

Il y a une erreur d'orthographe dans l'énoncé

Nos poissons sont des goinfres : à chaque tour, tous les poissons mangent, et ils mangent tout ce qu'ils ont commencé. En clair, ça veut dire que ce qui et mangé disparaît purement et simplement.

SpaceFox

ce qui ait :ninja: est mangé disparaît :)

+1 -0

Lu'!

Tes entités sont copiables, ce n'est pas normal : sémantique d'entité, formes à adopter. La conséquence est que l'opérateur != devient trivial : l'objet est à une adresse différente.

Renvoyer une valeur "const" n'a pas de sens. En effet, elle n'a pas d'effet sur l'invariant de l'objet qui la renverrait éventuellement ni sur le contexte global. Et elle est de toute façon copiable.

Le setter de points de vie de l'EtreVivant est probablement une mauvaise idée. C'est lui qui doit gérer le business en interne. Il reçoit des dégâts, ou se soigne, mais l'accès violent est une rupture sur l'encapsulation. (Why setters and getters are evil ?).

Dans la même veine, à propos de la portée protected, à propos des fonctions membres virtuelle publiques, voir ce post : https://openclassrooms.com/forum/sujet/public-private-ou-protected?page=1#message-90447802 .

Quand une valeur est nécessairement positive pour avoir du sens. Pas de raison pour utiliser "int", prend plutôt "unsigned". Encore plus pour les indices de tableaux qui ne sont pas de type "int" mais "std::size_t" (ou encore conteneur<T>::size_type).

Pour les boucles d'effacement, voir du côté de std::remove_if. Dans le cas cité, encore mieux : conteneur<T>::clear.

Salut,

Je reviens ici pour poser une question concernant la classe Poisson. Grâce à tes conseils Ksass`Peuk j'ai compris que ma classe Poisson devait avoir une sémantique d'entité, que chaque objet poisson doit représenter un poisson unique. Ca n'a donc aucun sens de pour copier un poisson ou appliquer les opérateurs de comparaison sur ceux-ci (vu qu'ils doivent être tous unique).

J'ai donc recoder ma classe comme ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Poisson
{
private:
    std::string m_nom;
    bool m_sexe;

public:
    Poisson() = default;
    Poisson(const std::string &nom = "", bool sexe = true);
    Poisson(const Poisson &) = delete;

    Poisson &operator=(const Poisson &) = delete;
    bool operator==(const Poisson &) = delete;
    bool operator!=(const Poisson &) = delete;

    std::string nom() const;
    bool sexe() const;
};

Je pense que c'est bon, j'ai interdit au compilateur de générer un constructeur par copie et interdit aussi les opérations de comparaison.

Dans ma classe Aquarium j'avais un vecteur comme ceci :

1
std::vector<Poisson> m_poissons;

Et évidement quand je veux ajouter une poisson de la sorte :

1
2
3
4
void Aquarium::ajouterPoisson(const std::string &nom, bool sexe)
{
    m_poissons.push_back(Poisson(nom, sexe));
}

ca ne fonctionne pas car je tente de copier mon poisson dans mon vecteur.

Du coup je pense que la meilleur façon est d'utiliser un vecteur de pointeur, mais la j'hésite sur le type de pointeur à utiliser. Shared_ptr ou unique_ptr ? J'ai encore un peu de mal a comprendre la différence et à priori j'utiliserai plus des shared_ptr mais je ne saurai pas vraiment le justifier.

Quelqu'un pourrait il m'éclairer sur ce qu'il faut choisir, et surtout si c'est une bonne façon de faire ?

Merci !

Je pense que c'est bon, j'ai interdit au compilateur de générer un constructeur par copie et interdit aussi les opérations de comparaison.

CitronHades

Interdire les opérateurs de comparaison n'est pas nécessaire puisqu'il ne sont de toute façon pas générés automatiquement. En revanche, avoir un constructeur par défaut n'est probablement pas une bonne idée, puisque c'est un poisson qui aurait juste un état de base par défaut, sans grand intérêt donc. Surtout qu'en le constructeur principal a ses arguments en optionnels ce qui produit déjà un constructeur par défaut en fait (mettre les arguments optionnels n'est probablement pas cohérent conceptuellement d'ailleurs).

Et évidement quand je veux ajouter une poisson de la sorte :

1
2
3
4
void Aquarium::ajouterPoisson(const std::string &nom, bool sexe)
{
    m_poissons.push_back(Poisson(nom, sexe));
}

ca ne fonctionne pas car je tente de copier mon poisson dans mon vecteur.

CitronHades

Ici, une première possibilité est de demander à ton compilateur de générer le constructeur et l'affectation par move-semantic :

1
2
3
4
5
class Poisson{
public:
  Poisson(Poisson &&) = default;
  Poisson& operator=(Poisson&&) = default;
};

Cela permettra au compilateur de "déplacer" le Poisson au sein du vector.

Une possibilité qui permettra d'étendre ton Aquarium avec du sous-typage sur les Poisson est effectivement de passer par un vector de Poisson. shared_ptr est plutôt l'exception que la règle d'abord parce qu'il engendre un surcoût qui n'est pas négligeable et ensuite parce que l'on peut généralement connaître bien plus précisément qui peut accéder et maintenir la vie d'une ressource à un instant t.

Donc ici, on choisira plutôt unique_ptr. Parce que si l'aquarium peut donner un accès à un poisson de l'extérieur, ce sera soit par l'envoi d'une référence (qui doit du coup vivre un temps très limité), soit en relâchant complètement la responsabilité de la ressource.

Me revoila !

Merci pour tes explications Ksass Peuk. J'ai finalement utilisé des std::unique_ptr pour gérer mes poissons et utilisé des références sur ces pointeurs quand j'en ai eu besoin.

J'ai refait un dépôt propre pour mon projet avec 1 commit par étape. On peut donc suivre l'avancement dans le projet.

Voici le lien : Javaquarium 2.0

J'aimerai bien que vous me donniez votre avis sur le code biensûr mais aussi sur ma conception. Savoir si il y'avait une meilleur façon (ou plus adéquat) de faire le projet.

En fait j'ai décider de ne pas faire de classe Carnivore/Herbivore, j'ai stocké cette information directement dans ma classe Poisson. Je ne sais pas si c'est optimal.

De plus je n'ai pas de lien fort entre le type de poisson (carnivore ou herbivore) et son espèce (mérou, poisson-clown, thon, etc). Et ça me gêne un peu car on peut tout à fait déclarer un poisson d'une espèce E comme herbivore et un autre poisson de la même espèce E mais carnivore !

J'ai donc la aussi stocké l'info de l'espèce dans ma classe Poisson car je me voyais mal faire des classes du type :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Classe de base des poissons
class Poisson
{

};

// Classe de base des carnivore héritant de poisson
class Carnivore : public Poisson
{

};

// Classe de base des herbivore héritant de poisson
class Herbivore : public Poisson
{

};

// Exemple de classe de poisson qui dérive du bon type 
class Merou : public Carnivore
{

};

Cette structure me semble lourde … Créer une nouvelle classe pour chaque espèce de poisson est chiant je trouve. Mais c'est peut être la meilleure façon ! Je vous demande votre point de vue sur ça :)

J'ai essayé de respecter toutes tes remarques Ksass`Peuk concernant la sémantiques des classes, les getters/setters et tout ce qui concerne la visibilité des classes (public, protected, private).

Merci pour vos retours !

+0 -0

En fouillant dans mes repos Github j'ai retrouvé l'implémentation du scalaquarium que j'avais commencée. Du coup j'ai essayé de la terminer dans le train.

Hyper intéressé par toute critique constructive franchement parce que j'ai l'impression d'écrire n'importe quoi, surtout un truc frustrant avec Scala pour moi (mais ça serait vrai avec n'importe quel langage que je ne connais pas bien) c'est que j'écris des trucs qui me semblent fonctionner sans avoir la certitude que c'est élégant. Ca me paraît potable, mais j'ai systématiquement l'impression que y'a un meilleur moyen d'écrire le truc. Simplement parce que je connais mal les langages fonctionnels, que même en essayant de penser différemment, l'OO revient à la charge.

Du coup mon implémentation est un savant mélange d'OO et de pattern matching. Je suis par exemple certain que les Mérous et autres devraient pouvoir s'exprimer en tant qu'alias de types (plutôt qu'une succession de with ..., ça me paraît "logique" mais j'arrive pas vraiment à l'écrire.

Une implém super sympa à faire pour la suite : Elm. Y'a sûrement moyen de faire des trucs super rigolos en jouant avec leur API canvas pour représenter l'aquarium et je pense vraiment que le langage permet de résoudre l'exercice de façon hyper élégante.

+0 -0

Bonjour tout le monde :)

J'ai recommencé le Javaquarium, cette fois ci en Java. J'ai essayé de créer le dépôt directement depuis git, c'est la première fois que je fais ça donc c'était expérimentale mais j'ai bien un dépôt sur mon compte github, donc c'est que ça a du marcher: https://github.com/Nekyia/Javaquarium-2.git

Je n'ai pas encore fait la partie reproduction, mais j'ai un petit bug:

1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(Unknown Source)
    at java.util.LinkedList$ListItr.next(Unknown Source)
    at Aquarium.tick(Simulation.java:191)
    at Simulation.main(Simulation.java:10)

Je n'ai pas encore vu ça (avant dernier chapitre de TIJ4 que je suis entrain d'étudier) donc un petit coup de pouce serait le bienvenue. J'attends aussi vos critiques sur mon code (mon bidouillage devrais-je dire). Notamment j'ai créé une classe interne DietBehavior pour distinguer les Herbivore des Carnivores et je fais appel à DietBehavior.eat() pour faire manger les poissons. Je ne sais pas ce que ça vaut comme solution. Pour rajouter une race, j'ai juste à rajouter la race dans le switch en lui donnant l'objet Diet adéquat. Je pensais faire la même chose pour la reproduction.

Comme d'habitude, merci d'avance !

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