Jeu de sudoku en React, développer comme projet final du cours de POO2. De plus, notre équipe avait pour défi de développer en respectant les différentes contraintes de la programmation immuable.
Pour tester et jouer au sudoku, appuyer ici!
-
Le principe de responsabilité unique dit qu'une classe doit avoir une seule et unique responsabilité dans son implementation. De plus toutes ces méthodes offertes doivent être en lien direct avec cette responsabilité. L'une des meilleurs exemples dans notre projet est notre classe
GridGenerator
, qui s'occupe de la generation d'une nouvelle grille de jeu partiellement commencer.On peut voir que la classe a qu'une seule et unique responsabilité qui est de générer une grille. Un autre exemple serait notre classe
File
qui s'occupe de l'écriture et de la lecture d'un fichier pour la sauvegarde et le chargement d'une grille. -
Un exemple de la façon dont notre projet implémente le principe SOLID Ouvert/Fermé, est par l'utilisation d'interfaces pour nos classes de Sudoku,
ICell
etIBox
. Ces interfaces font en sorte que l'extension des fonctionnalités de notre programme, avec desCell
et desBox
répondant à des critères différents, se ferait sans impacter les fonctionnalitées existantes. Un exemple de modifications qui pourraient bénéficier de notre choix d'utiliser les interfaces mentionnées ci-haut, est que notreGrid
pourrait utiliser desCell
ou desBox
avec des comportements différents, sans avoir besoin de faire un énorme réusinage du code. -
Pour ce projet nous avons plusieurs exemples qui correspondent au principe de substitution de Liskov, il s'agit de
IBox.ts
etICell.ts
. Les classesBox.ts
etCell.ts
héritent de ces interfaces respectivement. Dans notre programme, lorsqu'on veux utiliser ces variables, on passe leur interface comme type de variable pour la fonction. De cette façon, si on crée une nouvelle classe qui hérite d'une de ces interfaces elle va pouvoir être envoyer comme paramètre. Donc, si on change uneCell.ts
par cette nouvelle classeSpecialCell.ts
, elle va fonctionner sans problème dans notre programme. Même avec l'implémentation fonctionnelle, nous n'avons pas d'autres classes qui héritent d'une même interface car nous avons pas vu l'intêret d'en créer d'autres. -
Le principe de ségrégation d'interface indique qu'il est mieux d'utiliser plusieurs petites interfaces au lieu d'une seule grande interface. Cela permet de mieux séparer les fonctionnalités. Dans notre projet nous avons quelques interfaces comme
ICell.ts
,IBox.ts
etISavable.ts
, celles-ci permettent de découper des fonctionnalités en en plus petites parties. Dû à la nature du projet, et de la facon dont nous l'avions approché, nous n'avons pas eu la chance de faire assez d'interfaces qui demandait à être séparé en plus petites. Les quelques interfaces que nous avions créées étaient pour des entités de sudoku commeBox.ts
etCell.ts
. -
Dans notre cas, les fonctions
save()
etload()
de la classeGrid
utilisent une interfaceIFile
qui définie les fonctions qu'un fichier devrait contenir ainsi, la responsabilité d'instancier un object qui implémenteIFile
revient à l'utilisateur deGrid
. Ceci nous permet d'implémenter, à la fois, une vraie classe pour sauvegarder et charger des fichiers et un MockFile qui nous permet de faire des tests. Par exemple, dans le testTestGridSave()
qui, comme le nom l'indique, test si la sauvegarde du fichier fonctionne correctement en utilisant une propriété dansFileMock
au lieu de réellement écrire dans un fichier.
-
Le projet utilise le principe GRASP de créateur pour générer la grille du sudoku, l'initialisation des
Grid
est faite dans la classeGridGenerator
et retourne une grille de jeux partiellement compléter. Un autre exemple de créateur serait dans 'App.tsx' qui fait la création d'une nouvelleCell
lorsqu'on sauvegarde les informations reliées à celle-ci. Puisque notre code doit être immuable nous ne pouvons changer les propriétés d'uneCell
, nous devons en créer une nouvelle et c'estApp.tsx
qui s'occupe de prendre les données nécessaires et de créer une nouvelle cellule à partir des informations de l'ancienne et de l'information changée par l'utilisateur. -
Le projet utilise le patron spécialiste de l'information pour la validation du sudoku, au lieu de donner la responsabilité a la classe
Cell
(qui connait seulement sa valeur et ses commentaires), la validation ce fait dansIBox
, qui valide que tous cesCell
, 9 au total, ait une valeur différente. La validation se fait aussi dansGrid
qui valide les colonnes et les rangés de celle-ci. Pour résumé, la validation d'une grille se fait dansGrid
, en ce qui est pour les colonnes et les rangés, et dansIBox
pour la validation d'une boite 3x3, chaqu'un qui connait toute l'information nécessaire pour faire la validation. -
Le projet utilise en quelque sorte un contrôleur nommé
App.tsx
situé dans le dossierviews
qui s'occupe essentiellement de faire la connection entre le front-end et le back-end. Il s'occupe, par exemple, de lier la sauvegarde et le chargement d'uneGrid
en back-end au front-end qui affiche la grille générée et permet de convertir la grille dont l'utilisateur a modifié en un objetGrid
dont le backend peut utiliser pour faire la sauvegarde.
-
Le projet utilise le design pattern du Décorateur avec la fonction de undo / redo. En effet, puisque nos grilles de jeux implementent l'interface IGrid, nous pouvons donc utiliser n'importe quelle grille dans notre jeu de sudoku. Par exemple, le décorateur est implémenté avec notre
RememberingGrid
, qui prend en paramètre uneIGrid
, et qui va utiliser cetteIGrid
au travers de chacune des fonctions d'uneIGrid
qui vont seulement appeler cetteIGrid
. De plus,RememberingGrid
est un décorateur puisqu'il va garder une historique de chacun des états de laIGrid
lorsqu'il y a une modification pour pouvoir permettre le comportement du undo / redo. -
Notre projet utilise le design pattern de Fabrique pour la création d'une grille de jeu. Nous avons une classe abstraite
GridFactory
qui demande a tout ceux qui l'hérite de définircreateGrid(gridContent?: string)
, de cette facon nous pouvons facilement créer nos deux types de grille de jeu soitRememberingGrid
etGrid
, et cela viens facile a supporter si nous ajoutons de nouveau type dans le futur. Dans notre cas nous avons du utiliser une classe abstraite au lieu d'une interface puisque en TypeScript nous ne pouvons définir de méthode non abstraite et pour éviter de copier du code on donne les fonctionmakeGrid
etgetRandomString
a la classe parent pour permettre de créer une grille de jeu partiellement remplie si c'est le cas. -
Notre projet utilise le design pattern de l'Itérateur à plusieurs endroits dans le code. L'un des meilleurs exemple est la fonction
update()
dansGrid
.public update(newCell: ICell[]): Grid { let boxes = this.boxes; let newBoxes: IBox[] = []; boxes.forEach((box) => { let newCells: ICell[] = []; let cells = box.getCells(); cells.forEach((cell) => { let index = newCell.findIndex(c => c.getId() === cell.getId()); if (index !== -1) { newCells.push(newCell[index]) } else { newCells.push(cell); } }) newBoxes.push(new Box(newCells)); }); return new Grid(newBoxes); }
La fonction (montré ci-dessus), utilise le patron d'itérateur sous forme de foreach, nous aurions pu faire notre propre Itérateur si il faudrait faire quelque chose d'autre comme que de simplement itérer a travers notre collection comme modifier notre liste mais dans notre cas nous avons pas besoin, dans les endroits ou nous avons besoin de le faire on fait simplement une nouvelle liste basé sur celle qu'il faut changer puisque notre code doit rester immuable le plus possible. Pour ceci, nous avons décider d'utiliser des foreach qui sont des itérateur mais avec ce qu'on appele du
syntactic sugar
.
Tâche | Responsable(s) |
---|---|
Interface graphique (phase 1) | Piérik, Mickael, Hugo |
Notation spéciales (phase 1) | Xavier, Hugo, Pierik |
Système de vérification (phase 1) | Xavier, Maxime |
Sauvegarde et chargement (phase 1) | Maxime, Mickael |
Tests unitaires (phase 1) | Xavier, Maxime, Mickael |
Rapport (phase 1) | Xavier, Maxime, Mickael, Hugo, Piérik |
Undo/Redo (phase 2) | Mickael, Xavier, Hugo, Maxime |
Couleurs (phase 2) | Piérik, Hugo |
Sélection multiple (phase 2) | Hugo, Mickael |
Tests unitaires (phase 2) | Xavier, Maxime |
Rapport (phase 2) | Mickael, Maxime, Hugo, Xavier, Piérik |
Pour faire marcher les tests unitaires il suffit :
- Cloner le projet
- Aller dans le projet
- Exécuter la commande
npm install
- Exécuter la commande
npm test
*Il y a environ 47 tests dans 4 suites différentes - S'il y a seulement quelques tests qui ont été exécutés, appuyer sur la touche
a
pour exécuter tous les test