子对象与父母的沟通模式

时间:2011-08-10 18:57:47

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

我实际上在C#中开发了一个多线程游戏服务器。我试图尽可能地将其创建为可单元测试。

截至目前,我开始使用的模式类似于以下内容:

服务器类是实例化的:

  • 用于启动和关闭服务器

OnStart服务器实例化 ConnnectionManager

  • 包含类 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类>缺点: 难以测试
  • 在子构造函数中注入重要类的实例> 缺点:由于传递了大量信息,代码可读性较差

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

我收到了一个建议给孩子注入类似于第3种方法的代表,但我不确定单元测试这个和真正的效果,因为当然一切都是多线程的。

这里最好的建筑选择是什么?

1 个答案:

答案 0 :(得分:3)

我发现的主题是网络代码实际上不是单元可测试的,应该与您希望测试的代码(例如Player对象)隔离开来。对我来说,似乎你已经将Player紧密耦合到ClientConnection,这可能使其不太可测试。我还认为将玩家与他的联系联系起来可能违反了SRP,因为他们有非常不同的责任。

我编写了一个类似于你希望实现的系统,我通过代理和接口将播放器链接到连接来完成。我有一个单独的World和Server类库,以及第三个项目Application,它引用了这两个,并创建了Server和World的实例。我的播放器对象在世界中,而在服务器中是连接 - 这两个项目不相互引用,因此您可以考虑没有任何网络术语的播放器。

我使用的方法是使SocketHandler抽象化,并在Application项目中实现具体的处理程序。处理程序具有对world对象的引用,可以在创建时将其传递给它。 (我是通过工厂类来实现的 - 例如,ClientConnectionFactory(在Application中),其中Server只知道抽象连接和工厂。)

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对象,为更新操作分配委托或接口,然后将玩家添加到World中的玩家池中。最初我用Player中的事件列表完成了这个,我用ClientHandler中的方法订阅了它(知道了Connection),但结果发现Player对象中有很多事件变得一团糟。

我选择的选项是在播放器的World项目中使用抽象通知器。例如,对于移动,我将在Player中有一个IMovementNotifier对象。在Application中,我将创建一个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(...));
    }
}

可以更改ClientHandler ctor以实例化此通知程序。

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对象。世界图书馆完全可以进行单元测试,不需要考虑网络组件,这也是我想要在设计中实现的目标。

这是一个很大的系统,我在这里没有涉及太多,但希望这可以给你一些提示。问我是否想了解更多具体信息。

如果我能提供更多建议,那就是避免任何静态或单身 - 他们会回来咬你。如果您需要作为替代方案,请支持日益增加的复杂性,但需要提供您需要的文档。我所拥有的系统对于维护者来说可能看起来很复杂,但是有很好的记录。对于用户来说,它使用起来特别简单。主要基本上是

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