Шаблон общения для дочерних объектов с родителями - PullRequest
2 голосов
/ 10 августа 2011

Я на самом деле разрабатываю многопоточный игровой сервер на C #.Я пытаюсь создать его как можно более тестируемым.

На данный момент шаблон, который я начал использовать, напоминает что-то вроде этого:

A Сервер классэкземпляр:

  • Служит для запуска и завершения работы сервера

Запустить сервер, создать экземпляр ConnnectionManager :

  • Содержитэкземпляр класса ConnectionPool
  • Служит для приема асинхронных соединений (через TCPListener.BeginAcceptSocket)
  • Когда сокет принят, экземпляр ClientConnection создается и добавляется в список в ConnectionsPool

Класс ConnectionsPool :

  • Отвечает за добавление и удаление активных соединенийв пул, управляемый в виде списка

Класс ClientConnection :

  • Ответственный за получение данных через Socket.BeginReceive
  • Когда все получено, SocketHandler создан (в основном, анализирует содержимое сокета и выполняет действия)

Схема должна выглядеть следующим образом:
Сервер -> ConnectionManager -> ConnectionsPool -> ClientConnection-> SocketHandler

Проблема :
Когда я нахожусь внутри SocketHandler, как я могу повлиять на другого игрока?(например, игрок A обращается к игроку B: мне нужно получить экземпляр игрока B внутри ConnectionsPool и обновить его свойство HP) или даже обновить сам сервер (скажем, вызвать метод shutdown для класса Server)

Я предполагаю, что есть 3 варианта:

  • Преобразование всех важных классов (Server + ConnectionsPool) в статические классы> недостаток: не может быть проверено модулем
  • Преобразование всех важных классов в Singletonклассы> недостаток: трудно проверить
  • Внедрить в дочерний конструктор экземпляры важных классов> недостаток: менее читаемый код, так как передается много информации

Цель состоит в том, чтобы сделатьЭто все тестирование модуля с использованием лучших практик при сохранении простого подхода.

Я получил предложение ввести делегат childs, который похож на 3-й метод, но я не уверен насчет модульного тестирования этого и реального эффекта, посколькувсе, конечно, многопоточное.

Что было бы лучшеВыбор архитектуры здесь?

1 Ответ

3 голосов
/ 11 августа 2011

Тема, которую я нахожу, заключается в том, что сетевой код на самом деле не тестируется модулем и должен быть изолирован от кода, который вы хотите протестировать (например, объект Player).Мне кажется, вы тесно связали Player с ClientConnection, что может сделать его менее тестируемым.Я также думаю, что связывание Игрока с его соединением, вероятно, нарушает SRP, поскольку у них очень четкие обязанности.

Я написал систему, которая кажется похожей на ту, которую вы хотите достичь, и я сделал это путемпривязка проигрывателя к соединению только через делегатов и интерфейсы.У меня есть отдельная библиотека классов World и Server и третий проект, Application, который ссылается на них и создает экземпляр Server и World.Мой объект Player находится в Мире, и Соединение на сервере - эти два проекта не ссылаются друг на друга, поэтому вы можете рассматривать Player без каких-либо условий сетевого взаимодействия.

Метод, который я использую, состоит в том, чтобы сделать SocketHandler абстрактными реализовать конкретный обработчик в проекте приложения.Обработчик имеет ссылку на объект мира, который может быть передан ему при создании.(Я делаю это с помощью фабричных классов - например, ClientConnectionFactory (в Приложении), где Сервер знает только об абстрактных Соединениях и Фабриках.)

public class ClientConnectionFactory : IConnectionFactory
{
    private readonly World world;

    public ClientConnectionFactory(World world) {
        this.world = world;
    }

    Connection IConnectionFactory.Create(Socket socket) {
        return new ClientConnection(socket, world);
    }
}

Затем ClientConnection может пересылать ссылку World на свой обработчикнаряду с любой информацией, которую требует обработчик, о самом Соединении, например, о методах Send.Здесь я просто передаю сам объект Connection в обработчик.

public ClientConnection(Socket socket, World world)
    : base(socket) {
    this.handler = new ClientHandler(this, world);
    ...
}

Большая часть координации между игровой логикой и сетью содержится в ClientHandler и, возможно, в любых других дочерних объектах, на которые он ссылается.

class ClientHandler : SocketHandler
{
    private readonly Connection connection;
    private readonly World world;

    public ClientHandler(Connection connection, World world) {
        this.connection = connection;
        this.world = world;
        ...
    }

    //overriden method from SocketHandler
    public override void HandleMessage(Byte[] data) {
        ...
    }
}

Остальное включает в себя ClientHandler, который создает свой объект Player, назначает делегатов или интерфейсы для обновления действий, а затем добавляет игрока в пул игроков в World.Первоначально я сделал это с помощью списка событий в Player, на который я подписываюсь с помощью методов в ClientHandler (который знает о Connection), но оказалось, что в объекте Player были десятки событий, которые стали беспорядочными для обслуживания.

Я выбрал вариант использования абстрактных уведомлений в проекте World для проигрывателя.Например, для перемещения у меня будет объект IMovementNotifier в Player.В приложении я бы создал ClientNotifier, который реализует этот интерфейс и выполняет соответствующую отправку данных клиенту.ClientHandler создаст ClientNotifier и передаст его объекту Player.

class ClientNotifier : IMovementNotifier //, ..., + bunch of other notifiers
{
    private readonly Connection connection;

    public ClientHandler(Connection connection) {
        this.connection = connection;
    }

    void IMovementNotifier.Move(Player player, Location destination) {
        ...
        connection.Send(new MoveMessage(...));
    }
}

Можно изменить ctor ClientHandler для создания экземпляра этого уведомителя.

public ClientHandler(Connection connection, World world) {
    this.connection = connection;
    this.world = world;
    ...
    var notifier = new ClientNotifier(this);
    this.player = new Player(notifier);
    this.world.Players.Add(player);
}

Таким образом, в результате получается система, в которойClientHandler отвечает за все входящие сообщения и события, а ClientNotifier обрабатывает все исходящие сообщения.Эти два класса довольно сложно протестировать, поскольку они содержат много другого дерьма.В любом случае, сеть не может быть проверена модулем, но объект Connection в этих двух классах может быть поддельным.Библиотека World полностью модульно тестируется без какого-либо учета сетевых компонентов, что я в любом случае хотел достичь в своем дизайне.

Это большая система, и я не очень подробно рассмотрел ее здесь, но надеюсьэто может дать вам несколько советов.Спросите меня, не хотите ли вы получить какую-то более конкретную информацию по этому поводу.

Если бы я мог дать еще какие-либо советы, я бы хотел избежать каких-либо статических или одиночных ударов - они вернутся, чтобы укусить вас.Подумайте об увеличении сложности, если вам нужно, как альтернатива, но документ, где вам нужно.Моя система может показаться сложной для сопровождающего, но она хорошо документирована.Для пользователя это особенно просто в использовании.Главное по сути

var world = new World();
var connectionFactory = new ClientConnectionFactory(world);
var server = new Server(settings.LocalIP, settings.LocalPort, connectionFactory);
...