14 KiB
BoideloV3 - Documentation Architecture
Vue d'ensemble
Ce document décrit l'architecture mise en place pour l'application BoideloV3, un jeu de défi à boire pour Android.
L'architecture suit le pattern Repository + Separation of Concerns, permettant :
- Une meilleure séparation des responsabilités
- Un code testable unitairement
- Une gestion d'erreurs robuste
- Une maintenance facilitée
Structure du projet
app/src/main/java/com/example/boidelov3/
├── data/ # Couche d'accès aux données
│ ├── Result.java # Wrapper de résultat (succès/erreur)
│ └── QuestionRepository.java # Repository pour les questions
├── game/ # Logique métier du jeu
│ └── GameEngine.java # Moteur de jeu
├── ui/ # Activités et Fragments
│ ├── Jeux.java # Activité principale du jeu
│ ├── JeuxParametres.java # Écran de paramètres
│ ├── EndGameActivity.java # Écran de fin de partie
│ └── MainActivity.java # Écran d'accueil
└── utils/ # Utilitaires
├── DatabaseConnection.java # Connexion BDD PostgreSQL
├── SoundManager.java # Gestion des sons
└── BoideloAnimationUtils.java # Animations UI
app/src/test/java/com/example/boidelov3/
├── data/
│ └── ResultTest.java # Tests de Result
└── game/
└── GameEngineTest.java # Tests de GameEngine
1. Couche Data (données)
1.1 Result<T, E>
Wrapper type-safe pour représenter le résultat d'une opération qui peut échouer.
Avantages :
- Remplace les exceptions par des valeurs de retour
- Force le traitement des erreurs
- Rend le code plus prévisible
Utilisation :
// Créer un résultat succès
Result<String, Exception> success = Result.success("Données chargées");
// Créer un résultat erreur
Result<String, Exception> failure = Result.failure(new Exception("Erreur de chargement"));
// Utiliser le résultat
if (result.isSuccess()) {
String data = result.getData();
// Traiter les données
} else {
Exception error = result.getError();
// Gérer l'erreur
}
// Ou avec getOrElse
String value = result.getOrElse("valeur par défaut");
Méthodes disponibles :
| Méthode | Description |
|---|---|
success(T data) |
Crée un résultat réussi |
failure(E error) |
Crée un résultat d'erreur |
isSuccess() |
Vrai si réussi |
isFailure() |
Vrai si échoué |
getData() |
Retourne les données (null si erreur) |
getError() |
Retourne l'erreur (null si succès) |
getOrNull() |
Retourne les données ou null |
getOrElse(T default) |
Retourne les données ou une valeur par défaut |
1.2 QuestionRepository
Gère l'accès aux données des questions (JSON + SharedPreferences).
Responsabilités :
- Charger les questions depuis
assets/question.json - Suivre les questions déjà posées
- Sauvegarder les statistiques de jeu
Utilisation :
// Créer le repository
QuestionRepository repository = new QuestionRepository(context);
// Charger les questions
Result<Questions, QuestionLoadException> result = repository.loadQuestions();
if (result.isSuccess()) {
Questions questions = result.getData();
List<Question> questionList = questions.getQuestions();
// Utiliser les questions
} else {
QuestionLoadException error = result.getError();
Log.e("TAG", "Erreur: " + error.getMessage());
}
// Obtenir une question aléatoire non posée
Result<Question, QuestionLoadException> randomResult =
repository.getRandomUnaskedQuestion(questionList);
// Réinitialiser les questions posées
repository.resetAskedQuestions();
// Sauvegarder les stats
repository.saveGameStats(questionsPlayed, playersCount);
Méthodes principales :
| Méthode | Description |
|---|---|
loadQuestions() |
Charge toutes les questions depuis le JSON |
getRandomUnaskedQuestion(List) |
Retourne une question non posée |
markQuestionAsAsked(int) |
Marque une question comme posée |
resetAskedQuestions() |
Réinitialise le suivi des questions |
saveGameStats(int, int) |
Sauvegarde les statistiques |
2. Couche Game (logique métier)
2.1 GameEngine
Contient toute la logique du jeu, indépendante de l'UI.
Responsabilités :
- Remplacement des variables dans les questions (
<J1>,<variante>, etc.) - Gestion des manches actives
- Sélection aléatoire de joueurs
- Calcul des gorgées
Utilisation :
// Créer le moteur de jeu
GameEngine engine = new GameEngine();
// Préparer les joueurs
List<String> players = Arrays.asList("Alice", "Bob", "Charlie", "David");
int addedGorgees = 2; // Paramètre de jeu
// Traiter une question
Question rawQuestion = ...; // Question depuis le JSON
ProcessedQuestion processed = engine.processQuestion(rawQuestion, players, addedGorgees);
// Afficher la question traitée
textView.setText(Html.fromHtml(processed.question.getQuestion()));
// Vérifier si c'est une manche
if (processed.isManche) {
// C'est un défi à manches
Log.d("Game", "Manche active: " + processed.question.getManchesRestantes());
}
// Mettre à jour les manches (à chaque tour)
MancheState mancheState = engine.updateManches();
if (mancheState.endMessage != null) {
// Une manche s'est terminée
Toast.makeText(context, mancheState.endMessage, Toast.LENGTH_LONG).show();
} else if (mancheState.activeManche != null) {
// Une manche est toujours active
Question activeManche = mancheState.activeManche;
int remaining = activeManche.getManchesRestantes();
// Afficher le compteur de manches
}
// Sélectionner des joueurs aléatoires
List<String> selectedPlayers = engine.selectRandomPlayers(players, 3);
// Retourne 3 joueurs uniques
// Vider toutes les manches (fin de partie)
engine.clearManches();
Classes internes :
| Classe | Description |
|---|---|
ProcessedQuestion |
Conteneur pour une question traitée |
MancheState |
État des manches après mise à jour |
Méthodes principales :
| Méthode | Description |
|---|---|
processQuestion(Question, List, int) |
Traite une question (remplace variables) |
selectRandomPlayers(List, int) |
Sélectionne n joueurs uniques |
updateManches() |
Met à jour et retourne l'état des manches |
hasActiveManche() |
Vrai si une manche est active |
clearManches() |
Vide toutes les manches actives |
3. Utilisation recommandée dans une Activity
Voici comment intégrer les nouvelles classes dans Jeux.java :
public class Jeux extends AppCompatActivity {
private QuestionRepository repository;
private GameEngine gameEngine;
private List<String> players;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jeux);
// Initialiser les composants
repository = new QuestionRepository(this);
gameEngine = new GameEngine();
players = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR");
// Charger les questions
loadAndStartGame();
}
private void loadAndStartGame() {
Result<Questions, QuestionRepository.QuestionLoadException> result =
repository.loadQuestions();
if (result.isSuccess()) {
Questions questions = result.getData();
displayNextQuestion(questions);
} else {
Toast.makeText(this, "Erreur de chargement", Toast.LENGTH_SHORT).show();
finish();
}
}
private void displayNextQuestion(Questions allQuestions) {
// Obtenir une question aléatoire
Result<Question, QuestionRepository.QuestionLoadException> result =
repository.getRandomUnaskedQuestion(allQuestions.getQuestions());
if (result.isFailure()) {
endGame();
return;
}
Question question = result.getData();
// Traiter la question avec le moteur de jeu
int addedGorgees = getIntent().getIntExtra("EXTRA_AJOUT_GORGEE", 0);
GameEngine.ProcessedQuestion processed =
gameEngine.processQuestion(question, players, addedGorgees);
// Afficher
questionTextView.setText(Html.fromHtml(processed.question.getQuestion()));
// Gérer les manches
GameEngine.MancheState mancheState = gameEngine.updateManches();
if (mancheState.endMessage != null) {
Toast.makeText(this, mancheState.endMessage, Toast.LENGTH_LONG).show();
}
}
private void endGame() {
repository.saveGameStats(totalQuestions, players.size());
// Naviguer vers EndGameActivity
}
@Override
protected void onDestroy() {
super.onDestroy();
gameEngine.clearManches();
}
}
4. Tests unitaires
Les tests sont situés dans app/src/test/java/.
4.1 Exécuter les tests
Depuis Android Studio :
- Clic droit sur la classe de test
- Select "Run 'ResultTest'"
Depuis la ligne de commande :
./gradlew testDebugUnitTest
Note : Avec Gradle 8.13, il y a un bug connu. Utilisez Android Studio ou un JDK 17-21.
4.2 Résultat des tests
| Classe | Tests | Couverture |
|---|---|---|
ResultTest |
8 tests | 100% Result |
GameEngineTest |
15 tests | Logique jeu complète |
5. Connexion Base de Données
5.1 DatabaseConnection
Classe moderne pour la connexion PostgreSQL (remplace l'obsolète AsyncTask).
Utilisation :
DatabaseConnection dbConnection = new DatabaseConnection();
dbConnection.connectAsync(new DatabaseConnection.ConnectionCallback() {
@Override
public void onSuccess(PGConnection connection) {
// Connexion réussie - thread principal (UI)
Log.d("Database", "Connecté !");
// Utiliser la connexion...
}
@Override
public void onFailure(Exception error) {
// Erreur - thread principal (UI)
Log.e("Database", "Erreur de connexion", error);
Toast.makeText(context, "Erreur: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
});
// Dans onDestroy()
@Override
protected void onDestroy() {
super.onDestroy();
dbConnection.shutdown(); // Libérer les ressources
}
5.2 Configuration
Les identifiants de connexion sont dans local.properties (non versionné) :
# Database credentials (DO NOT COMMIT)
db.url=jdbc:postgresql://your-host:5432/database
db.user=your-username
db.password=your-password
Ils sont injectés automatiquement dans BuildConfig par build.gradle.
6. Bonnes pratiques
6.1 Gestion des erreurs
// ✅ BON - Utiliser Result
Result<Data, Exception> result = repository.loadQuestions();
if (result.isSuccess()) {
// Traiter les données
} else {
// Gérer l'erreur
}
// ❌ Mauvais - Exceptions non gérées
try {
Data data = repository.loadQuestions();
} catch (Exception e) {
// Peut être oublié...
}
6.2 Séparation des responsabilités
// ✅ BON - Logique dans GameEngine
ProcessedQuestion processed = gameEngine.processQuestion(question, players, addedGorgees);
// ❌ Mauvais - Logique dans l'Activity
String text = question.getQuestion().replace("<J1>", players.get(0));
// ... logique compliquée dans l'UI
6.3 Cycle de vie
@Override
protected void onDestroy() {
super.onDestroy();
// Toujours nettoyer les ressources
if (dbConnection != null) {
dbConnection.shutdown();
}
if (gameEngine != null) {
gameEngine.clearManches();
}
}
7. Diagramme de flux
┌─────────────────┐
│ Jeux Activity │ (UI)
└────────┬────────┘
│
├── loadQuestions() ──────► QuestionRepository
│ │
│ ├── Lire JSON
│ └── Retourner Result<Questions>
│
├── processQuestion() ──────► GameEngine
│ │
│ ├── Remplacer <J1>, <J2>, etc.
│ ├── Remplacer <variante>
│ ├── Gérer <manches>
│ └── Retourner ProcessedQuestion
│
└── Afficher dans TextView
8. Migration depuis l'ancien code
Ancienne méthode (AsyncTask)
// Ancien code - À NE PAS UTILISER
new DatabaseConnection().execute();
@Override
protected void onPostExecute(Connection connection) {
if (connection != null) {
// ...
}
}
Nouvelle méthode (ExecutorService + Callback)
// Nouveau code - Recommandé
DatabaseConnection db = new DatabaseConnection();
db.connectAsync(new ConnectionCallback() {
@Override
public void onSuccess(PGConnection connection) {
// Succès
}
@Override
public void onFailure(Exception error) {
// Erreur
}
});
9. Dépannage
Problème : Les tests unitaires échouent avec Gradle 8.13
Solution : Exécutez les tests depuis Android Studio, ou utilisez un JDK 17-21 au lieu de Java 24.
Problème : "Cannot resolve BuildConfig"
Solution : Faites un Build > Rebuild Project pour régénérer BuildConfig.
Problème : "QuestionRepository.QuestionLoadException"
Solution : Importez la classe interne :
import com.example.boidelov3.data.QuestionRepository.QuestionLoadException;
10. Ressources supplémentaires
Dernière mise à jour : 31 décembre 2025 Version : 3.0