2016-03-12 2 views
4

Voici un exemple simplifié de ce qu'on appelle (j'espère - corrigez-moi si je me trompe) Stratégie modèle: il y a une classe FileWriter qui écrit des paires valeur-clé dans un fichier et utilise l'objet de l'interface IFormatter pour le formatage du texte en cours d'écriture. Il existe différentes implémentations de formateurs et l'objet formateur est passé lorsque FileWriter est créé. est ici une (mauvaise) mise en œuvre de ce modèle:Modèle de stratégie en C++. Options d'implémentation

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

Comme on le voit, l'inconvénient principal de cette approche est qu'il est manque de fiabilité-Formatter objet passé à FileWriter doit exister au cours de la vie de toute FileWriter, appelle donc comme FileWriter("test.txt", JsonFormatter()) conduire directement à SegFault.

À cet égard, je voudrais discuter ce qui pourrait être les autres options pour la mise en œuvre d'une telle approche avec « facile à utiliser » et les exigences de simplicité:

  • soit nouvelle formatter peut être transmis lorsque l'auteur du fichier est créé, ou
  • le formateur existant peut être transmis et utilisé.

je suis venu avec plusieurs variantes décrites ci-dessous avec leurs inconvénients (OMI):

  • modèles: ayant FileWriter en tant que classe de modèle qui prend FormatterClass exactement comme argument; inconvénient: moche d'appeler: FileWriter<JsonFormatter>("test.txt", JsonFormatter()) - ici, JsonFormatter est tapé deux fois.
  • pointeurs bruts:FileWriter("test.txt", new JsonFormatter()); drawback - qui doit supprimer l'objet formateur? FileWriter? si oui, le fait de passer une adresse du formateur existant conduira à SegFault une fois que l'objet FileWriter aura tenté de supprimer le formateur.
  • pointeurs partagés:FileWriter("test.txt", dynamic_pointer_cast<IFormatter*>(shared_ptr<JsonFormatter*>(new JsonFormatter())); inconvénient: moche à appeler, et encore, et si formatter a été créé avant la création de l'écrivain de fichier?

Quelles seraient les meilleures pratiques ici?

MISE À JOUR

En réponse aux réponses qui suggéraient d'utiliser std::function - Que faire si Formatter peut stocker un état (par exemple, la précision) et ont d'autres méthodes, comme getHeader(), par exemple, pour les fichiers CSV?

De plus, stocker IFormatter par valeur n'est pas possible car il s'agit d'une classe abstraite.

Répondre

2

La est d'utiliser la solution la plus simple:

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

L'autre option est un peu mieux est d'avoir une fonction clone() dans IFormatter.Ensuite, FileWriter peut cloner l'objet, prendre possession du clone et le supprimer dans son destructeur.

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

Maintenant, vous pouvez aussi appeler FileWriter avec un objet temporaire.

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

alors que cette solution fonctionnera, en effet, la conception décrite en question me semble plutôt être une approche générale qui devrait être supportée par des capacités de bibliothèque de langage/standard (ou nécessiter un design/modèle différent) manipulez-le en écrivant des méthodes 'clone()' pour ses classes. – peetonn

1

modèles: ayant FileWriter en tant que classe de modèle qui prend FormatterClass exactement comme argument; inconvénient: moche à appeler: FileWriter ("test.txt", JsonFormatter()) - ici, JsonFormatter est tapé deux fois.

Plus de modèles!

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

Ta da! Maintenant, il est aussi simple que:

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

merci pour votre réponse! J'essaierai toujours de ne pas utiliser de modèles pour l'instant, à moins que je trouve une solution appropriée. – peetonn

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

Votre formatter doit être une fonction, pas d'interface.

Les appelants peuvent utiliser std::ref s'ils veulent garantir la durée de vie, envelopper un PTR partagé s'ils veulent une durée de vie nébuleuse, ou passer une valeur de référence. Si vous voulez une interface plus riche, vous pouvez soit en prendre une pile, soit écrire une classe qui en est une pile (soit par héritage soit en écrivant manuellement notstd::functions).

Enregistrer IFormatter fmt; par valeur, utiliser fmt(a,b) au lieu de fmt.format(a,b) (DRY!). Le code client peut en faire une référence ou une sémantique intelligente s'il le souhaite.

L'héritage en tant que détail d'implémentation, au lieu de générer votre conception, est libéré.

+0

pourquoi il devrait être une fonction? Que faire si le formateur doit enregistrer un certain état, disons, de précision pour les doubles qui est spécifié lors de la construction du formateur? – peetonn

+0

@peet La fonction std peut le faire. – Yakk

+0

Il n'est pas possible de stocker 'IFromatter' par valeur, car c'est une classe abstraite. en ce qui concerne l'utilisation de 'std :: function', je ne pense pas qu'elle s'applique à ce cas - formatter peut aussi avoir d'autres méthodes (dans mon cas plus complexe, formatter a la méthode' getHeader() 'qui renvoie un en-tête pour les fichiers CSV par exemple). – peetonn

1

C'est ce que fait la bibliothèque standard (par exemple, std::shared_ptr peut prendre un suppresseur). Formatter doit être copie constructible, et évidemment l'expression f << fmt(key, value) doit être bien formée.

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

Si vous avez besoin de plus d'une fonction dans votre interface, vous pouvez utiliser votre approche originale, mais contrôler la durée de vie du formatter avec std::unique_ptr ou std::shared_ptr (souvenez-vous de faire le destructor virtuel).

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

Si vous voulez passer un Formatter à la FileWriter existante, soit vous devez copier/déplacer dans un pointeur intelligent pour transférer la propriété, ou vous avez besoin de l'envelopper dans l'interface formatter.

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

Ceci a toujours le problème de gestion de durée de vie que vous essayez d'éviter. Cependant, je ne vois aucun moyen de contourner cela. Soit vous donnez la propriété unique ou partagée du Formatter au FileWriter, soit vous laissez la gestion de la vie entre les mains de l'appelant (ce qui est une approche parfaitement valable si vous appréciez l'efficacité par rapport à la sécurité).

+0

même commentaire que pour la réponse de @ Yakk - que faire si 'IFormatter' doit porter un état (disons, précision pour double) ou peut avoir des méthodes supplémentaires (comme' getHeader() 'pour le formatage des fichiers CSV)? Devrait-il y avoir un design différent? – peetonn

+1

Tant que 'Formatter' est constructible, il n'y a aucune raison pour qu'il ne puisse pas porter l'état. Si vous avez besoin de méthodes supplémentaires, vous pouvez soit passer un formateur qui implémente 'operator()', soit envelopper votre formateur dans un lambda: '[formatter] (std :: chaîne s, double d) {return formatter.format (s, ré); } '(ici, soit make' format' 'const' ou lambda' mutable'). –

+0

@peetonn Je pense savoir ce que vous voulez dire par w.r.t. méthodes supplémentaires. J'ai mis à jour ma réponse. Faites-moi savoir si ça aide. –