3

I am developing a C++ application which should handle multiple communication protocols (Ethernet, Serial, etc.). Each of the communication protocols is handled as a specific class. In order to expose as little as possible information about the internal structure and organization of the said classes and protocols, I would like to somehow wrap all of this functionality and provide somewhat generic API for sending data over a selected protocol.

Basically, what API should provide (the parameters are not restricted to this, but is the general idea):

bool sendData(uint8_t* buffer, const size_t& bufferSize);

void receiveData(uint8_t* dataBuffer, size_t& bufferSize);

What is the best way to create a generic API for the said functionality, and if possible involve some design pattern?

Regards.

thealchemist
  • 421
  • 4
  • 12
  • This seems like a pretty straightforward use case for inheritance from an abstract `class`. Have you tried that? – Stephen Newell May 18 '20 at 13:59
  • @StephenNewell well yes, i have been looking into the factory design pattern, but I am not 100% sure if that is, by design and usage of that pattern, correct way... – Robert Šandor May 18 '20 at 14:03

1 Answers1

5

What is the best way to create a generic API for the said functionality, and if possible involve some design pattern?

The Strategy Pattern looks suitable in this scenario.

First, define an interface for all your distinct communication startegies, Communication:

class Communication {
public:
   virtual ~CommunicationStrategy() = default;
   virtual bool sendData(uint8_t* buffer, const size_t& bufferSize) = 0;
   virtual void receiveData(uint8_t* dataBuffer, size_t& bufferSize) = 0;
};

Then, your concrete implementations – i.e., strategies – should derive from this interface:

class EthernetCommunication: public Communication {
public:
   // ...
   bool sendData(uint8_t*, const size_t&) override;
   void receiveData(uint8_t*, size_t&) override;
};

class SerialCommunication: public Communication {
public:
   // ...
   bool sendData(uint8_t*, const size_t&) override;
   void receiveData(uint8_t*, size_t&) override;
};

class CarrierPigeon: public Communication {
public:
   // ...
   bool sendData(uint8_t*, const size_t&) override;
   void receiveData(uint8_t*, size_t&) override;
};

The client code will work with a (pointer to) Communication – i.e., the interface – rather than directly with a particular implementation like EthernetCommunication, SerialCommunication, or CarrierPigeon. Thus, the code follows the "program to an interface, not to an implementation" advice. For example, you may have a factory function like:

std::unique_ptr<Communication> CreateCommunication();

This factory function returns one of the strategies above. Which strategy to return can be determined at run time.

std::unique_ptr<Communication> com = CreateCommunication();
// send data regardless of a particular communication strategy
com->sendData(buffer, bufferSize);

This way, the code above isn't coupled to any particular implementation, but only to the interface Communication which is common to all the different possible communication strategies.


If the different communication strategies don't need per-instance data, just having two callbacks instead of an object will do:

using data_sender_t = bool (*)(uint8_t*, const size_t&);
using data_receiver_t = void (*)(uint8_t*, size_t&);

// set these function pointers to the strategy to use
data_sender_t data_sender;
data_receiver_t data_receiver;
JFMR
  • 23,265
  • 4
  • 52
  • 76
  • This seems that it will be a good approach. I think I'll need to have more than one callback function per instance. edit: Is it possible to add functionality which is not common for both classes? – Robert Šandor May 18 '20 at 14:14
  • @RobertŠandor The object-based approach is more general than the callback-based one. The latter you generally use when you only encapsulate behavior. – JFMR May 18 '20 at 14:19
  • 1
    I used this pattern for exactly this purpose, it works well: https://public.msli.com/lcs/muscle/html/classmuscle_1_1DataIO.html – Jeremy Friesner May 18 '20 at 15:23
  • @RobertŠandor you can implement additional member functions in a concrete implementation that aren't part of the `Communication` interface. However, if the client code works with this implementation through the (common) interface, it can only call the member functions exposed by the interface. – JFMR May 19 '20 at 09:07