【问题标题】:Using templates instead of bridge pattern in C++在 C++ 中使用模板而不是桥接模式
【发布时间】:2017-08-21 19:52:10
【问题描述】:

我有三种类型的数据链路:RS485、I2C 和蓝牙。每个数据链路都具有连接、读取和写入数据等功能。在 PC 软件上,我必须实现应用程序/协议层才能与设备一起使用。 在我之前关于 OOP 的问题中,我得到了使用桥接模式或工厂方法的答案,但我认为这可以做得更好。 我会问是否更好地使用模板来完成这项任务。这是我想如何使用它的简单示例:

// 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;
}

【问题讨论】:

  • 为什么你认为这比使用带有纯虚拟send 方法的基类更好?
  • 因为很难做出“开始” :) 检查我的代码中的最后一个示例“Tcp over Modbus over RS485”
  • [OT]:你有内存泄漏,不需要你的新的。

标签: c++ templates design-patterns generic-programming


【解决方案1】:

您可以使用 mixins-from-below 来减少一些样板。它与您的示例没有太大区别,但是您的代码更少,也没有指针。您仍然可以(让我说)组合您的协议,从它的各个部分开始。
这是您的 sn-p 重新设计以使用它们:

#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 ...");
}

如果您的部分具有接受不同参数列表的构造函数,则可以使用转发引用和模板构造函数来满足要求。
举个例子:

// 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);
    }
};

这是以这种方式修改的完整示例:

#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 ...");
}

【讨论】:

  • 很好的提示,但不能在实际代码中使用,因为我必须首先创建 I2C、RS485 或 TCP 等对象,并针对每个协议参数进行设置。我不能在课堂上这样做,因为 I2C 和 RS485 的参数当然会有所不同。所以在实际代码中会是这样的:` I2C *i2c = new I2C(); i2c->SetAddr(0x33); i2c->setTimeout(444); MODBUS *mb_i2c = 新 MODBUS(i2c); mb_i2c->send("数据...");`
  • @Tomag 好吧,你仍然可以在构建过程中向它们传递不同的参数。让我在答案中举一个例子。
  • 是的,在某些情况下我可以,但有时我必须更改使用低级数据链路的类内的参数。例如,当我想接收大量数据时,我必须设置更大的超时时间,但仅针对这一读数,因此必须可以更改参数。
  • @Tomag 添加了更多细节。请让我知道这对你有没有用。如果您想了解更多详情,我随时为您服务。如您所见,您现在可以使用不同的参数集创建不同的实例。
  • @Tomag 你所描述的对我来说听起来像是策略模式。我没有看到在这里使用模板的理由......
【解决方案2】:

根据经验 - 不要优化(暂时)。如果对send 的虚拟调用不是瓶颈,为什么还要用具有更多样板代码的模板替换接口?

从您的代码来看,似乎没有必要追求任何模式 - 只需对这些类进行硬编码,您就会更快地完成工作。

编辑:如果您真的想将协议链接在一起,这是一种功能性方式:

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);
}

基本上,您希望对象一个接一个地接收消息,谁说它们的类型根本需要相关?

【讨论】:

  • 但是您希望如何使用 OOP 添加诸如“Tcp over Modbus over RS485”之类的功能?我的示例非常简单,但是在实际代码中我有更多的协议,每个协议也有很多应用程序类,并且一些协议必须使用另一个协议,所以我需要一些设计模式以尽可能最好的方式做到这一点。我放了一个我认为最好的例子来描述我需要什么,我不想在这里放更多不可读的代码。
  • 提供了另一种解决方案
【解决方案3】:

在这种情况下,模板解决方案似乎是个坏主意。

您真的希望对象的 类型 取决于“实现”的对象吗?

使用虚函数似乎是正确的方法(将指向较低级别通道的指针作为构造函数中的基类指针传递)。

虚函数方法需要使用指针并小心处理生命周期,但为此标准解决方案是使用智能指针。

#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;
}

【讨论】:

  • 检查我的代码“Tcp over Modbus over RS485”中的最后一个示例。你想如何使用基类和虚函数来实现它?使用模板很容易实现,但使用基类则不然。
  • @Tomag:我添加了一个示例。请注意,使用虚函数的一个很好的特性是,如果您创建一个库,则调用者可以只连接一个基础对象,而无需知道(或关心)连接是否使用 TCP 或其他任何东西,您可以编写一个可以使用的单个函数连接类型或其他取决于配置文件的说法。无法按照您的方式使用模板,因为这些决定是在编译时做出的。
  • 最好直接采用智能指针,而不是在底层将原始指针转换为智能指针。
  • @Jarod42:点了。如果可能,我通常更喜欢使用侵入式引用计数指针,因为它们更有效并且语义问题更少(传递裸对象地址没有风险)。相反,shared_ptr 确实非常危险,所以我按照你的建议在这个例子中尽快将它们包装起来。
  • @6502 我没有解释像 I2C、RS485 和 BT 这样的类并不像我在示例中写的那么容易。这些类从另一个类继承,所以如果你想从 DataLink 继承不会那么容易,你有两个解决方案:使用一些适配器类或使用多重继承。所以对我来说,使用模板是目前最简单的解决方案。
【解决方案4】:

虽然模板是可能的,但使用多态类并不复杂:

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 ...");
}

【讨论】:

  • 但是如果 RS485、I2C 和 BT 从另一个类继承呢?我举了一个简单的例子,但在实际代码中要复杂得多,使用这个解决方案我必须添加适配器类或使用多继承。
【解决方案5】:
// 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 );
  };
}

现在我们可以链接了。

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;

这使用类型擦除; crtp 或 koenig 操作员可以删除该擦除。更多样板,只有在您首先分析性能影响时才这样做。

这使用 std::function;你可以用其他方式作曲。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-21
    • 2021-06-14
    • 2019-03-03
    • 2023-01-31
    • 2018-05-27
    • 2013-05-31
    • 2011-07-21
    • 1970-01-01
    相关资源
    最近更新 更多