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 concret simple.
Les codes sont testés directement 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”.
L'autre aspect de la programmation par contrat est l'utilisation des pré et post-conditions. Ces conditions s'appliquent sur les fonctions et garantissent que si les pré-conditions et les invariants sont respectés lors de l'appel d'une fonction, alors les post-conditions et les invariants seront aussi respectés à la fin de l'appel de la fonction.
On arrive donc naturellement au principe d'encapsulation. Une encapsulation correcte est donc une encapsulation qui apportera des garanties fortes sur l'utilisation correcte des données. Cela peut être résumé par la phrase suivante :
Facile a utiliser correctement, difficile a utiliser de façon incorrect.
Scott Meyer
Beaucoup pensent que l'encapsulation consiste simplement a mettre des variables en private
et des fonctions en public
dans une classe. Mais en pratique, ce n'est aussi simple : des fonctions libres peuvent respecter l’encapsulation (voir par exemple les fonctions std::begin
et std::end
) et au contraire des fonctions membres peuvent briser l'encapsulation (en premier lieu, les mutateurs : les setters et getters).
Pour concevoir correctement les classes, on conseille souvent de penser les classes en termes de services rendus, pas en termes d'agrégation de données. Dit autrement, il faut se poser la question “quels sont les services que doivent rendre ma classe ?” et pas “quelles sont les données contenues dans ma classe ?”.
#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; }