c# - child 对象与 parent 的沟通模式

标签 c# unit-testing design-patterns c#-4.0

我实际上正在用 C# 开发一个多线程游戏服务器。我正在尝试将其创建为可单元测试的。

到目前为止,我开始使用的模式类似于这样:

实例化Server类:

  • 用于启动和关闭服务器

OnStart 服务器实例化 ConnectionManager :

  • 包含 ConnectionPool 类的实例
  • 用于接受异步连接(通过 TCPListener.BeginAcceptSocket)
  • 当接受套接字时,将创建 ClientConnection 实例并将其添加到 ConnectionsPool 列表中

ConnectionsPool:

  • 负责向池中添加和删除事件连接,以列表形式进行管理

ClientConnection:

  • 负责通过Socket.BeginReceive接收数据
  • 收到所有内容后,将创建 SocketHandler 实例(主要解析套接字内容并运行操作)

架构应如下所示:
服务器 -> ConnectionManager -> ConnectionsPool -> ClientConnection -> SocketHandler

问题:
当我在 SocketHandler 内部时,我如何影响其他玩家? (例如,玩家A击中玩家B:我需要在ConnectionsPool中获取玩家B的实例并更新他的HP属性)甚至更新服务器本身(比如调用Server类上的shutdown方法)

我假设有 3 个选择:

  • 将所有重要的类(Server + ConnectionsPool)转换为 静态类 > 缺点:无法进行单元测试
  • 将所有重要的类转换为 Singleton 类 > 缺点: 难以测试
  • 在子构造函数中注入(inject)重要类的实例> 缺点:由于传递了大量信息,可读性较差

这里的目标是使用最佳实践使所有单元都可测试,同时保持简单的方法。

我收到了向 child 注入(inject)委托(delegate)的建议,这与第三种方法类似,但我不确定是否对其进行单元测试以及真正的效果,因为当然一切都是多线程的。

这里最好的架构选择是什么?

最佳答案

我发现的主题是网络代码并不是真正可进行单元测试的,并且应该与您希望测试的代码(例如 Player 对象)隔离。对我来说,您似乎将 Player 与 ClientConnection 紧密耦合,这可能会使其难以测试。我还认为将玩家与他的连接耦合可能违反了 SRP,因为他们有非常不同的职责。

我已经编写了一个看起来与您希望实现的系统类似的系统,并且我通过仅通过委托(delegate)和接口(interface)将玩家链接到连接来实现这一点。我有一个单独的 World 和 Server 类库,以及第三个项目 Application,它引用这两个项目,并创建 Server 和 World 的实例。我的 Player 对象位于世界中,连接位于服务器中 - 这两个项目不会相互引用,因此您可以考虑 Player,而无需任何网络条款。

我使用的方法是将SocketHandler抽象化,并在Application项目中实现一个具体的处理程序。处理程序具有对世界对象的引用,可以在创建时将其传递给它。 (我通过工厂类来实现这一点 - 例如,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 引用转发给它的处理程序,以及处理程序所需的有关 Connection 本身的任何信息,例如特别是 Send 方法。这里我只是将 Connection 对象本身传递给 Handler。

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 对象,分配委托(delegate)或接口(interface)来更新操作,然后将玩家添加到 World 中的玩家池中。最初,我使用 Player 中的事件列表来完成此操作,我使用 ClientHandler 中的方法(它知道 Connection)来订阅这些事件,但事实证明 Player 对象中有数十个事件,这使得维护变得一团糟。

我选择的选项是在播放器的 World 项目中使用抽象通知程序。例如,对于运动,我将在 Player 中拥有一个 IMovementNotifier 对象。在应用程序中,我将创建一个 ClientNotifier,它实现此接口(interface)并向客户端发送相关数据。 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(...));
    }
}

可以更改 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);

关于c# - child 对象与 parent 的沟通模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7016212/

相关文章:

c# - 使用 LINQ 对字符串数组进行排序

c# - 调用基方法和派生方法

android - 包含CompositeDisposable的测试演示者

javascript - 在 Jasmine 中 mock Angular $元素

swift - 核心数据 : How to Create a Persistent Document with Singleton Entity?

android - 用今天、昨天、明天等字符串格式化日期的正确方法

c# - 将大文件加载到词典的最佳解决方案

c# - Process.Start ("name.exe") - 如何找到 'name.exe'?

unit-testing - 使用 Pester 模拟 [System.IO.Path]::IsPathRooted()?

design-patterns - 哪个是正确的导航设计模式?