I am currently in the process of writing a (custom) Minecraft server application in C#. I figured it's a good way to teach me a lot of important things like concurrency and especially memory efficiency (due to simply the vastness of the object space in Minecraft).
While on the train yesterday, I was finding it hard to really "think" of a way to organize the network module. I wanted a simple external interface that would give out the necessaries (Let users react to a connection being received, message being received, and when a connection is dropped). However, I also wanted it to be fairly abstract such that I could allow for either real game clients to connect or simple fake clients.
I started over-engineering things a la IConnection
, INetworkModule
, IConnectionFactory
, IConnectionProtocol
, IMessageEncoder
, IMessageDecoder
and so on. Kingdom of nouns galore. Not only that but I wasn't really getting anywhere - it just felt like I was putting the fundamental problem behind other layers of abstraction in that I had no idea how I wanted to organize the communication between all these modules.
Then I came to the thought - why don't I just use verbs (actions/functions/methods/whatever) instead of nouns (types) - let's focus on what I want to achieve for now, and split it up into nouns later!
And so, I came up with something similar to this:
private static Func<IDisposable> Listen(
Action<TcpClient> onConnectionReceived,
Action<TcpClient> onConnectionDropped,
Action<TcpClient, int, byte[]> onBytesReceived,
Func<bool> isOpen)
{
// todo: 25565 needs to be injected
// todo: give opportunity to supply listener
// todo: refactor listener into role interface
var listener = new TcpListener(IPAddress.Any, 25565);
return () => Observable.FromAsync(listener.AcceptTcpClientAsync)
.DoWhile(isOpen)
.Subscribe(client =>
{
var stream = client.GetStream();
// on subscribe invoke the delegate first
onConnectionReceived(client);
// the delegate has the option to disconnect the client
// based on whatever criteria it see fit.
// we should now create an observable to
// listen for messages on this client.
Observable.Defer(() =>
{
// 8kb buffer for each connection
// this is actually fairly small
var buffer = new byte[8024];
// we can handle the 'doing' of things with these bytes
// inside the passed function. This function will be doing too much otherwise
return Observable.FromAsync(ct => stream.ReadAsync(buffer, 0, buffer.Length, ct))
.Select(n => new {Read = n, Bytes = buffer});
}).Subscribe(a =>
{
// drop connection if nothing was read
if (a.Read <= 0)
onConnectionDropped(client);
else
onBytesReceived(client, a.Read, a.Bytes);
});
});
}
Really simple. Well, I mean, not quite, but this basically does the role of all of the nouns (except IMessageEncoder/Decoder
that I listed earlier), but only making assumptions of the fact that we need a TcpClient
(obviously, that isn't what I want right now, but I can work to that in iteration 2!)
However, my problem with this is, is that this isn't really typical C# code.. and it surely breaks the SRP in that this function is returning a function for the entire execution of a server.
But - it makes sense to me.
So my question is, is there any real inherent downside to using a functional-esque paradigm like this as opposed to traditional Kingdom of Nouns OOP inside of C# - traditionally a multi-paradigm language? And if there are downsides, what would I face and why?
For the record, I intend for this to be OSS, so there's another issue in that some developers might not understand the code style because it's simply not OOP orientated, just pure functions.