Aller au contenu

Compétence 4 : Architecture Logicielle

Sélectionner une hypothèse d'architecture et l'urbanisme de la solution logicielle ou web, afin de garantir l'intégration et la pérennité d'une solution, en prenant en compte le reste de l'écosystème technique présent au sein de l'entreprise ainsi que l'accessibilité numérique de la solution.


Observable 4.1 : Architecture Hexagonale

Choix Architectural

Le projet R-Type implémente une Architecture Hexagonale (Ports & Adapters), aussi connue sous le nom de Clean Architecture. Ce choix répond aux exigences de maintenabilité, testabilité et évolutivité d'un projet long terme.

Principes de l'Architecture Hexagonale

graph TB
    subgraph "Couche Infrastructure (Adapters)"
        IN1["TCPAuthServer<br/>(Adapter IN)"]
        IN2["UDPServer<br/>(Adapter IN)"]
        OUT1["MongoDBRepository<br/>(Adapter OUT)"]
        OUT2["SpdLogAdapter<br/>(Adapter OUT)"]
    end

    subgraph "Couche Application (Orchestration)"
        UC1["Register UseCase"]
        UC2["Login UseCase"]
        PORT1["IUserRepository<br/>(Port OUT)"]
        PORT2["ILogger<br/>(Port OUT)"]
    end

    subgraph "Couche Domain (Coeur Métier)"
        E1["User Entity"]
        E2["Player Entity"]
        VO1["Health (Value Object)"]
        VO2["Email (Value Object)"]
        SVC["GameRule Service"]
    end

    IN1 --> UC1
    IN2 --> UC2
    UC1 --> PORT1
    UC2 --> PORT2
    PORT1 -.->|"implements"| OUT1
    PORT2 -.->|"implements"| OUT2
    UC1 --> E1
    E1 --> VO1
    E1 --> VO2
    E2 --> SVC

    style E1 fill:#fff4e6
    style E2 fill:#fff4e6
    style VO1 fill:#fff4e6
    style VO2 fill:#fff4e6
    style SVC fill:#fff4e6

Structure des Répertoires

src/server/
├── include/
│   ├── domain/                      # Couche Domaine (coeur métier)
│   │   ├── entities/                # Entités métier
│   │   │   ├── Player.hpp
│   │   │   ├── User.hpp
│   │   │   └── Room.hpp
│   │   ├── value_objects/           # Objets de valeur immuables
│   │   │   ├── Health.hpp
│   │   │   ├── Position.hpp
│   │   │   ├── player/PlayerId.hpp
│   │   │   └── user/Email.hpp
│   │   ├── services/                # Services domaine (stateless)
│   │   │   └── GameRule.hpp
│   │   └── exceptions/              # Exceptions métier
│   │       └── DomainException.hpp
│   │
│   ├── application/                 # Couche Application
│   │   ├── use_cases/               # Cas d'usage
│   │   │   └── auth/
│   │   │       ├── Register.hpp
│   │   │       └── Login.hpp
│   │   ├── ports/out/               # Ports de sortie (interfaces)
│   │   │   ├── ILogger.hpp
│   │   │   ├── IIdGenerator.hpp
│   │   │   └── persistence/
│   │   │       ├── IUserRepository.hpp
│   │   │       └── ILeaderboardRepository.hpp
│   │   └── services/
│   │       └── AchievementChecker.hpp
│   │
│   └── infrastructure/              # Couche Infrastructure
│       ├── adapters/
│       │   ├── in/network/          # Adapters entrants
│       │   │   ├── UDPServer.hpp
│       │   │   ├── TCPAuthServer.hpp
│       │   │   └── VoiceUDPServer.hpp
│       │   └── out/                 # Adapters sortants
│       │       ├── SpdLogAdapter.hpp
│       │       └── persistence/
│       │           ├── MongoDBUserRepository.hpp
│       │           └── MongoDBLeaderboardRepository.hpp
│       └── game/
│           └── GameWorld.hpp

La Règle de Dépendance

Principe fondamental : Les dépendances pointent toujours vers l'intérieur. Le Domain ne connaît jamais l'Infrastructure.

graph LR
    I["Infrastructure"] --> A["Application"]
    A --> D["Domain"]

    I -.->|"INTERDIT"| D

    style D fill:#90EE90
    style I fill:#FFB6C1

Preuve dans le code (src/server/include/domain/entities/Player.hpp) :

namespace domain::entities {
    class Player {
    private:
        value_objects::Health _health;           // Domain seulement
        value_objects::player::PlayerId _id;    // Domain seulement
        value_objects::Position _position;      // Domain seulement
        // AUCUN include de MongoDB, Boost, spdlog, etc.
    };
}

Couche Domain : Le Coeur Métier

Entités

Les entités représentent les concepts métier avec identité propre.

User (src/server/include/domain/entities/User.hpp) :

class User {
private:
    value_objects::user::UserId _id;
    value_objects::user::Username _username;
    value_objects::user::Email _email;
    value_objects::user::Password _password;
public:
    bool verifyPassword(const std::string& attempt) const;
    void changePassword(const std::string& newPassword);
};

Room (src/server/include/domain/entities/Room.hpp) :

class Room {
public:
    enum class State { Waiting, Starting, InGame, Closed };
private:
    std::string _code;
    std::vector<std::string> _playerIds;
    State _state = State::Waiting;
    uint8_t _maxPlayers = 4;
public:
    bool addPlayer(const std::string& playerId);
    void startGame();
    bool isFull() const;
};

Value Objects

Les objets de valeur sont immuables et identifiés par leur valeur, pas par une identité.

Health (src/server/include/domain/value_objects/Health.hpp) :

class Health {
private:
    float _healthPoint;
    void validate(float value);  // Throws si invalide
public:
    // Méthodes retournent NOUVELLE instance (immuabilité)
    Health heal(float value) const {
        return Health(_healthPoint + value);
    }
    Health damage(float value) const {
        return Health(std::max(0.0f, _healthPoint - value));
    }
};

Services Domain

Les services domaine contiennent la logique métier pure, sans état.

GameRule (src/server/include/domain/services/GameRule.hpp) :

class GameRule {
public:
    // Méthodes const, stateless, sans I/O
    uint8_t getMissileDamage(uint8_t weaponType, uint8_t weaponLevel) const;
    float getMissileSpeed(uint8_t weaponType, uint8_t weaponLevel) const;
    uint16_t getEnemyPointValue(uint8_t enemyType) const;
    bool shouldBossChangePhase(uint16_t currentHP, uint16_t maxHP, uint8_t phase) const;
};

Couche Application : Orchestration

Ports de Sortie (Interfaces)

Les ports définissent les contrats que l'infrastructure doit implémenter.

IUserRepository (src/server/include/application/ports/out/persistence/IUserRepository.hpp) :

class IUserRepository {
public:
    virtual ~IUserRepository() = default;
    virtual void save(const domain::entities::User& user) const = 0;
    virtual std::optional<domain::entities::User> findById(const std::string& id) = 0;
    virtual std::optional<domain::entities::User> findByEmail(const std::string& email) = 0;
    virtual std::optional<domain::entities::User> findByUsername(const std::string& username) = 0;
};

ILogger (src/server/include/application/ports/out/ILogger.hpp) :

class ILogger {
public:
    virtual void debug(const std::string& message) = 0;
    virtual void info(const std::string& message) = 0;
    virtual void warn(const std::string& message) = 0;
    virtual void error(const std::string& message) = 0;

    // Template pour formatage C++20
    template<typename... Args>
    void info(std::format_string<Args...> fmt, Args&&... args) {
        info(std::format(fmt, std::forward<Args>(args)...));
    }
};

Use Cases

Les cas d'usage orchestrent le domaine via les ports.

Register (src/server/include/application/use_cases/auth/Register.hpp) :

class Register {
private:
    std::shared_ptr<IUserRepository> _userRepository;  // Port OUT
    std::shared_ptr<IIdGenerator> _idGenerator;        // Port OUT
    std::shared_ptr<ILogger> _logger;                  // Port OUT

public:
    // Injection de dépendances
    explicit Register(
        std::shared_ptr<IUserRepository> userRepository,
        std::shared_ptr<IIdGenerator> idGenerator,
        std::shared_ptr<ILogger> logger
    );

    std::optional<domain::entities::User> execute(
        const std::string& username,
        const std::string& email,
        const std::string& unHashedPassword
    );
};

Couche Infrastructure : Adapters

Adapters Entrants (IN)

Reçoivent les requêtes externes et appellent les Use Cases.

TCPAuthServer (src/server/infrastructure/adapters/in/network/TCPAuthServer.hpp) :

class TCPAuthServer {
private:
    std::unique_ptr<application::use_cases::auth::Register> _registerUseCase;
    std::unique_ptr<application::use_cases::auth::Login> _loginUseCase;

public:
    void handle_login(const LoginMessage& msg) {
        auto userOpt = _loginUseCase->execute(msg.username, msg.password);
        // ...
    }
};

Adapters Sortants (OUT)

Implémentent les ports pour les services externes.

MongoDBUserRepository (src/server/infrastructure/adapters/out/persistence/MongoDBUserRepository.hpp) :

class MongoDBUserRepository : public application::ports::out::persistence::IUserRepository {
private:
    std::shared_ptr<MongoDBConfiguration> _mongoDB;

public:
    void save(const domain::entities::User& user) const override {
        auto collection = getCollection();
        bsoncxx::builder::document builder;
        // Sérialisation vers BSON
        collection.insert_one(builder.view());
    }

    std::optional<domain::entities::User> findByEmail(const std::string& email) override {
        auto collection = getCollection();
        auto result = collection.find_one(make_document(kvp("email", email)));
        // Désérialisation depuis BSON
    }
};

SpdLogAdapter (src/server/infrastructure/adapters/out/SpdLogAdapter.hpp) :

class SpdLogAdapter : public application::ports::out::ILogger {
public:
    void info(const std::string& message) override {
        server::logging::Logger::getMainLogger()->info(message);
    }
    void error(const std::string& message) override {
        server::logging::Logger::getMainLogger()->error(message);
    }
};


Observable 4.2 : Intégration dans l'Écosystème Technique

L'architecture hexagonale de R-Type s'intègre naturellement dans un écosystème technique moderne.

Diagramme d'Intégration

graph TB
    subgraph "Clients"
        C1["Client SFML<br/>(Linux/Windows)"]
        C2["Client SDL2<br/>(Fallback)"]
        C3["Discord Bot<br/>(Admin/Leaderboard)"]
    end

    subgraph "Serveurs R-Type"
        TCP["TCPAuthServer<br/>:4125 (TLS)"]
        UDP["UDPServer<br/>:4124"]
        VOICE["VoiceUDPServer<br/>:4126"]
        ADMIN["TCPAdminServer<br/>:4127 (localhost)"]
    end

    subgraph "Persistance"
        MONGO["MongoDB<br/>9 collections"]
    end

    subgraph "Infrastructure"
        VPS["VPS Debian<br/>systemd services"]
        CERT["Let's Encrypt<br/>Certificats TLS"]
    end

    C1 -->|"TLS 1.2"| TCP
    C1 -->|"UDP"| UDP
    C1 -->|"UDP Opus"| VOICE
    C2 -->|"TLS 1.2"| TCP
    C3 -->|"TCP"| ADMIN

    TCP --> MONGO
    UDP --> MONGO
    VPS --> TCP
    VPS --> UDP
    CERT --> TCP

Points d'Intégration

Composant Technologie Port Protocole
Authentification Boost.ASIO + OpenSSL 4125 TCP/TLS 1.2
Jeu temps réel Boost.ASIO 4124 UDP binaire
Voice chat Boost.ASIO + Opus 4126 UDP
Administration Boost.ASIO 4127 TCP (localhost)
Persistance mongocxx - MongoDB Wire
Logging spdlog - Fichiers + console

Bénéfices de l'Architecture

1. Testabilité

Les ports permettent de mocker les dépendances :

// Test unitaire du UseCase Register
class MockUserRepository : public IUserRepository {
    void save(const User& user) const override { /* mock */ }
    std::optional<User> findByEmail(const std::string& email) override {
        return std::nullopt;  // Simule utilisateur non existant
    }
};

// Test
auto mockRepo = std::make_shared<MockUserRepository>();
Register registerUseCase(mockRepo, mockIdGen, mockLogger);
auto user = registerUseCase.execute("test", "test@test.com", "password");
EXPECT_TRUE(user.has_value());

2. Évolutivité

Changer de base de données = implémenter un nouveau Repository :

// Futur: PostgreSQL
class PostgresUserRepository : public IUserRepository {
    // Même interface, implémentation PostgreSQL
};

// Injection au démarrage
auto repo = std::make_shared<PostgresUserRepository>(pgConfig);
auto registerUseCase = std::make_unique<Register>(repo, ...);

3. Maintenabilité

Chaque couche a une responsabilité claire : - Domain : Règles métier (jamais de changement pour raison technique) - Application : Orchestration (change si workflow change) - Infrastructure : Implémentation technique (change si technologie change)

Métriques d'Architecture

Métrique Valeur Interprétation
Couches strictes 3 Domain, Application, Infrastructure
Dépendances Domain vers Infra 0 Règle de dépendance respectée
Ports abstraits 11+ Découplage complet
Adapters MongoDB 9 Couverture complète
Value Objects 10+ Immutabilité respectée
Injection de dépendances 100% UseCases Testabilité maximale

Conformité aux Principes SOLID

Principe Application Preuve
**S**ingle Responsibility Chaque classe = 1 rôle Health.hpp gère uniquement la santé
**O**pen/Closed Extensible sans modification Ajouter arme = nouvelle enum, pas modifier GameRule
**L**iskov Substitution Adapters interchangeables MongoDBUserRepository remplaçable par PostgresUserRepository
**I**nterface Segregation Ports ciblés ILogger séparé de IUserRepository
**D**ependency Inversion UseCases → Ports (abstractions) Register dépend de IUserRepository, pas MongoDBUserRepository

Conclusion

L'architecture hexagonale de R-Type : - Isole le domaine des préoccupations techniques - Facilite les tests via l'injection de mocks - Permet l'évolution sans refactoring majeur - S'intègre dans l'écosystème technique existant (MongoDB, TLS, systemd)

Cette architecture garantit la pérennité du projet face aux évolutions technologiques futures.