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:
StillHammer 2025-10-17 11:42:39 +08:00
parent 5e4235889a
commit 919b68afd0

View 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