Ceci est une ancienne révision du document !
Le sujet de cette série d'exercice est disponible sur Javaquarium - Zeste de Savoir
Voici également quelques réalisations en C++ :
Pour mes implémentations, j'utilise une approche Entity-Component-System, le but étant d'étudier un peu cette approche sur un exemple concrès simple.
Les codes sont testés directectement sur Coliru.com.
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, poisson }; using type_component = std::pair<entity::id, type>; } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } void add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); } void add_poisson(component::name n, component::is_male m) { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::poisson)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << count << " algues"; } void print_poissons() { if (internal::_details.empty()) { std::cout << "No poissons"; return; } std::cout << internal::_details.size() << " poissons: "; auto print = [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }; std::transform(begin(internal::_details), end(internal::_details) - 1, std::ostream_iterator<std::string>(std::cout, ", "), print); std::cout << print(internal::_details.back()); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << '.' << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_poisson("toto", true); ecs::add_algue(); ecs::add_poisson("titi", false); ecs::add_poisson("tata", true); ecs::add_algue(); ecs::print(); return 0; }
Affiche :
4 algues. 3 poissons: toto [M], titi [F], tata [M].
Cette implémentation peut paraître surprenante : je ne crées aucune classe. Surtout que cet exercice est à la base un exercice de programmation orientée objet. Mais le but est justement d'expliquer l'intérêt de la programmation orientée objet.
Quel est le problème avec cette approche ?
Une première critique que l'on peut faire est l’utilisation de variables globales (dans internal
). Cela implique que si on veut avoir plusieurs ECS en même temps, ce n'est pas possible. Et bien sur, le code est moins réutilisable, il est écrit spécifiquement autour de ces variables globales, les fonctions ne sont pas réutilisables sans elles.
Il est facile de corriger ce problème, en passant ces variables en paramètres de fonctions et en déclarant les variables globales dans main
.
entity::id create_entity(entity::entities & entites) { const auto id = entities.empty() ? 1 : entities.back() + 1; entities.push_back(id); return id; }
Le code devient un peu plus lourd dans les fonctions qui utilisent plusieurs variables globales, mais le principe est le même.
Au final, c'est l'approche que l'on utilise en C pour faire de l'objet : on crées des structures contenant les données et des fonctions qui prennent un pointeur sur un objet. (J'utilise std::pair
et std::tuple
, qui sont en pratique des classes dont les variables membres n'ont pas de noms, et des références, mais ça serait la même chose si j'utilisais struct
et des pointeurs).
Quel est l'intérêt de l'approche objet du C++ par rapport à celle utilisée en C ?
Une première réponse serait de dire que l'on est obligé de donner des noms différents à des fonctions qui font la même chose, comme par exemple print
, print_algues
et print_poissons
dans l'exemple. En mettant les fonctions comme membre des classes plutôt que des fonctions libres, on pourrait utiliser le même nom.
algues.print(); poissons.print(); print();
Mais en fait, ce n'est pas un argument valide. En C++, il est possible de surcharger des fonctions, contrairement au C. Il est donc possible de donner le même nom à plusieurs fonctions, le compilateur appellera la fonction correcte en utilisant les paramètres.
print(algues); print(poissons); print();
Une seconde est de dire qu'il n'est pas possible en C de regrouper (“encapsuler”) ensemble les données et les fonctions qui manipulent ces données ensemble, pour avoir un tout cohérent et plus lisible.
class Poissons { public: entity::id add(); void print(); private: std::vector<Poisson> poissons; };
L'argument n'est pas faux, mais est limité. Pourquoi par exemple ne pas remplacer la création d'une classe dans le code précédent par un namespace ? Quelle sera la différence ?
Il y a bien sûr l'accessibilité des membres, publique ou privée. Mais cela sera vite limité aussi : dans un ECS (et dans de nombreux cas), les données sont manipulées par plusieurs classes, ce qui implique de mettre les membres en publique ou d'ajouter des mutateurs (getters et setters)… ce qui est fait en pratique dans de nombreux langages de programmation.
Mais il y a plus à attendre d'une encapsulation. Qu'est-ce que c'est ? Quel problème veut-on corriger avec la programmation orientée objets ?
La réponse est simple : on veut que les données soient utilisées correctement.
Par exemple, dans un ECS, chaque entité doit être unique. Il est nécessaire et indispensable de ne pas avoir de doublons dans la liste des entités. Or, dans l’implémentation proposée, rien n'interdit à l’utilisateur d'écrire le code suivant :
int main() { ecs::internal::_entities.push_back(0); ecs::internal::_entities.push_back(0); }
Ce qui violerait la règle de l'entité unique.
Les règles imposées aux données sont appelées les invariants en programmation par contrat. Les invariants sont importants, puisqu'ils signifient : “si les invariants sont respectés, le programme fera ce qu'on attend de lui. Si les invariants ne sont pas respectés, il n'est pas possible de garantir le comportement du programme”.
……. suite à rédiger
(L'autre aspect de la programmationpar contract est l'utilisation des pré et post-conditoions. Ces conditions s'appliquent sur les fonctoins et garantissent que si les pré-conditions et l'invarient sont respectés lors de l'appel d'une fonction, alors les post conditions et les invariants seront aussi respectés à la fn de l'appel de la fonction.)
classes comme service et pas comme données
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using type_component = std::pair<entity::id, type>; bool is_algue(type t) { return (t == type::algue); } bool is_poisson(type t) { return !is_algue(t); } bool is_carnivore(type t) { return (t == type::Mérou || t == type::Thon || t == type::PoissonClown); } bool is_herbivore(type t) { return (t == type::Sole || t == type::Bar || t == type::Carpe); } std::string to_string(type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); return types[i]; } } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, t)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto p){ return (p.first == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto p){ return (p.first == id); }); internal::_types.erase(types_it, internal::_types.end()); } void eat(entity::id eater, entity::id target) { assert(eater != target); const auto eater_it = std::find_if(begin(internal::_types), end(internal::_types), [eater](auto p){ return (p.first == eater); }); const auto target_it = std::find_if(begin(internal::_types), end(internal::_types), [target](auto p){ return (p.first == target); }); assert(is_poisson(eater_it->second)); assert(is_carnivore(eater_it->second) || (is_herbivore(eater_it->second) && is_algue(target_it->second))); remove_entity(target); //eat } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << "algues: " << count; } void print_poissons() { std::cout << "poissons: "; std::transform(begin(internal::_details), end(internal::_details), std::ostream_iterator<std::string>(std::cout, ", "), [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); const auto algue = ecs::add_algue(); const auto carnivore = ecs::add_poisson(ecs::component::type::Mérou, "toto", true); const auto poisson = ecs::add_poisson(ecs::component::type::Mérou, "titi", false); const auto herbivore = ecs::add_poisson(ecs::component::type::Mérou, "tata", true); ecs::add_poisson(ecs::component::type::Mérou, "tuto", false); ecs::add_poisson(ecs::component::type::Mérou, "tyty", true); ecs::print(); ecs::remove_entity(poisson); ecs::print(); ecs::eat(herbivore, algue); ecs::eat(carnivore, herbivore); ecs::print(); return 0; }
affiche :
algues: 8. poissons: toto [M], titi [F], tata [M], tuto [F], tyty [M], algues: 8. poissons: toto [M], tata [M], tuto [F], tyty [M], algues: 7. poissons: toto [M], tuto [F], tyty [M],
typage fort
enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; void add_poisson(component::name n, component::is_male m, component::type t) { assert(is_poisson(t)); ... } enum class type { algue, poisson }; enum class race { Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; void add_poisson(component::name n, component::is_male m, component::race r) { // assert(is_poisson(t)); pas necessaire ... }
idem:
void eat(entity::id eater, entity::id target) { const auto eater_it = std::find(begin(internal::_types), end(internal::_types), [eater](auto p){ return (p.first == eater); }); const auto target_it = std::find(begin(internal::_types), end(internal::_types), [target](auto p){ return (p.first == target); }); assert(is_poisson(eater_it->second)); assert(is_carnivore(eater_it->second) || (is_herbivore(eater_it->second) && is_poisson(target_it->second))); remove_entity(target); //eat }
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using type_component = std::pair<entity::id, type>; bool is_algue(type t) { return (t == type::algue); } bool is_poisson(type t) { return !is_algue(t); } bool is_carnivore(type t) { return (t == type::Mérou || t == type::Thon || t == type::PoissonClown); } bool is_herbivore(type t) { return (t == type::Sole || t == type::Bar || t == type::Carpe); } std::string to_string(type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); assert(true); return types[i]; } } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, component::type::algue)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_pair(id, t)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto p){ return (p.first == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto p){ return (p.first == id); }); internal::_types.erase(types_it, internal::_types.end()); } void eat(entity::id eater, entity::id target) { assert(eater != target); const auto eater_it = std::find_if(begin(internal::_types), end(internal::_types), [eater](auto p){ return (p.first == eater); }); const auto target_it = std::find_if(begin(internal::_types), end(internal::_types), [target](auto p){ return (p.first == target); }); assert(is_poisson(eater_it->second)); assert(is_carnivore(eater_it->second) || (is_herbivore(eater_it->second) && is_algue(target_it->second))); remove_entity(target); //eat } entity::id random_entity() { std::uniform_int_distribution<size_t> distribution(0, internal::_entities.size() - 1); const auto index = distribution(internal::_random_engine); return internal::_entities[index]; } entity::id random_poisson() { std::uniform_int_distribution<size_t> distribution(0, internal::_details.size() - 1); const auto index = distribution(internal::_random_engine); return internal::_details[index].first; } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (p.second == component::type::algue); }); std::cout << "algues: " << count; } void print_poissons() { std::cout << "poissons: "; std::transform(begin(internal::_details), end(internal::_details), std::ostream_iterator<std::string>(std::cout, ", "), [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_poisson(ecs::component::type::Mérou, "toto", true); ecs::add_poisson(ecs::component::type::Mérou, "titi", false); ecs::add_poisson(ecs::component::type::Mérou, "tata", true); ecs::add_poisson(ecs::component::type::Mérou, "tuto", false); ecs::add_poisson(ecs::component::type::Mérou, "tyty", true); ecs::print(); for (size_t i {}; i < 50; ++i) { std::uniform_int_distribution<size_t> distribution(0, ecs::internal::_types.size() - 1); auto index = distribution(ecs::internal::_random_engine); const auto eater = ecs::internal::_types[index]; index = distribution(ecs::internal::_random_engine); const auto target = ecs::internal::_types[index]; if (eater.first == target.first) continue; if (is_algue(eater.second)) continue; if (is_herbivore(eater.second) && is_poisson(target.second)) continue; std::cout << " " << eater.first << " mange " << target.first << std::endl; ecs::remove_entity(target.first); // eat ecs::print(); } return 0; }
affiche :
algues: 8. poissons: toto [M], titi [F], tata [M], tuto [F], tyty [M], 13 mange 3 algues: 7. poissons: toto [M], titi [F], tata [M], tuto [F], tyty [M], 12 mange 6 algues: 6. poissons: toto [M], titi [F], tata [M], tuto [F], tyty [M], 10 mange 12 algues: 6. poissons: toto [M], titi [F], tata [M], tyty [M], 13 mange 10 algues: 6. poissons: toto [M], tata [M], tyty [M], 13 mange 7 algues: 5. poissons: toto [M], tata [M], tyty [M], 13 mange 4 algues: 4. poissons: toto [M], tata [M], tyty [M], 13 mange 9 algues: 4. poissons: tata [M], tyty [M], 11 mange 2 algues: 3. poissons: tata [M], tyty [M], 11 mange 5 algues: 2. poissons: tata [M], tyty [M], 13 mange 11 algues: 2. poissons: tyty [M], 13 mange 8 algues: 1. poissons: tyty [M], 13 mange 1 algues: 0. poissons: tyty [M],
#include <iostream> #include <vector> #include <string> #include <algorithm> #include <numeric> #include <iterator> #include <cassert> #include <random> #include <tuple> namespace ecs { namespace entity { using id = size_t; using entities = std::vector<id>; } namespace component { using name = std::string; using is_male = bool; using detail = std::pair<name, is_male>; using detail_component = std::pair<entity::id, detail>; enum class type { algue, Mérou, Thon, PoissonClown, Sole, Bar, Carpe }; using pv = size_t; using type_component = std::tuple<entity::id, type, pv>; bool is_algue(type t) { return (t == type::algue); } bool is_poisson(type t) { return !is_algue(t); } bool is_carnivore(type t) { return (t == type::Mérou || t == type::Thon || t == type::PoissonClown); } bool is_herbivore(type t) { return (t == type::Sole || t == type::Bar || t == type::Carpe); } std::string to_string(type t) { static const std::string types[] = { "Algue", "Mérou", "Thon", "Poisson-clown", "Sole", "Bar", "Carpe" }; const auto i = static_cast<size_t>(t); assert(true); return types[i]; } } namespace system { using details = std::vector<component::detail_component>; using types = std::vector<component::type_component>; } namespace internal { entity::entities _entities; system::details _details; system::types _types; std::random_device _random_device; std::default_random_engine _random_engine { _random_device() }; } entity::id create_entity() { const auto id = internal::_entities.empty() ? 1 : internal::_entities.back() + 1; internal::_entities.push_back(id); return id; } entity::id add_algue() { const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, component::type::algue, 10)); return id; } entity::id add_poisson(component::type t, component::name n, component::is_male m) { assert(is_poisson(t)); const auto id = create_entity(); internal::_types.push_back(std::make_tuple(id, t,10)); internal::_details.push_back(std::make_pair(id, make_pair(n, m))); return id; } void remove_entity(entity::id id) { const auto entities_it = std::remove(internal::_entities.begin(), internal::_entities.end(), id); internal::_entities.erase(entities_it, internal::_entities.end()); const auto details_it = std::remove_if(internal::_details.begin(), internal::_details.end(), [id](auto p){ return (p.first == id); }); internal::_details.erase(details_it, internal::_details.end()); const auto types_it = std::remove_if(internal::_types.begin(), internal::_types.end(), [id](auto p){ return (std::get<0>(p) == id); }); internal::_types.erase(types_it, internal::_types.end()); } void eat(entity::id eater, entity::id target) { assert(eater != target); const auto eater_it = std::find_if(begin(internal::_types), end(internal::_types), [eater](auto p){ return (std::get<0>(p) == eater); }); const auto target_it = std::find_if(begin(internal::_types), end(internal::_types), [target](auto p){ return (std::get<0>(p) == target); }); assert(is_poisson(std::get<1>(*eater_it))); assert(is_carnivore(std::get<1>(*eater_it)) || (is_herbivore(std::get<1>(*eater_it)) && is_algue(std::get<1>(*target_it)))); remove_entity(target); //eat } entity::id random_entity() { std::uniform_int_distribution<size_t> distribution(0, internal::_entities.size() - 1); const auto index = distribution(internal::_random_engine); return internal::_entities[index]; } entity::id random_poisson() { std::uniform_int_distribution<size_t> distribution(0, internal::_details.size() - 1); const auto index = distribution(internal::_random_engine); return internal::_details[index].first; } void print_algues() { const auto count = std::count_if(begin(internal::_types), end(internal::_types), [](auto p){ return (std::get<1>(p) == component::type::algue); }); std::cout << "algues: " << count; } void print_poissons() { std::cout << "poissons: "; std::transform(begin(internal::_details), end(internal::_details), std::ostream_iterator<std::string>(std::cout, ", "), [](auto p){ return (p.second.first + " [" + (p.second.second ? 'M' : 'F') + ']'); }); } void print() { print_algues(); std::cout << ". "; print_poissons(); std::cout << std::endl; } } int main() { ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_algue(); ecs::add_poisson(ecs::component::type::Mérou, "toto", true); ecs::add_poisson(ecs::component::type::Mérou, "titi", false); ecs::add_poisson(ecs::component::type::Mérou, "tata", true); ecs::add_poisson(ecs::component::type::Mérou, "tuto", false); ecs::add_poisson(ecs::component::type::Mérou, "tyty", true); ecs::print(); for (size_t i {}; i < 50; ++i) { std::uniform_int_distribution<size_t> distribution(0, ecs::internal::_types.size() - 1); auto index = distribution(ecs::internal::_random_engine); const auto eater = ecs::internal::_types[index]; index = distribution(ecs::internal::_random_engine); auto & target = ecs::internal::_types[index]; if (std::get<0>(eater) == std::get<0>(target)) continue; if (is_algue(std::get<1>(eater))) continue; if (is_herbivore(std::get<1>(eater)) && is_poisson(std::get<1>(target))) continue; std::get<2>(target) -= 2; // eat std::cout << " " << std::get<0>(eater) << " mange " << std::get<0>(target) << " reste " << std::get<2>(target) << std::endl; ecs::print(); } return 0; }