I'm developing a software to comunicate with a device.
The software will send commands for the device. The device has to answer using the protocol below:
<STX><STX><COMMAND>[<DATA_1><DATA_2>...<DATA_N>]<CHKSUM><ETX>
where:
<STX> is the Start of TeXt (0x55);
<COMMAND> can be 0x01 for read, 0x02 for write, etc;
<DATA> is any value;
<CHKSUM> is the checksum;
<ETX> is the End of TeXt (0开发者_JAVA技巧x04).
So, I have to validate the received data.
Then, the received data:
- cannot be empty;
- must have 3 or more characters;
- must have an header in the first two characters of the string data;
- must have a "footer" in the last character of the string data;
- must hava a valid CheckSum.
If the answer is valid, then I can handle the data. But before I'll have to extract this data from the response received.
Ok, this is a relatively easy task. Beforetime I would do it on a procedural way, using only one function and putting many if's.
Now I'm studying more about good programming practices, things seem to be getting harder to do.
To validate the device answer, is better create a class "ValidateReceivedData" for example and pass the received data in the constructor of this class? And then create a public method called "IsReceivedDataValid" that check all steps given above?
Or maybe would be better create a library with with several functions to validate the received data?
I'd like to use unit test too.
As I said before, I'm studying more to make better code. But I realize that I'm spending more time now to code than before. And there are too many questions that are arising, but in my view they seem easy to solve, but I'm not getting.
For what it's worth, I've done this sort of thing before using object-oriented design. Here's a high level possibility for your design:
ProtocolParser
class:
- Takes a
SerialPort
object, or equivalent, in the constructor and listens to it for incoming bytes - Passes received bytes to
OnByteReceived
, which implements the protocol-specific state machine (with states likeUnknown
,Stx1Received
,Stx2Received
, ...,CkSumReceived
). - After an entire good message is received, creates an object of type
Packet
, which accepts a byte list in its constructor. It then raises an eventPacketReceived
, passing thePacket
as an argument. - If a bad byte is received, it raises an event
BadDataReceived
and passes the bad data (for logging/debugging purposes, perhaps).
Packet
class:
- Takes a list/array of bytes and stores them as
Command
andData
properties. - Does not need to save the checksum, as this class is only meant to represent a valid packet.
The above classes are sufficient to implement the receive protocol. You should be able to test it by mocking a SerialPort class (i.e., the ProtocolParser
could actually take an IDataSource
instead of a SerialPort
).
You could then add a higher-level class to implement your device-specific functions, which would listen to the PacketReceived
event of the ProtocolParser
.
Of course it will better to use OOP design.
By what you explained, I'd make at least 2 classes:
Message
Executer
The message will receive the command from the device, and the Executer will handle the message.
The Message object will initiate with the device's answer. It will parse it, and hold fields as you described:
STX
COMMAND
DATA
CHKSUM
ETX
Then an Executer object will receive the Message object and do the actual execution of the message, and hold the logical code.
I would go a step further than Yochai's answer, and create the following classes:
- Command: Actually not a class, but an Enum value so you can check against Command.Read, etc., rather than just "knowing" what 0x01 and 0x02 mean.
- Message: Just a plain object (POJO/POCO/whatever) that's intended to hold a data representation of the message. This would contain the following fields:
- Command (the enum type mentioned earlier)
- Data: List of the data. Depending on how the data is represented, you might create a class for this, or you could just represent each datum as a string.
- MessageParser: this would have a function that would parse a string or text stream and create a Message object. If the text is invalid, I'd throw a customized exception (another class), which can be caught by the caller.
- MessageExecutor: This would take a Message object and perform the action that it represents.
By making the intermediate representation object (Message), you make it possible to separate the various actions you're performing. For example, if the Powers That Be decide that the message text can be sent as XML or JSON, you can create different MessageParser classes without having to mess with the logic that decides what to do with the message.
This also makes unit testing far easier, because you can test the message parser independently of the executor. First test the message parser by calling the parse function and examining the resulting Message object. Then test the executor by creating a Message object and ensuring that the appropriate action is taken.
精彩评论