Strategy pattern in C++. Implementation options

不羁岁月 提交于 2019-12-21 05:41:06

问题


Here's a simplified example of what is called (I hope - please, correct me if I'm wrong) Strategy pattern: there's a class FileWriter which writes key-value pairs to a file and uses object of IFormatter interface for formatting text being written. There are different formatters implementations and formatter object is passed when FileWriter is created. Here's one (bad) implementation of such pattern:

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

As can be seen, the main drawback of such approach is it's unreliability - Formatter object passed to FileWriter has to exist during whole FileWriter's lifetime, thus calls like FileWriter("test.txt", JsonFormatter()) lead directly to SegFault.

In this regard, I'd like to discuss what could be the other options for implementing such an approach with "easy-to-use" and simplicity requirements:

  • either new formatter can be passed when file writer is created, or
  • existing formatter can be passed and used.

I came up with several alternatives described below with their drawbacks (IMO):

  • templates: having FileWriter as a template class which takes exact FormatterClass as an argument; drawback: ugly to call: FileWriter<JsonFormatter>("test.txt", JsonFormatter()) - here, JsonFormatter is typed twice.
  • raw pointers: FileWriter("test.txt", new JsonFormatter()); drawback - who should delete formatter object? FileWriter? if yes, then passing an address of existing formatter will lead to SegFault once FileWriter object attempts to delete formatter.
  • shared pointers: FileWriter("test.txt", dynamic_pointer_cast<IFormatter*>(shared_ptr<JsonFormatter*>(new JsonFormatter())); drawback: ugly to call, and again, what if formatter was created before creating file writer?

What would be the best practices here?

UPDATE

In response to answers that suggested to use std::function - What if Formatter may store a state (say, precision) and have additional methods, like getHeader(), for instance, for CSV files?

Additionaly, storing IFormatter by value isn't possible since it's an abstract class.


回答1:


The simplest solution is to use:

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

The other option that is a little bit better is to have a clone() function in IFormatter. Then, FileWriter can clone the object, take ownership of the clone and delete it in its destructor.

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

Now, you can call FileWriter with temporary object too.

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



回答2:


templates: having FileWriter as a template class which takes exact FormatterClass as an argument; drawback: ugly to call: FileWriter("test.txt", JsonFormatter()) - here, JsonFormatter is typed twice.

More templates!

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

Ta da! Now it's as simple as:

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



回答3:


This is what the standard library does (e.g. std::shared_ptr can take a deleter). Formatter must be copy constructible, and obviously the expression f << fmt(key, value) must be well-formed.

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

If you need more than one function in your interface, you can use your original approach but control the lifetime of the formatter with std::unique_ptr or std::shared_ptr (remember to make the destructor virtual).

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

If you want to pass an existing Formatter to the FileWriter, you either need to copy/move it into a smart pointer to transfer ownership, or you need to wrap it in the formatter interface.

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

This still has the lifetime management issue you are trying to avoid. However, I do not see any way around this. Either you give unique or shared ownership of the Formatter to the FileWriter, or you leave lifetime management in the hands the caller (which is a perfectly valid approach if you value efficiency over safety).




回答4:


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

Your formatter should be a function, not an interface.

Callers can use std::ref if they want to guarantee lifetime, wrap a shared ptr if they want nebulous lifetime, or pass by-value.

If you want a richer interface, you can either take a pile of such, or write a class that is a pile of such (either via inheritance or by writing notstd::functions manually).

Store IFormatter fmt; by-value, use fmt(a,b) instead of fmt.format(a,b) (DRY!). Client code can make it ref or smart semantics if it wants.

inheritance as an implementation detail, instead of driving your design, is freeing.



来源:https://stackoverflow.com/questions/35952765/strategy-pattern-in-c-implementation-options

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!