How can I reduce bloat in my callback based serial communication?
https://softwareengineering.stackexchange.com/questions/358610
-
20-01-2021 - |
Domanda
I'm communicating with a device that's connected to the computer via com port. The device accepts certain predefined commands in order to interact with it. I'm essentially creating a more abstract API on top of the one provided by the device.
I encapsulated the charactersistics of the Command
s I use, so that I can just throw those objects at my Connection
class, which handles the com port.
connection.Send(new FooCommand());
connection
then
- shoves the
ICommand
into an internally usedSerialPort
object - listens for
SerialPort.DataReceived
for the expected result - sends the next
ICommand
(if there is one)
So far so good.
For some ICommand
, the device returns meaningful values (and does not simply acknowledge the reception). Those can be handled in optional Action<>
callbacks, that are passed to the constructor.
connection.Send(new BarCommand(response => Console.WriteLine(response)));
This works ok to just get a value, but becomes complicated for more complex communication that invoves multiple instructions being sent that depend on each other. To illustrate, let's assume the returned value of one command might be necessary to send another one.
The trivial solution is to put the second call into the response callback of the first one.
connection.Send(new BarCommand(response => connection.Send(new BazCommand(response)));
This quickly becomes messy. A more readable solution could be to store the response in a variable.
var baz = new BazCommand();
connection.Send(new BarCommand(response => baz.Parameter = response));
connection.Send(baz)
Now however, BazCommand
has to be mutable, because it will be completed (with the necessary value for its parameter
property) only after it is passed to the connection.Send()
method. It also means that connection.Send()
has to evaluate its parameter lazily.
Even in this simple example, the order of operations is a bit obscured already.
It gets worse if sending the BazCommand
at all should be dependent on the response. Lambdas are not well suited for such branching code. A method can be used.
connection.Send(new BarCommand(responseHandler));
private void responseHandler(bool response) {
if(response) connection.Send(baz)
This leads to dozens of methods like responseHandler
that essentially handle the different state transitions of my object (not a bad thing in itself), which bloats the code and reduces readability.
I'm not too experienced with C# and need some guidance how to improve the communication code.
Is this a use case for async
/await
?
Would it allow me to write code like the following?
bool response = connection.Send(new BarCommand);
/* stop here until response is actually set to a value comming from the device */
connection.Send(new BazCommand(response))
or
bool response = connection.Send(new BarCommand);
/* stop here until response is actually set to a value comming from the device */
if (response) connection.Send(new BazCommand())
Soluzione
I'm not too experienced with C# and need some guidance how to improve the communication code. Is this a use case for async/await? Would it allow me to write code like the following?
This is indeed a very applicable scenario for using async/await.
public static class CommandHelper
{
public static async Task<T> SendAsync<T>(this SerialConnection connection, ICommand command)
{
var tcs = new TaskCompletionSource<T>();
command.DataReceived += (sender, data) => tcs.SetResult((T)data);
connection.Send(command);
return await tcs.Task;
}
}
With this you'll be able to 'await' every serial call and chain them together like so
var welcomeResponse = await connection.SendAsync<WelcomeResponse>(new WelcomeCommand());
var registrationReply = await connection.SendAsync<RegistrationReply>(new RegistrationCommand(welcomeResponse.Name));
You get the idea. Async/await really cleans up these scenarios where you'd otherwise wait for data using an event handler.