6

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;
}
Tomag
  • 101
  • 7

6 Answers6

1

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?

TheWisp
  • 306
  • 1
  • 6
  • But how you want add function like "Tcp over Modbus over RS485" using OOP? My example is very simple but I have in real code much more protocols and every protocol have also a lot application class and some protocols must use another protocols so I need some design patter to do this in the best way which I can. I put example which as I think in the best way describe what I need and I do not want to put here more code which will be not readable. – Tomag Aug 21 '17 at 20:10
  • Provided another solution – TheWisp Aug 21 '17 at 20:30
1

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 ...");
}
skypjack
  • 49,335
  • 19
  • 95
  • 187
  • Nice tip but cannot be used in real code because I must first create object like I2C, RS485 or TCP and set specific to every protocol parameters. I cannot do this inside class because parameters for I2C and for RS485 of course will be different. So in real code will be like this: ` I2C *i2c = new I2C(); i2c->SetAddr(0x33); i2c->setTimeout(444); MODBUS *mb_i2c = new MODBUS(i2c); mb_i2c->send("Data ...");` – Tomag Aug 21 '17 at 20:18
  • @Tomag Well, you can still pass different parameters to them during construction. Let me put an example for that in the answer. – skypjack Aug 21 '17 at 20:19
  • yes I can in some situation but sometimes I must change parameters inside class which use low level datalink. For example when I want receive huge data I must set bigger timeout but only for this one reading so must be possible to change parameters. – Tomag Aug 21 '17 at 20:23
  • @Tomag Added a few more details. Let me know if it works for you. I'm at your disposal if you want more details. As you can see, you can create different instances with different sets of parameters now. – skypjack Aug 21 '17 at 20:25
  • @Tomag what you are describing sounds like strategy pattern to me. I do not see a reason for using templates here... – Artemy Vysotsky Aug 21 '17 at 20:46
  • @skypjack nice solution, now I must think if will be enough :) Thanks a lot! – Tomag Aug 21 '17 at 20:56
  • @Tomag You're welcome. Let me know if you need something else. – skypjack Aug 21 '17 at 20:57
1

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;
}
6502
  • 112,025
  • 15
  • 165
  • 265
  • Check last example in my code "Tcp over Modbus over RS485". How you want to implement this using base class and virtual functions? Using templates is very simple to implement but using base class not. – Tomag Aug 21 '17 at 20:46
  • @Tomag: I've added an example. Note that a nice property of using virtual function is that if you make a library the caller can just interface a base object without knowing (or caring) if the connection is using TCP or whatever and you can write a single function that will work with a type of connection or another depending say on a configuration file. Using templates the way you did this cannot be done because the decisions are taken at compile time. – 6502 Aug 21 '17 at 21:43
  • Better to take smart pointers directly instead of transforming raw pointer into smart pointers under the hoods. – Jarod42 Aug 21 '17 at 21:45
  • @Jarod42: Point taken. When possible I normally prefer to use intrusive reference counted pointers as they are more efficient and have less semantic problems (there's no risk in passing around naked object addresses). It's instead indeed very dangerous with shared_ptr instead so I wrapped them asap in this example as you suggested. – 6502 Aug 21 '17 at 21:57
  • @6502 I does not explain that class like I2C, RS485 and BT is not so easy as I write in example. These classes inherit from another class so if you want to make inheritance from DataLink will not be so easy and you have 2 solutions: use some adapter class or use multi inheritance. So for me using templates is the easiest solution at the moment. – Tomag Aug 22 '17 at 06:42
0

Whereas template is possible, using polymorphism classes is not more complicated:

class Sender
{
public:
    virtual ~Sender() = default;
    virtual void send(const char *data) = 0;
};

// Low level datalink class
class RS485 : public Sender
{
public:
    void send(const char *data) override {
        // datalink function to send data using RS485
        printf("RS485: %s \n", data);
    }
};

class I2C: public Sender
{
public:
    void send(const char *data) override {
        // datalink function to send data using I2C
        printf("I2C: %s \n", data);
    }
};

class BT : public Sender
{
public:
    void send(const char *data) override {
        // datalink function to send data using Bluetooth
        printf("BT %s \n", data);
    }
};

// Protocol class
class MODBUS : public Sender
{
public:
    explicit MODBUS(Sender* sender) : sender(sender) {}

    void send(const char *data) override {
        printf("MODBUS\n");
        sender->send(data);
    }
private:
    Sender *sender;
};

// Protocol class
class TCPS : public Sender
{
public:
    expolicit TCP(Sender* sender) : sender(sender) {}

    void send(const char *data) override {
        printf("TCP\n");
        sender->send(data);
    }
private:
    Sender* sender;
};

int main() {
    // Modbus over I2C
    I2C i2c;
    MODBUS mb_i2c(&i2c);
    mb_i2c.send("Data ...");

    // Modbus over RS485
    RS485 rs;
    MODBUS mb_rs(&rs);
    mb_rs.send("Data ...");

    // Tcp over Modbus over RS485
    TCP tcp_modbus_rs(mb_rs);
    tcp_modbus_rs.send("Data ...");
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • but what if RS485, I2C and BT inherit from another class ? I give simple example but in real code is much more complicated and using this solution I must add adapter class or use multi inherit. – Tomag Aug 22 '17 at 10:45
0
// strong typedef:
struct sink:std::function<void(char const*)>{
  using std::function<void(char const*)>::function; // inherit ctors
};
using step=std::function<void(char const*, sink)>;

inlne sink operator|( step s, sink e ){
  return [=](char const* data){
    s( data, e );
  };
}
inlne step operator|( step one, step two ){
  return [=](char const* data, sink end){
    two( data, one|end );
  };
}

Now we can chain.

step fake_step(sts::string name){
  return [name](char const* data, sink s){
    std::cout<<name<<": \n";
    s(data);
  };
}
auto tcp=fake_step("tcp");
auto modbus=fake_step("modbus");

sink fake_sink(std::string name){
  return [name](char const* data){
    std::cout << name << ": " << data << "\n";
  };
}
auto ABC=fake_sink("ABC");
auto XYZ=fake_sink("XYZ");


auto tcp_over_xyz = tcp|XYZ;

this uses type erasure; crtp or koenig operators can remove that erasure. Lots more boilerplate, and only do that if you first profile a performance hit.

This uses std::function; you can compose other ways.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

It's a bit late. The question is about static polymorphism vs dynamic polymorphism. There are arguments that static polymorphism is better - more type-safe and also may lead to better performance.

The idea of pure virtual function and abstract classes is to enforce an interface. Here, datalink serves that purpose. With concepts in C++20, it's easy now to implement a compile-time interface.

Here is my take of the above (mostly the same, only with the added datalink concept:

#include <concepts>
#include <iostream>

template <typename T>
concept datalink = requires(T&& t) {
  { t.send(std::declval<const char*>()) } -> std::same_as<void>;
};

class rs485 {
public:
  void send(char const *data) { std::cout << "rs485: " << data << '\n'; }
};

class i2c {
public:
  void send(char const *data) { std::cout << "i2c: " << data << '\n'; }
};

class bt {
public:
  void send(char const *data) { std::cout << "bt: " << data << '\n'; }
};

template <datalink Comm> 
class modbus {
private:
  Comm m_comm;

public:
  modbus(Comm const &comm) : m_comm{comm} {}
  void send(char const *data) {
    std::cout << "modbus over ";
    m_comm.send(data);
  }
};

template <datalink Comm> 
class tcp {
private:
  Comm m_comm;

public:
  tcp(Comm const &comm) : m_comm{comm} {}
  void send(char const *data) {
    std::cout << "tcp over ";
    m_comm.send(data);
  }
};

int main() {
  i2c i2c_inst{};
  modbus<i2c> mb_i2c_inst{i2c_inst};
  mb_i2c_inst.send("Data...");

  rs485 rs485_inst{};
  modbus<rs485> mb_rs485_inst{rs485_inst};
  mb_rs485_inst.send("Data...");

  tcp<modbus<rs485>> tcp_mb_rs485_inst{mb_rs485_inst};
  tcp_mb_rs485_inst.send("Data...");
}

Arnab De
  • 402
  • 4
  • 12