Add complete message communication system documentation
- Type-safe message classes with IMessage/AMessage architecture - Breaking changes versioning strategy (strict, no compatibility) - AMessage with enforced immutable metadata (timestamp, sender, messageId, partId) - Automatic fragmentation/defragmentation by IO layer - Template helper pullMessageAs<T>() for clean type-safe reception - No ordering guarantees + replaceable messages by default - Async handling via ITaskScheduler delegation - Centralized deserialization with factory pattern - Complete error handling strategy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5e4235889a
commit
919b68afd0
419
docs/02-systems/message-communication-system.md
Normal file
419
docs/02-systems/message-communication-system.md
Normal file
@ -0,0 +1,419 @@
|
||||
# Message Communication System
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système de communication inter-modules utilise des **classes de messages typées** au lieu de JSON brut pour garantir la fiabilité et la maintenabilité.
|
||||
|
||||
## Concept fondamental
|
||||
|
||||
**Une classe = un type de message**
|
||||
**Une instance = un message individuel**
|
||||
|
||||
Exemple: La classe `TankMovedMessage` définit le type "mouvement de tank". Chaque fois qu'un tank bouge, on crée une nouvelle instance de cette classe.
|
||||
|
||||
## Pourquoi des classes?
|
||||
|
||||
### Problème avec JSON brut
|
||||
- Erreurs découvertes uniquement au runtime
|
||||
- Pas de validation automatique
|
||||
- Typos dans les clés non détectées
|
||||
- Difficile à maintenir et refactoriser
|
||||
|
||||
### Solution avec classes
|
||||
- Validation au compile-time
|
||||
- Contrat clair entre modules
|
||||
- IDE autocomplétion et refactoring
|
||||
- Hot-reload friendly (petites classes isolées)
|
||||
- Évolutif (ajout de champs sans casser l'existant)
|
||||
|
||||
## Architecture
|
||||
|
||||
### IMessage - Interface de base
|
||||
|
||||
Interface pure définissant le contrat minimal pour tous les messages:
|
||||
|
||||
- **Type identification**: Chaque message déclare son type via enum
|
||||
- **Serialization**: Conversion vers JSON pour transport via IIO
|
||||
- **Deserialization**: Reconstruction depuis JSON reçu
|
||||
|
||||
### AMessage - Classe abstraite avec métadata
|
||||
|
||||
Classe abstraite obligatoire fournissant l'implémentation partielle commune à tous les messages.
|
||||
|
||||
**Métadata immutables (enforced):**
|
||||
- `timestamp` - Horodatage de création (const)
|
||||
- `sender` - Module émetteur (const)
|
||||
- `messageId` - ID unique pour tracking et reassemblage fragments (const)
|
||||
- `partId` - ID de fragment pour messages multi-parts (const)
|
||||
|
||||
**Caractéristiques:**
|
||||
- Constructeur protégé force passage par classes enfants
|
||||
- Métadata initialisées automatiquement à la construction
|
||||
- Impossible de modifier métadata après création
|
||||
- Tous messages garantis d'avoir timestamp/sender/messageId
|
||||
|
||||
### MessageType - Enum central
|
||||
|
||||
Enum listant tous les types de messages du système:
|
||||
- `TANK_MOVED`, `TANK_FIRED`, `TANK_DESTROYED`
|
||||
- `PRICE_UPDATED`, `TRADE_EXECUTED`
|
||||
- `ITEM_PRODUCED`, `BELT_CONGESTION`
|
||||
- etc.
|
||||
|
||||
**Pourquoi enum plutôt que strings?**
|
||||
- Performance (comparaison d'entiers)
|
||||
- Type safety (typos détectées au compile)
|
||||
- Centralisation (tous les types visibles)
|
||||
|
||||
### Messages concrets
|
||||
|
||||
Chaque type de message est une classe dédiée héritant de `AMessage`:
|
||||
|
||||
**TankMovedMessage**: Position, vitesse, ID tank
|
||||
**PriceUpdatedMessage**: Item, ancien prix, nouveau prix
|
||||
**ItemProducedMessage**: Item type, quantité, factory ID
|
||||
|
||||
Chaque classe:
|
||||
- Hérite obligatoirement de `AMessage`
|
||||
- Appelle constructeur `AMessage(senderModule)`
|
||||
- Stocke ses données spécifiques
|
||||
- Valide à la construction
|
||||
- Sérialise/désérialise son propre format
|
||||
|
||||
## Flow de communication
|
||||
|
||||
### Publication
|
||||
|
||||
**Exemple complet:**
|
||||
```cpp
|
||||
void TankModule::process(const json& input) {
|
||||
// 1. Calculer nouvelle position
|
||||
Vector2 newPos = calculatePosition();
|
||||
float currentSpeed = getSpeed();
|
||||
|
||||
// 2. Créer message (validation automatique à la construction)
|
||||
TankMovedMessage msg(newPos, currentSpeed, tankId);
|
||||
|
||||
// 3. Sérialiser en JSON
|
||||
json serialized = msg.serialize();
|
||||
|
||||
// 4. Publier via IIO
|
||||
io->publish("tank:" + std::to_string(tankId), serialized);
|
||||
}
|
||||
```
|
||||
|
||||
**Étapes:**
|
||||
1. Module crée instance de message (validation à la construction)
|
||||
2. Message sérialisé en JSON via `serialize()`
|
||||
3. Publié via `IIO::publish(topic, json)`
|
||||
4. IIO route vers subscribers du topic
|
||||
|
||||
### Réception
|
||||
|
||||
**Méthode 1: Type-safe template helper (recommandé)**
|
||||
```cpp
|
||||
// Clean syntax avec type safety automatique
|
||||
auto tankMsg = io->pullMessageAs<TankMovedMessage>();
|
||||
|
||||
if (tankMsg) { // nullptr si type mismatch ou queue vide
|
||||
Vector2 pos = tankMsg->getPosition();
|
||||
float speed = tankMsg->getSpeed();
|
||||
}
|
||||
```
|
||||
|
||||
**Méthode 2: Manuelle (si besoin de flexibilité)**
|
||||
```cpp
|
||||
// Récupère message brut
|
||||
Message rawMsg = io->pullMessage();
|
||||
|
||||
// Désérialise vers base
|
||||
std::unique_ptr<IMessage> baseMsg = IMessage::deserialize(rawMsg.data);
|
||||
|
||||
// Cast vers type concret
|
||||
if (TankMovedMessage* tankMsg = dynamic_cast<TankMovedMessage*>(baseMsg.get())) {
|
||||
// Accès type-safe
|
||||
}
|
||||
```
|
||||
|
||||
**Template helper implementation:**
|
||||
```cpp
|
||||
class IIO {
|
||||
// Template helper inline (zero overhead)
|
||||
template<typename T>
|
||||
std::unique_ptr<T> pullMessageAs() {
|
||||
if (!hasMessages()) return nullptr;
|
||||
|
||||
Message raw = pullMessage();
|
||||
std::unique_ptr<IMessage> base = IMessage::deserialize(raw.data);
|
||||
|
||||
T* casted = dynamic_cast<T*>(base.get());
|
||||
if (!casted) return nullptr; // Type mismatch
|
||||
|
||||
base.release();
|
||||
return std::unique_ptr<T>(casted);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
- Template inlined = zero function call overhead
|
||||
- `dynamic_cast` = ~5-10ns (négligeable vs JSON parsing)
|
||||
- Bottleneck réel = JSON serialization, pas le cast
|
||||
|
||||
### Désérialisation centralisée
|
||||
|
||||
**Factory pattern avec routing par type:**
|
||||
```cpp
|
||||
// IMessage base class
|
||||
std::unique_ptr<IMessage> IMessage::deserialize(const json& data) {
|
||||
// Extract type from JSON
|
||||
int typeInt = data.value("type", -1);
|
||||
MessageType type = static_cast<MessageType>(typeInt);
|
||||
|
||||
// Route to concrete deserializer
|
||||
switch (type) {
|
||||
case MessageType::TANK_MOVED:
|
||||
return TankMovedMessage::deserialize(data);
|
||||
|
||||
case MessageType::PRICE_UPDATED:
|
||||
return PriceUpdatedMessage::deserialize(data);
|
||||
|
||||
case MessageType::ITEM_PRODUCED:
|
||||
return ItemProducedMessage::deserialize(data);
|
||||
|
||||
// Add new messages here
|
||||
|
||||
default:
|
||||
return nullptr; // Unknown type
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Chaque message implémente sa propre désérialisation:**
|
||||
```cpp
|
||||
class TankMovedMessage : public AMessage {
|
||||
static std::unique_ptr<TankMovedMessage> deserialize(const json& data) {
|
||||
try {
|
||||
Vector2 pos{data["position"]["x"], data["position"]["y"]};
|
||||
float speed = data["speed"];
|
||||
int tankId = data["tankId"];
|
||||
|
||||
return std::make_unique<TankMovedMessage>(pos, speed, tankId);
|
||||
} catch (const json::exception& e) {
|
||||
return nullptr; // Malformed JSON
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Gestion des erreurs:**
|
||||
- JSON malformé → retourne `nullptr`
|
||||
- Type inconnu → retourne `nullptr`
|
||||
- Validation échoue → exception à la construction
|
||||
- Modules doivent vérifier `if (msg != nullptr)`
|
||||
|
||||
## Organisation du code
|
||||
|
||||
### Location des messages
|
||||
```
|
||||
modules/shared/messages/
|
||||
├── IMessage.h # Interface pure
|
||||
├── AMessage.h # Classe abstraite avec métadata
|
||||
├── MessageType.h # Enum des types
|
||||
├── TankMovedMessage.h
|
||||
├── PriceUpdatedMessage.h
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Single source of truth (pas de duplication)
|
||||
- Contrat partagé entre émetteur et récepteur
|
||||
- Facile à trouver et maintenir
|
||||
|
||||
### Validation
|
||||
|
||||
**À la construction**: Invariants validés immédiatement
|
||||
- Vitesse négative → exception
|
||||
- ID invalide → exception
|
||||
- Fail fast pour détecter bugs tôt
|
||||
|
||||
**À la désérialisation**: Données réseau/externes validées
|
||||
- JSON malformé → retourne nullptr
|
||||
- Champs manquants → retourne nullptr
|
||||
- Protège contre données corrompues
|
||||
|
||||
## Décisions de design finalisées
|
||||
|
||||
### Versioning - Breaking changes assumées
|
||||
|
||||
**Décision:** Types versionnés avec breaking changes strictes
|
||||
|
||||
Changement de format = nouveau type + nouvelle classe:
|
||||
- `TANK_MOVED_V1` → `TANK_MOVED_V2` = types différents
|
||||
- Pas de backward compatibility
|
||||
- Pas de forward compatibility
|
||||
- Migration forcée de tous les modules
|
||||
|
||||
**Rationale:**
|
||||
- Zéro ambiguïté sur le format attendu
|
||||
- Pas de logique conditionnelle complexe
|
||||
- Hot-reload force mise à jour synchronisée
|
||||
- Détection immédiate des incompatibilités
|
||||
- Code simple et clair
|
||||
|
||||
**Conséquence:**
|
||||
- Format change → tous modules doivent migrer
|
||||
- Ancien code ne compile plus → migration manuelle obligatoire
|
||||
- Breaking changes explicites et visibles
|
||||
|
||||
### Message inheritance - AMessage obligatoire
|
||||
|
||||
**Décision:** Classe abstraite `AMessage` avec métadata immutables enforced
|
||||
|
||||
Architecture:
|
||||
```
|
||||
IMessage (interface pure)
|
||||
↓
|
||||
AMessage (classe abstraite - métadata immutables)
|
||||
↓
|
||||
TankMovedMessage, PriceUpdatedMessage... (classes concrètes)
|
||||
```
|
||||
|
||||
**Enforcement:**
|
||||
- Constructeur `AMessage` protégé → impossible de créer message sans passer par enfant
|
||||
- Métadata const → immutables après construction
|
||||
- Tous messages garantis d'avoir timestamp/sender/messageId
|
||||
- Format uniforme pour tous messages du système
|
||||
|
||||
**Rationale:**
|
||||
- Pas de duplication (métadata dans AMessage)
|
||||
- Impossible d'oublier métadata
|
||||
- Contrat strict et enforced au compile-time
|
||||
- Simplicité pour messages concrets (métadata automatique)
|
||||
|
||||
## Décisions de design finalisées (suite)
|
||||
|
||||
### Size limits - Fragmentation automatique par IO
|
||||
|
||||
**Décision:** Pas de limite au niveau message, fragmentation automatique par couche transport
|
||||
|
||||
**Architecture:**
|
||||
- **Modules**: Publient/reçoivent messages complets (transparence totale)
|
||||
- **IIO**: Gère fragmentation/défragmentation automatiquement
|
||||
- **Transport adaptatif**: Fragmentation si nécessaire selon type IO
|
||||
|
||||
**Fragmentation par type:**
|
||||
- **IntraIO**: Pas de fragmentation (copie mémoire directe)
|
||||
- **LocalIO**: Fragmentation si > seuil (ex: 64KB chunks)
|
||||
- **NetworkIO**: Fragmentation si > MTU (~1500 bytes)
|
||||
|
||||
**Mécanisme:**
|
||||
1. Module publie message → sérialisé en JSON
|
||||
2. IO détecte si taille > seuil transport
|
||||
3. Si oui: découpe en fragments avec `messageId` + `partId`
|
||||
4. Transport des fragments
|
||||
5. IO destination reassemble via `messageId` (collect tous `partId`)
|
||||
6. Module reçoit message complet via `pullMessage()`
|
||||
|
||||
**Robustesse:**
|
||||
- **Packet loss**: Timeout si fragments incomplets (ex: 30s)
|
||||
- **Ordering**: `partId` garantit ordre de reassemblage
|
||||
- **Monitoring**: Log warning si message > 100KB (design smell probable)
|
||||
|
||||
**Conséquence:**
|
||||
- Messages peuvent être arbitrairement gros
|
||||
- Complexité cachée dans couche IO
|
||||
- Performance optimisée par type de transport
|
||||
|
||||
### Async handling - Délégation au module
|
||||
|
||||
**Décision:** Pas de gestion async spéciale au niveau messages
|
||||
|
||||
**Rationale:**
|
||||
- Messages = transport de données uniquement
|
||||
- `ITaskScheduler` existe déjà pour opérations coûteuses
|
||||
- Module décide si déléguer ou traiter directement
|
||||
- Keep messages simple et stupid
|
||||
|
||||
**Responsabilité:**
|
||||
- **Message**: Transport de données (aucune intelligence)
|
||||
- **Module**: Décide de déléguer opérations coûteuses au scheduler
|
||||
- **ITaskScheduler**: Gère threading et async
|
||||
|
||||
**Exemple:**
|
||||
```cpp
|
||||
// Module reçoit message déclenchant opération coûteuse
|
||||
if (PathfindingRequestMessage* req = dynamic_cast<...>) {
|
||||
// Module délègue au TaskScheduler
|
||||
scheduler->scheduleTask(PATHFINDING_TASK, req->serialize());
|
||||
// Continue traitement autres messages
|
||||
}
|
||||
|
||||
// Plus tard, récupère résultat
|
||||
if (scheduler->hasCompletedTasks() > 0) {
|
||||
TaskResult result = scheduler->getCompletedTask();
|
||||
// Publie résultat via message
|
||||
}
|
||||
```
|
||||
|
||||
### Message ordering - Pas de garantie + Messages remplaçables
|
||||
|
||||
**Décision:** Pas de garantie d'ordre, modules gèrent via timestamp si critique
|
||||
|
||||
**Approche:**
|
||||
- **Pas de FIFO enforced**: Messages peuvent arriver dans ordre arbitraire
|
||||
- **Messages remplaçables par défaut**: `SubscriptionConfig.replaceable = true` pour majorité
|
||||
- **Timestamp disponible**: `msg->getTimestamp()` permet tri manuel si nécessaire
|
||||
- **Utilité questionnable**: Architecture modulaire rend ordre moins critique
|
||||
|
||||
**Rationale:**
|
||||
- Performance maximale (pas de garanties coûteuses)
|
||||
- Messages remplaçables optimisent bandwidth (garde juste dernier)
|
||||
- Timestamp dans `AMessage` permet tri si vraiment nécessaire
|
||||
- Architecture découple les dépendances temporelles
|
||||
|
||||
**Exemples d'usage:**
|
||||
```cpp
|
||||
// Prix - remplaçable (dernière valeur suffit)
|
||||
io->subscribeLowFreq("economy:*", {.replaceable = true});
|
||||
|
||||
// Tank position - remplaçable (position actuelle suffit)
|
||||
io->subscribe("tank:*", {.replaceable = true});
|
||||
|
||||
// Events critiques - non remplaçable + tri si nécessaire
|
||||
io->subscribe("combat:*", {.replaceable = false});
|
||||
|
||||
// Si ordre vraiment critique (rare)
|
||||
std::vector<Message> messages = collectAllMessages();
|
||||
std::sort(messages.begin(), messages.end(),
|
||||
[](const Message& a, const Message& b) {
|
||||
return a.getTimestamp() < b.getTimestamp();
|
||||
});
|
||||
```
|
||||
|
||||
**Conséquence:**
|
||||
- Modules ne doivent pas assumer ordre de réception
|
||||
- Messages remplaçables = dernière valeur seulement
|
||||
- Tri par timestamp disponible mais rarement nécessaire
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question ouverte restante. Toutes les décisions de design ont été finalisées.
|
||||
|
||||
## Statut d'implémentation
|
||||
|
||||
**Phase actuelle:** Design et documentation
|
||||
|
||||
**Prochaines étapes:**
|
||||
1. Créer interface `IMessage.h`
|
||||
2. Créer classe abstraite `AMessage.h` avec métadata immutables
|
||||
3. Créer enum `MessageType.h`
|
||||
4. Implémenter 5-10 messages d'exemple
|
||||
5. Tester avec modules existants
|
||||
6. Itérer selon usage réel
|
||||
|
||||
## Références
|
||||
|
||||
- `src/core/include/warfactory/IIO.h` - Couche pub/sub
|
||||
- `docs/01-architecture/architecture-technique.md` - Patterns de communication
|
||||
- `docs/03-implementation/CLAUDE-HOT-RELOAD-GUIDE.md` - Impact sur design messages
|
||||
Loading…
Reference in New Issue
Block a user