【问题标题】:Strategy pattern in C++. Implementation optionsC++ 中的策略模式。实施选项
【发布时间】:2016-03-12 01:46:14
【问题描述】:

这是所谓的简化示例(我希望 - 如果我错了,请纠正我)策略 模式:有一个类 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_;
};    

可以看出,这种方法的主要缺点是它的不可靠性 - 传递给FileWriterFormatter 对象必须在FileWriter 的整个生命周期中都存在,因此像@ 这样的调用987654328@直接转至SegFault

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

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

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

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

这里的最佳做法是什么?

更新

针对建议使用 std::function 的答案 - 如果 Formatter 可以存储状态(例如精度)并具有其他方法(例如 getHeader())用于 CSV 文件怎么办?

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

【问题讨论】:

    标签: c++ templates pointers shared-ptr strategy-pattern


    【解决方案1】:

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

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

    另一个更好一点的选择是在IFormatter 中有一个clone() 函数。然后,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.
    

    【讨论】:

    • 虽然这个解决方案可以工作,但在我看来,所描述的设计更像是一种通用方法,应该由语言/标准库功能支持(或需要不同的设计/模式),而不是要求程序员通过为他的类编写clone() 方法来显式处理它。
    【解决方案2】:

    模板:将 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());`
    

    【讨论】:

    • 感谢您的回答!除非我找到合适的解决方案,否则我现在仍会尽量避免使用模板。
    【解决方案3】:

    这是标准库所做的(例如std::shared_ptr 可以带一个删除器)。 Formatter 必须是可复制构造的,显然f &lt;&lt; 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,要么将生命周期管理权交给调用者(如果您重视效率而不是安全,这是一种非常有效的方法)。

    【讨论】:

    • 与@Yakk 的答案相同的评论 - 如果IFormatter 必须携带状态(例如,双精度)或可能有其他方法(例如,getHeader() 用于 CSV 文件格式)怎么办?应该有不同的设计吗?
    • 只要Formatter 是可复制构造的,它就没有理由不能携带状态。如果您需要其他方法,您可以传递一个实现 operator() 的格式化程序,或者将您的格式化程序包装在一个 lambda 中:[formatter](std::string s, double d) { return formatter.format(s, d); }(这里,要么使 formatconst 或 lambda mutable)。跨度>
    • @peetonn 我想我知道你的意思 w.r.t.额外的方法。我已经更新了我的答案。如果有帮助,请告诉我。
    【解决方案4】:
    using IFormatter - std::function<std::string(std::string,double)>;
    

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

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

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

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

    继承作为实现细节,而不是驱动您的设计,正在释放。

    【讨论】:

    • 为什么它应该是一个函数?如果格式化程序应该保存一些状态,例如在格式化程序构造期间指定的双精度,该怎么办?
    • @peet std 函数可以做到这一点。
    • 不可能按值存储IFromatter - 因为它是一个抽象类。关于使用 std::function 我不认为它适用于这种情况 - 格式化程序也可以有其他方法(在我更复杂的情况下,格式化程序有方法 getHeader() 例如返回 CSV 文件的标题)。
    • @peetonn 我的IFormatter 按价值工作。你的没有。这是您的解决方案的问题,因此您的终身管理令人头疼。如果您想摆脱解决方案中的生命周期管理难题,请停止将指针放入接口:指针(和引用)暗示生命周期管理难题。您的实际用例(未反映在 OP 中)需要更多的工作才能使用类型擦除,但它是非常可行的。你认为std::function 不能存储状态是……不正确的。 std::function 的概括解决了您的一般问题。
    猜你喜欢
    • 2011-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多