Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using templates instead of bridge pattern in C++

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;
}
like image 473
Tomag Avatar asked Aug 21 '17 19:08

Tomag


People also ask

Can we use templates in C?

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.

Why do we use templates in C?

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.

Why do we need bridge design pattern?

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.

Is inheritance better than templates?

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.

What is difference between bridge and strategy pattern?

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.


3 Answers

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 ...");
}
like image 65
skypjack Avatar answered Oct 23 '22 03:10

skypjack


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?

like image 45
TheWisp Avatar answered Oct 23 '22 03:10

TheWisp


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;
}
like image 24
6502 Avatar answered Oct 23 '22 04:10

6502