Sto sviluppando un servizio di windows che comunica con alcuni client tramite i socket. L'architettura dell'applicativo include una classe Client con la responabilità di ricevere i dati dai client remoti connessi al server. La connessione viene gestita tramite la classe SocketChannel che implementa l'interfaccia IChannel le quali fanno parte del layer di comunicazione.
Un piccolo diagramma UML semplificato per schematizzare le classi e le interfacce coinvolte:
Ovviamente ho volutamente semplificato la reale architettura, infatti, manca il log, la gestione degli errori, ecc.
Una versione semplificata (ma non lontana dalla realtà) del metodo Receive:
public void Receive()
{
while(_channel.IsConnected)
{
string rawData = _channel.Receive();
IMessage[] messages = _messageParser.Parse(rawData);
_messageProcessor.ProcessMessages(messages);
}
}
Inizialmente il client instanziava internamente le classi concrete SocketChannel che implementa l'interfaccia IChannel, la classe MessageParser che implementa l'interfaccia IMessageParser ed infine la classe MessageProcessor che implementa l'interfaccia IMessaggeProcessor; poi ho applicato la strategia di design Dependency Injection ed ho estratto le tre interfacce spostando la responsabilità della loro creazione fuori dalla classe Client e all'interno della classe Server.
Vediamo come interagisce il Server con i clients:
Il metodo OnClientConnected:
public void OnClientConnected(IChannel channel)
{
Client client = new Client(channel, new MessageParser(), _messageProcessor);
_clients.Add(client);
}
Come si può vedere ogni volta che un client si connette viene creata una nuova istanza della classe MessageParser, mentre viene riutilizzato lo stesso MessageProcessor (il field _messageProcessor).
La domanda che ci si può porre a questo punto è perchè non viene usato la stessa istanza del parser per tutti i client, ma ne viene creata una nuova ogni volta?
La risposta è che il parser ha la responsabilità di comporre i messaggi che arrivano spezzati in più parti. Mi spiego meglio: i socket, in questo caso utilizzando il protocollo TCP/IP, non garantiscono che un messaggio spedito venga ricevuto in un'unica volta (maggiori dettagli in questo nel post General things to take into consideration while doing socket programming), ma possono essere necessarie più ricezioni, quindi è compito del ricevente gestire questa situazione.
Purtroppo il formato dei messaggi non prevede la lunghezza del messaggio (si basa solo su dei tag di inizio e fine) e quindi non mi è possibile fare questa composizione a livello di layer di comunicazione; questa responsabilità è stata data al parser, il quale solo una volta individuato un messaggio completo si accorge di aver finito la ricezione.
Per meglio chiarire supponiamo che il client spedisca il seguente messaggio:
<start>Messaggio di prova</end>
e il server lo riceva spezzato in due: <start>Messagg e poi separatamente io di prova</end>
La classe in grado di comprendere che quando ricevo la seconda parte ho terminato di ricevere il messaggio è il MessageParser. Quindi questo è il motivo per cui ne devo instanziare uno diverso per ogni client perchè il server ha un canale di comunicazione diverso per ogni client e i messaggi devono essere ricomposti separatamente.
Il problema è che dall'architettura che ho evidenziato questo non è chiaro, un'altra persona che prende in mano il progetto potrebbe togliere la new MessageParser() dal metodo OnClientConnected e sostituirla con un field. Un'altra strada è quella di spostare questa responsabilità al client, ma in questo caso gli aggiungerei troppo carico oltre quello che ha e dipenderebbe da un dettaglio che è più da layer di comunicazione.
Mi piacerebbe sentire il vostro parere su questo problema di architettura.