Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mieux programmer : chapitre 2 #57

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions content/courses/programmation/01-bases/05-boucles.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,3 @@ let fundingTotal = donations.reduce((a, b) => a + b)
Ici, `fundingTotal` vaut 63. Notre *reducer* a d'abord réalisé le calcul `3 + 50`, puis `53 + 10`, et aurait continué de la sorte si le tableau contenait d'autres valeurs.

Il s'agit d'un exemple très simple, mais on pourrait imaginer un *reducer* plus complexe qui récupèrerait notre liste d'ingrédients pour en faire une salade ! Le principe est toujours de transformer un ensemble de valeurs en une valeur unique.

> Le premier chapitre de cette formation touche à sa fin et les suivants sont en cours d'écriture. Revenez bientôt !
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: "La complexité d'un algorithme"
---

Dans le chapitre précédent, nous avons vu les opérateurs, les types, les fonctions et les structures de données. La base de la programmation, c'est d'utiliser tous ces outils pour construire un algorithme : un programme fait pour résoudre un problème.

Imaginons que l'on souhaite récupérer uniquement les chiens dans la liste des animaux de notre refuge. C'est un algorithme très simple, mais qui peut être écrit de plusieurs façons différentes ! On peut filtrer la liste avec une boucle *for in* ou bien avec une fonction de premier ordre.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne suis pas sur que avec une fonction de premier ordre soit correcte. Je préférerais avec la fonction filter, qui elle, prend une fonction en argument (parce que c'est celle passée en argument qui est de premier ordre).


```js
let dogs = []
for (animal in animals) {
if (animal.race == "chien") {
dogs.insert(animal)
}
}
```

```js
let dogs = animals.filter(animal => animal.race == "chien")
```

Pour des problèmes plus complexes, il peut exister des approches radicalement différentes et plus ou moins intéressantes. Par exemple, pour ranger tous nos chiens dans l'ordre alphabétique de leur nom, il nous faudrait un algorithme de tri. Cela peut paraître simple, mais il existe des centaines de manières différentes d'implémenter un tel tri !

La méthode du [tri par sélection](https://fr.wikipedia.org/wiki/Tri_par_s%C3%A9lection) passe en revue tous les éléments, trouve le plus petit et le place au début de la liste, avant de recommencer avec le reste de la liste. Plus stupide, le [bogosort](https://fr.wikipedia.org/wiki/Tri_stupide) mélange tous les éléments au hasard en espérant tomber sur le bon ordre. C'est un algorithme si inefficace que si vous avez beaucoup d'éléments à trier, vous pourriez bien ne jamais tomber sur la bonne solution ! D'autres tris sont bien plus performants, mais seraient plus difficiles à résumer en quelques lignes...

Pour juger de la performance d'un algorithme, on peut étudier sa **complexité**, aussi appelée temps de calcul. Dans le cas le plus simple, un algorithme est noté O(*n*), nous indiquant que son temps de résolution augmente linéairement avec le nombre d'éléments étudiés.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pense que j'ajouterais que dans le cas de la complexité en temps, on observe le traitement de manière pessimiste.


![Courbe évoluant linéairement selon deux axes, le temps de résolution et le nombre d'éléments.](./complexite.png)

Vous verrez également des algorithmes être notés O(log *n*), indiquant qu'ils sont performants sur de très nombreux éléments, ou à l'inverse, O(*n* log *n*) qui indique que le temps explose avec le nombre d'éléments. Un tel algorithme ne devrait être utilisé que sur de petites listes !

![Deux courbes au même format que l'image précédente. A gauche, la courbe évolue avec une réduction progressive du temps de résolution par élément étudié. A droite, une courbe dont le temps de résolution augmente exponentiellement avec le nombre d'éléments étudiés.](./complexite2.png)

:::profremi
La complexité d'un algorithme peut être difficile à comprendre, mais ce qui est important à retenir, c'est que l'on juge de la performance d'un algorithme au temps qu'il met à résoudre un problème avec plus ou moins d'éléments.
:::

En programmation, il est important d'éviter de réinventer la roue. Pas besoin de recréer un algorithme de tri quand votre langage en embarque déjà un qui sera compris par tous les autres programmeurs et qui sera mis à jour.

De la même manière, si vous êtes confronté·e à un problème qui vous semble courant, il existe peut-être une manière standardisée d'y répondre. C'est ce que nous allons voir dans la section suivante.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: "Les design patterns"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je pense que cette rubrique arrive prématurément. Je pense qu'il vaut mieux introduire les patterns progressivement, quand l'exemple l'impose. De mon point de vue, les motifs de conceptions sont une contrainte lié à un langage (et/ou à une collection d'outils) et quand ils sont présentés de manière théorique, je pense que ça à tendance à tendre vers le charlatanisme (je ne parle pas spécifiquement de votre rubrique, mais des livres dédiés).

---

Les design patterns sont des bouts de code qui permettent de résoudre des problèmes courants en programmation, mais qui ne sont pas automatisés par le langage que vous utilisez. Ces solutions standardisées sont partagées sous la forme d'un code à recopier dans votre programme et à adapter légèrement.

Les design patterns ont deux intérêts : le premier est d'avoir sans doute été pensé par quelqu'un d'expérimenté, le second est d'être connu, et donc compris, par une grande partie des programmeurs de votre langage.

:::remi
L'exemple qui va suivre peut sembler curieux, mais pas de panique. Le but est simplement de comprendre le concept, non pas ce design pattern spécifiquement.
:::

Prenons un exemple avec un pattern Factory pour créer des animaux. Tous les animaux auront la même structure : un objet contenant un attribut nom et une méthode sound pour leur cri. Plutôt que de réécrire plusieurs fois le même code, on va créer une `AnimalFactory` qui, en fonction du type de l'animal, va créer l'objet adapté.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve que c'est exemple est étrange, qu'apporte-t-il de plus qu'un constructeur ?
D'où mon insistance pour présenter progressivement les motifs en fonction de leurs besoins.
(ici, Factory peut résoudre de l'absence de surcharge ou de la décoration, ce qui n'est pas le cas dans cet exemple).


```js
let newCat = (name) => {
let cat = {
name: name
}
return cat
}
```

Dans notre exemple, toutes les parties "communes" aux chats et aux chiens (c'est à dire avoir un nom et renvoyer un objet) sont dans la factory `createAnimal`, et seuls les parties qui diffèrent (dans notre cas, la méthode `sound`) est dans les fonctions `Dog` et `Cat`.

Une fois cela fait nous pouvons, en une seule ligne, créer un chat dont le nom serait Potimarron et donc la méthode `.sound()` afficherait "miaou" dans la console.

```js
let nouvelAnimal = AnimalFactory.createAnimal("cat", "Potimarron")
Console.print(nouvelAnimal.sound()) // affiche miaou
```

Il n'est pas utile de s'éterniser sur l'explication ligne par ligne, car la manière de faire diffère énormément d'un langage à l'autre. Si le sujet vous intéresse, il existe de nombreux sites référençant les design patterns et vous expliquant comment les mettre en place, tels que [Refactoring Guru](https://refactoring.guru/fr/design-patterns) et [Dofactory](https://www.dofactory.com/javascript/design-patterns/). Le site [Game Programming Patterns](https://gameprogrammingpatterns.com/) référence les patterns spécifiquement liés à la création de jeux.

:::marvin
Super ! Je peux régler tous mes problèmes en copiant-collant des patterns d'internet !
:::

:::notlikethisremi
Pas si vite ! Il ne faut pas utiliser les design patterns à tout va...
:::

:::winkastride
Comme le dit l'adage, quand on a un marteau, tout ressemble à un clou ! Attention à ne pas utiliser un pattern complexe pour régler un problème simple.
:::

Les design patterns existent avant tout pour combler un manque d'outils dans certains langages (on peut parler d'un manque d'**abstraction**). De la même manière que les fonctions de premier ordre sont plus sûres et lisibles que des boucles for, une abstraction fournie par votre langage serait plus expressive et plus simple qu'un design pattern.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Même si je préfère la programmation fonctionnelle, je serais très curieux de comprendre en quoi les fonctions de premier ordre sont plus sûres que les boucles ?


Utiliser un pattern copié d'Internet et dont vous ne comprenez pas le fonctionnement pourrait rendre le débogage plus compliqué lorsque votre code ne fonctionnera plus. Ainsi, il ne faut pas voir les design patterns comme des solutions magiques, mais toujours se demander s'il existe une autre solution plus adaptée à votre problème !
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: "Les paradigmes orienté objet et fonctionnel"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve aussi que cette rubrique arrive prématurément.

---

Précédemment, nous avons vu que pour régler certains problèmes, il existe des solutions standardisées, que tout le monde utilise. Plus largement, il existe même plusieurs manières de penser l'organisation de son programme, qu'on appelle des **paradigmes**.

Le paradigme le plus ancien et le plus simple, c'est la **programmation impérative**. Il consiste simplement à penser un programme comme une suite d'instructions que la machine exécute de haut en bas pour arriver à la solution. Bien sûr, il existe des paradigmes plus complexes ! Nous aborderons les deux les plus populaires :
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est globalement faux :)
A titre personnel, je ne trouve pas que la programmation impérative soit plus simple, à titre factuel, le Lambda Calcul est arrivé plus de 15 ans avant la machine de Turing. Donc la forme la plus ancienne de programmation est la programmation fonctionnelle ;)


- la **programmation orientée objet**, largement majoritaire dans le développement de jeux ;
- la **programmation fonctionnelle**, moins présente dans les jeux, mais qui nous permettra de comprendre comment on peut penser les programmes différemment.
Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personnellement, je n'aime pas opposer les deux. Tous les langages OOP modernes étant aussi fonctionnels, en ajoutant que je ne suis pas du tout convaincu que les deux paradigmes soient les deux plus populaires...


### L'orienté objet

Comme son nom l'indique, l'orienté objet pense les programmes comme un assemblage de briques logiques que l'on appelle des objets. Un objet contient :

- des variables qui le définissent, que l'on appelle des attributs ou des propriétés ;
- des fonctions qui lui sont propres, que l'on appelle des méthodes.

Afin de faciliter la création de ces objets, on utilise des classes, qui servent de modèle. Une classe définit les propriétés et méthodes que peut avoir un certain type d'objet, et fournit une méthode pour créer des objets héritant de cette classe.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce qui ne s'applique que à l'OOP orienté classes (mais il existe d'autres forme d'OOP, notamment celle orientée prototype, à la Self, Js ou Io)


Par exemple, on peut créer une classe Voiture avec un poids et une vitesse fixe. La couleur de chaque voiture pourra être différente. La classe propose aussi une méthode `drive()` qui fait avancer la voiture en fonction de sa vitesse.
Comment on lines +14 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce paragraphe est relativement faux, parce que si je suis votre définition, ce code est OOP :

module Car = struct 

  type t = {
      weight : int
    ; speed : float 
    ; position : int * int
   ; color : string

  }

  let make ?(weight = 1500.0) ?(speed = 60) ?(position = (0, 0)) color = 
     { weight; speed; position; color}

  let drive ({ position = (x, y), _} as car) = 
     { car with position = (x + speed * 0.1, y) }

end

Ce qui n'est pas le cas.


```ts
class Car {
weight = 1500
speed = 60
position = (0, 0)
color: string

drive = () => {
position.x += speed * 0.1
}
}
```

Les classes proposent également un constructeur : une méthode pour créer un objet héritant de la classe. Dans notre exemple, le constructeur de `Car` permet de choisir la couleur de la voiture que l'on crée.


```ts
let redCar = new Car(color = "red")
let blueCar = new Car(color = "blue")
let greenCar = new Car(color = "green")
```

On appelle ces objets des **instances** de la classe `Car`. Chacun d'entre eux peut se déplacer individuellement en faisant appel à leur méthode.

```ts
redCar.drive()
blueCar.drive()
greenCar.drive()
```

#### L'héritage

Une classe peut hériter d'une autre classe. Dans ce cas, la classe enfant possède les mêmes attributs et méthodes que sa classe parente, mais également des attributs et méthodes supplémentaires. Par exemple, on peut créer une classe Camion avec une méthode supplémentaire, Charger, lui permettant de prendre des colis.

```ts
class Truck : Car {
trunk = list{} // coffre vide

load = (item) => {
trunk.add(item)
}
}
```

```ts
let blueTruck = new Truck(color = "blue")
blueTruck.load("cargaison")
blueTruck.drive()
```

Une classe enfant peut également remplacer des attributs et des fonctions de la classe parent. Par exemple, on pourrait créer une classe Voiture de course, avec une vitesse plus élevée.

```ts
class RaceCar : Car {
speed = 180
}
```

```ts
let redCar = new Car(color = "red")
let redRaceCar = new RaceCar(color = "red")
redCar.drive() // avance de 6
redRaceCar.drive() // avance de 18
```

:::marvin
En fait, tous les composants de mon jeu seront des objets !
:::

:::profremi
Exactement. La programmation orientée objet considère les programmes comme étant des interactions entre différents objets.
:::

:::astride
C'est pour cela que ce paradigme est populaire dans les jeux vidéo. Tout l'enjeu est de prévoir comment ces objets vont interagir. Par exemple, si deux véhicules entrent en collision, ils se suppriment !
:::

L'orienté objet présente beaucoup d'avantages mais a aussi quelques défauts, notamment des problèmes de maintenabilité. Si l'on change d'avis sur la manière de penser nos classes ou si l'on souhaite corriger un bug, le système d'héritage a tendance à nous faire réécrire de nombreuses classes pour arriver à nos fins, ce qui peut mener à du code spaghetti. C'est ainsi que l'on appelle un code devenu si fouilli et incohérent que l'on a du mal à le comprendre et à remonter à la source d'un problème. Il est donc intéressant de se pencher sur la programmation fonctionnelle, qui aide à contrer ce phénomène.

### La programmation fonctionnelle

Pour rappel, une fonction est un outil qui, pour des paramètres donnés, retourne un résultat. La programmation fonctionnelle considère un programme comme étant un ensemble de fonctions mathématiques permettant d'arriver à un résultat final. Pour ce faire, deux problèmes sont importants à comprendre : les effets de bord et la mutabilité des structures de données.

#### Éviter les effets de bord

Une des choses qui peut causer un code spaghetti est un programme avec des fonctions ou des méthodes qui ont trop d'effets de bord, autrement dit des effets secondaires qui ne sont pas la valeur de retour de la fonction.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Est-ce que c'est vrai ? Je pense que la présence d'effets est relativement décorélé de la notion de "code spaghetti". Peut-être qu'il faudrait clarifier le sens que vous donnez à ce mot ? (Pour ma part, le code spaghetti, c'est un code qui est dur à maintenir et je trouve qu'il existe beaucoup d'effets de bords qui ont peut d'incidence sur la maintenabilité).


Imaginons qu'une fonction prenne un nombre et me renvoie un nombre modifié, mais qu'entre-temps, elle effectue aussi d'autres actions, par exemple modifier une autre variable et afficher une image. Ce sont des effets de bord ! Si cela arrive trop fréquemment dans votre programme, il vous sera difficile de réécrire votre code, car vous pourriez accidentellement briser d'autres parties du programme qui dépendaient des effets de bord d'anciennes fonctions.

On dit qu'une fonction est pure si elle est transparente référentiellement : la fonction n'a aucun effet de bord, et donc on pourrait remplacer l'appel de la fonction par son résultat. En d'autres termes, pour un paramètre donné, la fonction renvoie toujours le même résultat.

:::oofmarvin
Mais il y a forcément des effets de bord dans mon programme, sinon je ne pourrais pas afficher mes graphismes par exemple...
:::

:::astride
C'est vrai ! Mais il faut essayer de les isoler dans quelques fonctions qui sont uniquement dédiées à cela.
:::

#### La mutabilité des données

Un second problème à résoudre est la mutabilité des structures de données. Si plusieurs processus tournent dans mon programme, chacun modifiant les mêmes valeurs, cela peut créer des bugs ou casser la logique d'autres processus qui ne se retrouvent pas devant les valeurs qu'ils attendaient.

Reprenons l'exemple d'une liste d'ingrédients. J'ai une fonction qui me permet de faire un sandwich en découpant ces ingrédients en tranches. Cependant, ailleurs dans mon programme, une fonction a besoin des ingrédients d'origine. Si cette fonction se retrouve devant les ingrédients coupés en rondelles, cela peut l'empêcher de fonctionner correctement !

En programmation fonctionnelle, les structures de données sont immuables et les variables sont quasiment toutes des constantes. La liste d'ingrédients ne pourrait donc pas changer, et je devrai en faire une copie pour créer une liste d'ingrédients découpés !

En évitant les effets de bord et en protégeant les données, un programme écrit de manière fonctionnelle est donc un ensemble de fonctions que l'on peut facilement refactorer (réécrire, remplacer) sans avoir peur de briser quelque chose à l'autre bout du programme. Chaque processus peut librement agir sur les données sans avoir peur d'empêcher le travail d'un autre.

Ce n'est pas pour autant que vous devriez forcément utiliser ce paradigme : il est peu courant dans le jeu vidéo et vous auriez du mal à trouver des outils qui le supportent convenablement. En pratique, dans la création d'un jeu, vous pourriez écrire une structure générale en orienté objet mais penser certaines parties du code dans la logique fonctionnelle pour vous aider à le rendre plus maintenable.
3 changes: 3 additions & 0 deletions content/courses/programmation/02-logique-et-design/chapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
title: "Logique et design"
---
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions content/courses/programmation/course.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
type: SKILL
title: "Les bases de la programmation"
short_title: "Programmation"
short_title: "Mieux programmer"
description: "Aspects théoriques de la programmation pour comprendre tous les termes techniques et devenir autonome dans la création de jeux et de logiciels."
date: "2021-08-02"
author: "Nev, Aurélien Dos Santos"
date: "2022-12-14"
author: "Goulven Clec'h, Aurélien Dos Santos"
medal: SILVER
medal_message: "Cette formation est en cours de rédaction : seul le chapitre 1 est finalisé. [Voir l'avancement.](https://github.com/gamedevalliance/fairedesjeux.fr/issues/39)"
video: "https://www.youtube.com/playlist?list=PLHKUrXMrDS5v1I6RCFObboACa2PtEfTmA"
Expand Down