Aller au contenu

Architecture Hexagonale

Le serveur R-Type utilise l'architecture hexagonale (Ports & Adapters).

Concept

graph TB
    subgraph "Infrastructure (Adapters)"
        TCP[TCP Server]
        UDP[UDP Server]
        Mongo[MongoDB]
    end

    subgraph "Application (Use Cases)"
        UC1[CreateRoom]
        UC2[JoinRoom]
        UC3[ProcessInput]
    end

    subgraph "Domain (Core)"
        E1[Player]
        E2[Room]
        E3[GameWorld]
    end

    TCP --> UC1
    TCP --> UC2
    UDP --> UC3
    UC1 --> E2
    UC2 --> E2
    UC3 --> E3
    E3 --> Mongo

    style E1 fill:#7c3aed,color:#fff
    style E2 fill:#7c3aed,color:#fff
    style E3 fill:#7c3aed,color:#fff

Couches

Domain (Core)

Le cœur métier, aucune dépendance externe.

// Entities
class Player {
    PlayerId id_;
    std::string name_;
    Position position_;
    int health_;
};

class Room {
    RoomId id_;
    std::vector<Player> players_;
    GameState state_;
};

// Value Objects
struct Position { float x, y; };
struct PlayerId { uint32_t value; };

// Domain Exceptions
class RoomFullException : public std::exception {};

Application (Use Cases)

Orchestration des cas d'usage via Ports.

// Port (interface) - src/server/include/application/ports/out/persistence/
class IUserRepository {
public:
    virtual std::optional<User> findByEmail(const std::string& email) = 0;
    virtual void save(const User& user) = 0;
    virtual std::vector<User> findAll() = 0;
};

class IUserSettingsRepository {
public:
    virtual std::optional<UserSettingsData> findByEmail(const std::string& email) = 0;
    virtual void save(const UserSettingsData& settings) = 0;
};

// Use Case - src/server/include/application/use_cases/auth/
class LoginUseCase {
    IUserRepository& userRepo_;
    IPasswordHasher& hasher_;

public:
    AuthResult execute(const std::string& email, const std::string& password);
};

Infrastructure (Adapters)

Implémentations concrètes des Ports.

// Adapter MongoDB - src/server/infrastructure/adapters/out/persistence/
class MongoDBUserRepository : public IUserRepository {
    mongocxx::database db_;

public:
    std::optional<User> findByEmail(const std::string& email) override {
        auto doc = db_["users"].find_one(
            bsoncxx::builder::basic::make_document(
                bsoncxx::builder::basic::kvp("email", email)
            )
        );
        if (!doc) return std::nullopt;
        return User::fromBson(*doc);
    }
};

// Adapter TCP Server - src/server/infrastructure/adapters/in/network/
class TCPAuthServer {
    boost::asio::io_context& io_;
    LoginUseCase& login_;
    SessionManager& sessions_;

    void handleLoginRequest(const LoginMessage& msg) {
        auto result = login_.execute(msg.email, msg.password);
        if (result.success) {
            auto token = sessions_.createSession(result.user);
            // Send AuthResponseWithToken
        }
    }
};

Structure des Dossiers

src/server/
├── include/
│   ├── domain/
│   │   ├── entities/
│   │   │   └── Player.hpp
│   │   ├── value_objects/
│   │   │   ├── Position.hpp
│   │   │   └── Health.hpp
│   │   ├── exceptions/
│   │   └── services/
│   │       └── GameRule.hpp
│   ├── application/
│   │   ├── ports/out/
│   │   │   ├── persistence/
│   │   │   │   ├── IUserRepository.hpp
│   │   │   │   ├── IUserSettingsRepository.hpp
│   │   │   │   └── ILeaderboardRepository.hpp
│   │   │   └── ILogger.hpp
│   │   ├── use_cases/auth/
│   │   │   ├── LoginUseCase.hpp
│   │   │   └── RegisterUseCase.hpp
│   │   └── services/
│   │       └── AchievementChecker.hpp
│   └── infrastructure/
│       ├── adapters/
│       │   ├── in/network/
│       │   │   ├── TCPAuthServer.hpp
│       │   │   ├── UDPServer.hpp
│       │   │   ├── VoiceUDPServer.hpp
│       │   │   └── TCPAdminServer.hpp
│       │   └── out/persistence/
│       │       ├── MongoDBUserRepository.hpp
│       │       └── MongoDBLeaderboardRepository.hpp
│       ├── game/
│       │   └── GameWorld.hpp
│       ├── session/
│       │   └── SessionManager.hpp
│       └── logging/
│           └── Logger.hpp

Avantages

Avantage Description
Testabilité Domain testable sans infra
Flexibilité Changer de BDD sans toucher au core
Maintenabilité Responsabilités claires
Découplage Dépendances inversées

Tests

// Test avec mock repository
class MockRoomRepository : public IRoomRepository {
    std::map<RoomId, Room> rooms_;
public:
    Room findById(RoomId id) override {
        return rooms_.at(id);
    }
    void save(const Room& room) override {
        rooms_[room.id()] = room;
    }
};

TEST(JoinRoom, AddsPlayerToRoom) {
    MockRoomRepository repo;
    JoinRoomUseCase useCase(repo);

    Room room(RoomId{1}, "Test");
    repo.save(room);

    Player player(PlayerId{42}, "Alice");
    useCase.execute(RoomId{1}, player);

    auto updated = repo.findById(RoomId{1});
    EXPECT_EQ(updated.playerCount(), 1);
}