C++的战略模式.实施选项

Strategy pattern in C++. Implementation options

本文关键字:选项 施选项 模式 C++      更新时间:2023-10-16

这是一个简化的例子,叫做(我希望 - 如果我错了,请纠正我) 策略模式:有一个类FileWriter,它将键值对写入文件并使用IFormatter接口的对象来格式化正在写入的文本。有不同的格式化程序实现,并且在创建格式化程序时传递FileWriter对象。这是这种模式的一个(糟糕的)实现:

#include <iostream>
#include <fstream>
#include <stdlib.h>
#include <sstream>
using namespace std;
class IFormatter {
  public:
    virtual string format(string key, double value) = 0;
};
class JsonFormatter : public IFormatter {
  public:
    string format(string key, double value) { 
        stringstream ss;
        ss << """+key+"": " << value;
        return ss.str();
    }
};
class TabFormatter : public IFormatter {
  public:
    string format(string key, double value) { 
        stringstream ss;
        ss << key+"t" << value;
        return ss.str();
    }
};
class FileWriter {
  public:  
    FileWriter(string fname, IFormatter& fmt):fmt_(fmt)
    {
        f_.open(fname.c_str(), ofstream::out);
    }
    void writePair(string key, double value)
    {
        f_ << fmt_.format(key, value);
    }
  private:
    ofstream f_;
    IFormatter& fmt_;
};    

可以看出,这种方法的主要缺点是不可靠的 - 传递给FileWriter的对象Formatter在整个FileWriter生命周期内必须存在,因此像FileWriter("test.txt", JsonFormatter())这样的调用直接导致SegFault

在这方面,我想讨论一下实现这种方法的其他选择,这些方法具有"易于使用"和简单性的要求:

  • 可以在创建文件编写器时传递新的格式化程序,或者
  • 可以传递和使用现有的格式化程序。

我想出了下面描述的几种替代方案及其缺点(IMO):

  • 模板:FileWriter作为模板类,将精确FormatterClass作为参数;缺点:调用起来很难看:FileWriter<JsonFormatter>("test.txt", JsonFormatter()) - 在这里,JsonFormatter被键入两次。
  • 原始指针: FileWriter("test.txt", new JsonFormatter()) ;缺点 - 谁应该删除格式化程序对象? FileWriter ?如果是,则传递现有格式化程序的地址将导致FileWriter对象尝试删除格式化程序后SegFault
  • 共享指针: FileWriter("test.txt", dynamic_pointer_cast<IFormatter*>(shared_ptr<JsonFormatter*>(new JsonFormatter())) ;缺点:调用起来很难看,再说一次,如果在创建文件编写器之前创建了格式化程序怎么办?

这里的最佳实践是什么?

更新

为了响应建议使用std::function的答案 - 如果格式化程序可以存储状态(例如,精度)并具有其他方法(例如 getHeader() ,例如 CSV 文件,该怎么办?

此外,无法按值存储IFormatter,因为它是一个抽象类。

最简单的解决方案是使用:

JsonFormatter formatter;
FileWriter writer("test.txt", formatter);
// Use writer.

另一个更好的选择是 clone()功能 在 IFormatter .然后,FileWriter可以克隆对象,获取克隆的所有权并在其析构函数中删除它。

 class IFormatter {
   public:
     virtual string format(string key, double value) = 0;
     virtual IFormatter* clone() const = 0;
 };

 class FileWriter {
   public:  
     FileWriter(string fname, IFormatter const& fmt):fmt_(fmt.clone())
     {
         f_.open(fname.c_str(), ofstream::out);
     }
     ~FileWriter()
     {
        delete fmt_;
     }
     void writePair(string key, double value)
     {
         f_ << fmt_->format(key, value);
     }
   private:
     ofstream f_;
     IFormatter* fmt_;
 };    

现在,您也可以使用临时对象调用FileWriter

FileWriter writer("test.txt", JsonFormatter());
// Use writer.

模板:将FileWriter作为模板类,该类将确切的FormatterClass作为参数; 缺点:调用起来很难看:FileWriter("test.txt",JsonFormatter()) - 在这里,JsonFormatter被键入了两次。

更多模板!

template<class Formatter> 
FileWriter<Formatter> makeFileWriter(const std::string& filename, const Formatter& formatter) 
{return FileWriter<Formatter>(filename, formatter);}

哒! 现在它就像

auto fileWriter = makeFileWriter("test.txt", JSonFormatter());`

这就是标准库所做的(例如 std::shared_ptr可以采取删除程序)。 Formatter必须是可复制的,显然表达式f << fmt(key, value)必须格式正确。

class FileWriter {
public:
    template<typename Formatter>
    FileWriter(std::string fname, Formatter fmt) :
        fmt(fmt)
    {
        f.open(fname.c_str(), std::ofstream::out);
    }
    void writePair(std::string key, double value)
    {
        f << fmt(key, value);
    }
private:
    std::ofstream f;
    std::function<std::string (std::string, double)> fmt;
};

如果您的界面中需要多个函数,则可以使用原始方法,但使用 std::unique_ptrstd::shared_ptr 控制格式化程序的生存期(请记住使析构函数成为虚拟函数)。

struct Formatter
{
    virtual ~Formatter() {}
    virtual std::string format(std::string key, double value) = 0;
};
class FileWriter {
public:
    FileWriter(std::string fname, std::unique_ptr<Formatter>&& fmt_)
    {
        if (!fmt_)
        {
            throw std::runtime_error("Formatter cannot be null");
        }
        f.open(fname.c_str(), std::ofstream::out);
        fmt = std::move(fmt_); // strong exception safety guarantee
    }
    void writePair(std::string key, double value)
    {
        f << fmt->format(key, value);
    }
private:
    std::ofstream f;
    std::unique_ptr<Formatter> fmt;
};

如果要将现有Formatter传递给FileWriter,则需要将其复制/移动到智能指针中以转移所有权,或者需要将其包装在格式化程序接口中。

class FormatterProxy : public Formatter
{
public:
    FormatterProxy(Formatter& fmt) :
        fmt(fmt)
    {
    }
    std::string format(std::string key, double value)
    {
      return fmt.format(key, value);
    }
private:
  Formatter& fmt;
};

这仍然存在您试图避免的生命周期管理问题。但是,我看不出有什么办法可以解决这个问题。要么你把Formatter的唯一或共享所有权交给FileWriter,要么把生命周期管理权交给调用者(如果你重视效率而不是安全,这是一种完全有效的方法)。

using IFormatter - std::function<std::string(std::string,double)>;

您的格式化程序应该是一个函数,而不是一个接口。

如果调用方想要保证生存期,可以使用std::ref,如果他们想要模糊的生存期,可以使用包装共享 ptr,或者按值传递。

如果你想要一个更丰富的接口,你可以拿一堆这样的接口,或者写一个一堆这样的类(通过继承或手动编写notstd::functions)。

按值存储IFormatter fmt;,使用fmt(a,b)而不是fmt.format(a,b)(DRY! 如果需要,客户端代码可以使其成为引用或智能语义。

继承作为实现细节,而不是驱动你的设计,是自由的。