我实际上在C#中开发了一个多线程游戏服务器。我试图尽可能地将其创建为可单元测试。
截至目前,我开始使用的模式类似于以下内容:
服务器类是实例化的:
OnStart服务器实例化 ConnnectionManager :
班级 ConnectionsPool :
班级 ClientConnection :
架构应如下所示:
服务器 - > ConnectionManager - > ConnectionsPool - > ClientConnection - >的SocketHandler
问题:
当我在SocketHandler中时,我怎么能影响另一个玩家呢? (例如,玩家A击中玩家B:我需要在ConnectionsPool中获取玩家B的实例并更新他的HP属性)或甚至更新服务器本身(比如在Server类上调用shutdown方法)
我假设有3个选择:
这里的目标是在保持简单方法的同时,使用最佳实践使所有单元都可测试。
我收到了一个建议给孩子注入类似于第3种方法的代表,但我不确定单元测试这个和真正的效果,因为当然一切都是多线程的。
这里最好的建筑选择是什么?
答案 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);