Паттерн стратегии в C++. Варианты реализации

Вот упрощенный пример того, что называется (надеюсь — поправьте меня, если я ошибаюсь) паттерном Стратегия: есть класс 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_;
};    

Как видно, основным недостатком такого подхода является его ненадежность - Formatter объект, переданный FileWriter, должен существовать в течение всего времени жизни FileWriter, поэтому вызовы типа FileWriter("test.txt", JsonFormatter()) ведут непосредственно к SegFault.

В связи с этим я хотел бы обсудить, какие могут быть другие варианты реализации такого подхода с требованиями «простоты в использовании» и простоты:

  • либо новый форматер может быть передан при создании файла записи, либо
  • существующий форматер может быть передан и использован.

Я придумал несколько альтернатив, описанных ниже, с их недостатками (IMO):

  • шаблоны: наличие FileWriter в качестве класса шаблона, который принимает точное значение FormatterClass в качестве аргумента; недостаток: некрасиво называть: FileWriter<JsonFormatter>("test.txt", JsonFormatter()) - здесь JsonFormatter набирается дважды.
  • необработанные указатели: FileWriter("test.txt", new JsonFormatter()); недостаток — кто должен удалять объект форматирования? FileWriter? если да, то передача адреса существующего средства форматирования приведет к SegFault после того, как FileWriter объект попытается удалить средство форматирования.
  • общие указатели: FileWriter("test.txt", dynamic_pointer_cast<IFormatter*>(shared_ptr<JsonFormatter*>(new JsonFormatter())); недостаток: некрасиво вызывать, и опять же, что, если средство форматирования было создано до создания средства записи файлов?

Каковы были бы лучшие практики здесь?

ОБНОВЛЕНИЕ

В ответ на ответы, которые предложили использовать std::function - Что, если Formatter может хранить состояние (скажем, точность) и иметь дополнительные методы, такие как getHeader(), например, для файлов CSV?

Кроме того, сохранение IFormatter по значению невозможно, так как это абстрактный класс.


person peetonn    schedule 12.03.2016    source источник


Ответы (4)


Самое простое решение — использовать:

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.
person R Sahu    schedule 12.03.2016
comment
в то время как это решение будет работать, действительно, описанный дизайн кажется мне больше похожим на общий подход, который должен поддерживаться возможностями языка/стандартной библиотеки (или требовать другого дизайна/шаблона), а не требовать, чтобы программист явно обрабатывал его с помощью написание clone() методов для своих классов. - person peetonn; 14.03.2016

шаблоны: наличие 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());`
person Mooing Duck    schedule 12.03.2016
comment
спасибо за Ваш ответ! Я все же постараюсь пока воздержаться от использования шаблонов, если только не найду подходящее решение. - person peetonn; 14.03.2016

Это то, что делает стандартная библиотека (например, 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_ptr или std::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, либо оставляете управление жизненным циклом в руках вызывающей стороны (что является вполне допустимым подходом, если вы цените эффективность выше безопасности).

person Joseph Thomson    schedule 12.03.2016
comment
тот же комментарий, что и для ответа @Yakk - что, если IFormatter должен нести состояние (скажем, точность для двойного числа) или может иметь дополнительные методы (например, getHeader() для форматирования файлов CSV)? Должен ли быть другой дизайн? - person peetonn; 14.03.2016
comment
Пока Formatter является копируемым, нет никаких причин, по которым он не может нести состояние. Если вам нужны дополнительные методы, вы можете либо передать средство форматирования, реализующее operator(), либо обернуть ваше средство форматирования лямбдой: [formatter](std::string s, double d) { return formatter.format(s, d); } (здесь либо сделайте format const, либо лямбду mutable). - person Joseph Thomson; 14.03.2016
comment
@peetonn Я думаю, что знаю, что ты имеешь в виду. дополнительные методы. Я обновил свой ответ. Дайте мне знать, если это поможет. - person Joseph Thomson; 14.03.2016

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

Ваш форматер должен быть функцией, а не интерфейсом.

Вызывающие могут использовать std::ref, если они хотят гарантировать время жизни, обернуть общий ptr, если они хотят туманное время жизни, или передать по значению.

Если вы хотите более богатый интерфейс, вы можете либо взять кучу таких, либо написать класс, который представляет собой кучу таких (либо через наследование, либо написав notstd::functions вручную).

Сохраните IFormatter fmt; по значению, используйте fmt(a,b) вместо fmt.format(a,b) (СУХОЙ!). Клиентский код может сделать его ref или умной семантикой, если захочет.

наследование как деталь реализации вместо того, чтобы управлять вашим дизайном, освобождает.

person Yakk - Adam Nevraumont    schedule 12.03.2016
comment
почему это должна быть функция? что, если средство форматирования должно сохранить какое-то состояние, скажем, точность для двойных значений, указанную при создании средства форматирования? - person peetonn; 12.03.2016
comment
Функция @peet std может это сделать. - person Yakk - Adam Nevraumont; 12.03.2016
comment
сохранение IFromatter по значению невозможно, так как это абстрактный класс. что касается использования std::function, я не думаю, что это применимо к этому случаю - форматтер может иметь и другие методы (в моем более сложном случае форматтер имеет метод getHeader(), который, например, возвращает заголовок для файлов CSV). - person peetonn; 14.03.2016
comment
@peetonn Мой IFormatter работает по значению. У вас нет. Это проблема с вашим решением, отсюда и ваши головные боли по управлению жизненным циклом. Если вы хотите избавиться от головной боли, связанной с управлением жизненным циклом, встроенной в ваше решение, перестаньте встраивать указатели в свои интерфейсы: указатели (и ссылки) подразумевают головную боль, связанную с управлением жизненным циклом. Ваш фактический вариант использования, не отраженный в OP, потребует немного больше работы, чтобы использовать стирание типа, но это вполне выполнимо. Ваше убеждение, что std::function не может хранить состояние,... неверно. Обобщения std::function решают вашу общую проблему. - person Yakk - Adam Nevraumont; 14.03.2016