2

Let's say I want to make a file parser which uses the strategy-like pattern to allow use of different specific parsers which would do all the hard work and then returned the results which could vary between them.

For example file contains A, B, C, D and E in each line and I want to get only A, C and E, so I create a specific parser that pulls out just them.

I should also note that I'm dealing with NMEA format and each parser would handle different statements (lines) from the file.

class FileParser
{
std::string file_path;
public:
    FileParser(std::string file_path, ??? parser);

    void parseFile() {
        // ...
        // for each line:
            parser.parseStatement(line);
        // end for
        // ...
    }

    ??? getResults() {
        return parser.getResults();
    }
};

class Parser
{
public:
    virtual void parseLine(std::string line);
    ??? getResults();
}

class SpecificParser : public Parser
{
    void parseLine(std::string line);
    SpecificFormat getResults();
}

And my problem is - how to correctly write such code? My first thought was templates, but I don't yet know how to use them, so as a second thought I got:

FileParser otherFunction() {
    ExampleParser ep();
    FileParser fp("example.txt", &ep);
    return fp;
}

void function() {
    FileParser fp = otherFunction(); // countains reference to deleted instance of ExampleParser
}

... but it's also buggy (I showed an edge case purposefully).

What's the proper way to structure that code?

Example use case (as requested below)

auto parser1 = FileParser<SpecificParserType1>(file_name);
parser1.parseFile();
auto parser1.getResults(); // returns instance of specific type 1, e.g. std::vector<Type1>

auto parser2 = FileParser<SpecificParserType2>(file_name);
parser2.parseFile();
auto parser2.getResults(); // returns instance of specific type 2, e.g. std::vector<Type2>

The main goal is to be able to have different strategies of getting data from the file.

Luke
  • 1,369
  • 1
  • 13
  • 37

3 Answers3

2

From my point of view your idea how to use the strategy pattern here is the wrong way because:

You want to make a class hierarchy which have allways the nearly same signature which already shows up your problem: There is a design problem, because your concrete strategy method is not a variant of a strategy because it has a different return type.

If such a thing can be implemented with a hack like pointers to anything you run into the next problem. So let us define that anything can be reduced to data objects.

Your concrete file parser reading strategies methods delivers a huge amount of different data object types which must be described somewhere in your program to handle such. Hint: Which type of interface would your data objects have? Maybe you want to read some int values but also std::string objects and also container classes. Is there any common interface for that types?

If you don't have such a common interface for your data types you simply have to break a general OOP design rule: Never look from the outside of the details inside a class/object. You workaround could be something like:

switch ( dataobject[n].getType() ) 
{
    case INT:
        ...;
    case STRING:
        ...;

    ... and all the objects you have defined yourself!
}

You write now code like:

auto parser1.getResults();

your result must be always the same. auto did not mean it can change it type during runtime. So that is not a solution here.

Next problem: You program code seems to make some assumptions to the content of your file. The user of your strategie pattern must know how the objects are stored in the file in which concrete order. A better way is that the object contains some content and a parser can handle this. But here is the strategy pattern not the right one.

My hint: If your design, independent of any design pattern, results in defining a class hierarchy which have the some virtual functions which must have different return types ( which is not possible ), your design is broken!

Klaus
  • 24,205
  • 7
  • 58
  • 113
  • It's a question concerning design. Any suggestions of possible modifications are welcome. – Luke Jul 22 '15 at 11:31
1

Just use inheritance for your format as well:

class Format{
    public:
        virtual ~Format(){}
        virtual char *data() = 0;
};

class SpecificFormat: public Format{
    public:
        char *data(){return nullptr;}
};

Then return a std::unique_ptr for your getResults function:

class Parser{
    public:
        virtual std::unique_ptr<Format> get_results() = 0;
};

and in your concrete class:

class SpecificParser: public Parser{
    public:
        std::unique_ptr<Format> get_results(){
            return std::make_unique<SpecificFormat>();
        }
};

Then in your FileParser class:

class FileParser{
    public:
        FileParser(const std::string &file_path, std::unique_ptr<Parser> &&parser)
            : parser(std::move(parser)){}

        std::unique_ptr<Format> get_results(){
            return parser->get_results();
        }

    private:
        template<typename T>
        struct TypeTag{};

        template<typename T>
        FileParser(const std::string &file_path, TypeTag<T>)
            : parser(std::make_unique<T>()){}

        template<typename T>
        friend FileParser make_parser(const std::string &filepath);

        std::unique_ptr<Parser> parser;
};

template<typename T>
FileParser make_parser(const std::string &filepath){
    return FileParser(filepath, FileParser::TypeTag<T>{});
}

Of course, you would return a more filled out Format child class.

Use it like this:

int main(){
    auto parser = FileParser{"test_file.my_format", std::make_unique<SpecificFormat>()};
    auto results = parser.get_results();
    auto data = results->data();
}

or

int main(){
    auto parser = make_parser<SpecificFormat>("testfile");
    auto results = parser.get_results();
    auto data = results->data();
}
RamblingMad
  • 5,332
  • 2
  • 24
  • 48
  • But the main problem is the relation between `FileParser` and specific parsers which must inherit `IParser` and returning data in a specific format, depending on which specific parser was passed to `FileParser`. – Luke Jul 22 '15 at 09:36
  • @Luke I modified my answer for you, using templates so you can see how they can be used. – RamblingMad Jul 22 '15 at 09:44
  • `parser.get_results()` from the last code snippet would return obejct of `SpecificFormat` since `get_results()` is `virtual` - right? – Luke Jul 22 '15 at 09:56
  • @Luke yeah, a pointer to `SpecificFormat`, anyway. That's the whole point of polymorphism, and your example is a good time to make use of it! – RamblingMad Jul 22 '15 at 09:57
  • No, it won't, beacuse it's return type is ``. Can we do something about it? – Luke Jul 22 '15 at 09:57
  • @Luke I think you need to read up on polymorphism. A pointer to `Format` could be pointing to any of its child classes but never `Format` because it is a pure abstract type. You can test with `dynamic_cast` or `typeid` or some other run-time type checking utility which child it is. – RamblingMad Jul 22 '15 at 09:59
  • Yeah, I know. But I wanted to avoid using `dynamic_cast` and such "hacks". – Luke Jul 22 '15 at 10:06
  • @Luke you don't have to use `dynamic_cast`... that was just how you could prove that this works. Polymorphism is not a 'hack', it's a language construct. – RamblingMad Jul 22 '15 at 10:07
  • I still can't see how with this solution I could have parsers returning data in really different formats, say `std::map`s of pairs and of triplets. – Luke Jul 22 '15 at 10:17
  • @Luke you could have 50 different parser types all stored in the same container with this approach. Why don't you think you could? – RamblingMad Jul 22 '15 at 10:19
  • Could you please provide one more example, with those specific parsers returning different data structures (not specifically `char*`) eg. `std::vector` and `std::vector`? – Luke Jul 22 '15 at 10:23
  • I'm not exactly sure what you mean, could you put an example of exactly what you want in your question? – RamblingMad Jul 22 '15 at 10:27
  • I've just added an example use case in my question. – Luke Jul 22 '15 at 10:40
  • @Luke what you want is not possible without further polymorphism. You are actually complicating your situation by adding in the `FileParser` class in the first place. You need to know what types your file could possibly contain and check the type of file opened at runtime, then downcast your `Format` pointer to the appropriate format. – RamblingMad Jul 22 '15 at 12:12
  • @Luke if you look at [this](http://coliru.stacked-crooked.com/a/620d8a266a104ef9) you can see that I get different data depending on file format. You just need to interpret what data you are getting and create an interface for handling that. – RamblingMad Jul 22 '15 at 12:14
  • Thanks, I'll look at your code and try to understand it. I know what is the structure of the file. I just want to have ability to specify which data I want to get from the file. When file contains for example A, B, C, D, E and I want to get only A, C and E, I could make a parser "strategy" that pulls out only specific data. – Luke Jul 22 '15 at 12:23
  • @Luke Well, for that I would say that you shouldn't use polymorphism **at all**. Try something like [this](http://coliru.stacked-crooked.com/a/0aab07047b7f1203). You shouldn't unnecessarily use polymorphism. It isn't a free feature; it has runtime cost. – RamblingMad Jul 22 '15 at 12:36
  • The method doesn't really matter for me. I just would like to have a possibility to specify which data from file I need (which columns, let's say) and get the structure only with them inside. – Luke Jul 22 '15 at 12:44
1

Templates seem to be the best way to go here, as you know the type you want to get from the parser. Therefore you can make the whole FileParser class a template, and take the specific parser class as a template parameter.

This way, you don't need a single base class for specific parsers, and you can get the correct return type for FileParser::getResults() by using the auto getResults() -> decltype(parser->getResults()) syntax. In C++14, you can even write just "auto getResults()", without the arrow part, and the compiler will automatically determine the return type based on the function's body.

template <typename Parser>
class FileParser
{
    std::string file_path;
    Parser parser;
public:
    FileParser(std::string file_path, Parser parser): file_path(file_path), parser(parser) {}

    void parseFile() {
        // ...
        // for each line:
            parser.parseStatement(line);
        // end for
        // ...
    }

    auto getResults() -> decltype(parser.getResults()) {
        return parser.getResults();
    }
};

class SpecificParser
{
    void parseStatement(std::string line);
    SpecificFormat getResults();
}
LEW21
  • 161
  • 4
  • 1
    Can you provide some context to your answer, that way future readers can learn how to apply it to their issues and not just in this situation. – Newd Jul 22 '15 at 12:58