I have three types of data link: RS485, I2C and Bluetooth. Every data link have functions like connect, read and write data. On PC software I must implement application/protocols layers to work with devices. In my previous question about OOP I got the answers to use Bridge pattern or factory method but I think this can be done better. I would ask if is not better to use templates to this task. Here is my simple example how I want to use it:
// Low level datalink class
class RS485
{
public:
void send(const char *data) {
// datalink function to send data using RS485
printf("RS485: %s \n", data);
}
};
class I2C
{
public:
void send(const char *data) {
// datalink function to send data using I2C
printf("I2C: %s \n", data);
}
};
class BT
{
public:
void send(const char *data) {
// datalink function to send data using Bluetooth
printf("BT %s \n", data);
}
};
// Protocol class
template<typename Comm>
class MODBUS
{
public:
MODBUS(Comm *c) { _c = c; }
void send(const char *data) {
printf("MODBUS\n");
_c->send(data);
}
private:
Comm *_c;
};
// Protocol class
template<typename Comm>
class TCP
{
public:
TCP(Comm *c) { _c = c; }
void send(const char *data) {
printf("TCP\n");
_c->send(data);
}
private:
Comm *_c;
};
int main() {
// Modbus over I2C
I2C *i2c = new I2C();
MODBUS<I2C> *mb_i2c = new MODBUS<I2C>(i2c);
mb_i2c->send("Data ...");
// Modbus over RS485
RS485 *rs = new RS485();
MODBUS<RS485> *mb_rs = new MODBUS<RS485>(rs);
mb_rs->send("Data ...");
// Tcp over Modbus over RS485
TCP< MODBUS<RS485> > *tcp_modbus_rs = new TCP< MODBUS<RS485> >(mb_rs);
tcp_modbus_rs->send("Data ...");
return 0;
}
The main type of templates that can be implemented in C are static templates. Static templates are created at compile time and do not perform runtime checks on sizes, because they shift that responsibility to the compiler.
A template is a simple yet very powerful tool in C++. The simple idea is to pass data type as a parameter so that we don't need to write the same code for different data types. For example, a software company may need to sort() for different data types.
Bridge design pattern can be used when both abstraction and implementation can have different hierarchies independently and we want to hide the implementation from the client application.
Inheritance provides runtime abstraction. Templates are code generation tools. Because the concepts are orthogonal, they may happily be used together to work towards a common goal.
Strategy Pattern is used for Behavioural decisions, while Bridge Pattern is used for Structural decisions. Brigde Pattern separats the abstract elements from the implementation details, while Strategy Pattern is concerned making algorithms more interchangeable.
You can use mixins-from-below instead to reduce a bit the boilerplate. It doesn't differ much from your example, but you have less code and no pointers around. You can still (let me say) compose your protocol up from its parts.
Here is your snippet reworked to use them:
#include<cstdio>
// Low level datalink class
struct RS485 {
void send(const char *data) {
// datalink function to send data using RS485
printf("RS485: %s \n", data);
}
};
struct I2C {
void send(const char *data) {
// datalink function to send data using I2C
printf("I2C: %s \n", data);
}
};
struct BT {
void send(const char *data) {
// datalink function to send data using Bluetooth
printf("BT %s \n", data);
}
};
// Protocol class
template<typename Comm>
struct MODBUS: private Comm {
void send(const char *data) {
printf("MODBUS\n");
Comm::send(data);
}
};
// Protocol class
template<typename Comm>
struct TCP: private Comm {
void send(const char *data) {
printf("TCP\n");
Comm::send(data);
}
};
int main() {
// Modbus over I2C
MODBUS<I2C> mb_i2c{};
mb_i2c.send("Data ...");
// Modbus over RS485
MODBUS<RS485> mb_rs{};
mb_rs.send("Data ...");
// Tcp over Modbus over RS485
TCP< MODBUS<RS485> > tcp_modbus_rs{};
tcp_modbus_rs.send("Data ...");
}
If your parts have constructors that accept different parameters list, you can use forwarding references and templated constructors to satisfy the requirement.
As an example:
// Protocol class
template<typename Comm>
struct MODBUS: private Comm {
template<typename... T>
MODBUS(T&&... t): Comm{std::forward<T>(t)...} {}
void send(const char *data) {
printf("MODBUS\n");
Comm::send(data);
}
};
Here is the full example reworked this way:
#include<cstdio>
#include<utility>
// Low level datalink class
struct RS485 {
RS485(int) {}
void send(const char *data) {
// datalink function to send data using RS485
printf("RS485: %s \n", data);
}
};
struct I2C {
I2C(char, double) {}
void send(const char *data) {
// datalink function to send data using I2C
printf("I2C: %s \n", data);
}
};
struct BT {
void send(const char *data) {
// datalink function to send data using Bluetooth
printf("BT %s \n", data);
}
};
// Protocol class
template<typename Comm>
struct MODBUS: private Comm {
template<typename... T>
MODBUS(T&&... t): Comm{std::forward<T>(t)...} {}
void send(const char *data) {
printf("MODBUS\n");
Comm::send(data);
}
};
// Protocol class
template<typename Comm>
struct TCP: private Comm {
template<typename... T>
TCP(T&&... t): Comm{std::forward<T>(t)...} {}
void send(const char *data) {
printf("TCP\n");
Comm::send(data);
}
};
int main() {
// Modbus over I2C
MODBUS<I2C> mb_i2c{'c', .3};
mb_i2c.send("Data ...");
// Modbus over RS485
MODBUS<RS485> mb_rs{42};
mb_rs.send("Data ...");
// Tcp over Modbus over RS485
TCP< MODBUS<RS485> > tcp_modbus_rs{23};
tcp_modbus_rs.send("Data ...");
}
As a rule of thumb - don't optimize (yet). If the virtual call to send
is not the bottleneck, why bother replacing interfaces with templates that have more boilerplate code?
From your code, it doesn't seem necessary to pursue any pattern - just hard-code those classes and you'll finish the work sooner.
Edit: If you really want to chain protocols together, here's a functional way:
struct TCP
{
void onsend(const char* data) {}
};
struct MODBUS
{
void onsend(const char* data) {}
};
struct RS485
{
void onsend(const char* data) {}
};
template<typename F, typename Prot, typename... TProtocols>
auto channel(F&& f, Prot&& prot, TProtocols&&... protocols)
{
return [&](const char* data)
{
f(prot, data);
channel(f, protocols...)(data);
};
}
template<typename F, typename Prot>
auto channel(F&& f, Prot&& prot)
{
return [&](const char* data)
{
f(prot, data);
};
}
int main()
{
TCP tcp;
MODBUS modbus;
RS485 rs;
auto chan = channel([](auto&& protocol, const char* data)
{
protocol.onsend(data);
},
tcp, modbus, rs);
const char msg[] = "asdfasdf";
chan(msg);
}
Basically, you want the objects to receive message one by one, who said their types need to be related at all?
A template solution seems a bad idea in this case.
Do you really want the type of the object depend on what is "implemented on"?
Using a virtual function seems the correct approach (passing a pointer to the lower level channel as a base class pointer in the constructor).
The virtual functions approach requires the use of pointers and careful handling of lifetimes, but for that the standard solution is to use smart pointers.
#include <stdio.h>
#include <memory>
struct DataLink {
virtual void send(const char *data) = 0;
virtual ~DataLink(){}
};
typedef std::shared_ptr<DataLink> DLPtr;
struct RS485 : DataLink {
void send(const char *data) { printf("RS485: %s \n", data);}
};
struct I2C : DataLink {
void send(const char *data) { printf("I2C: %s \n", data); }
};
struct BT : DataLink {
void send(const char *data) { printf("BT %s \n", data); }
};
struct MODBUS : DataLink {
DLPtr channel;
MODBUS(const DLPtr& channel) : channel(channel) {}
void send(const char *data) {
printf("MODBUS\n");
channel->send(data);
}
};
struct TCP : DataLink {
DLPtr channel;
TCP(const DLPtr& channel) : channel(channel) {}
void send(const char *data) {
printf("TCP\n");
channel->send(data);
}
};
int main() {
DLPtr dl1(new MODBUS(DLPtr(new I2C)));
dl1->send("data ...");
DLPtr dl2(new MODBUS(DLPtr(new RS485)));
dl2->send("data ...");
DLPtr dl3(new TCP(DLPtr(new MODBUS(DLPtr(new RS485)))));
dl3->send("data ...");
return 0;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With