简体   繁体   中英

Which design pattern should I use in this case?

I have a class named DS which can (1) read a data from file and accordingly build a data structure from scratch, or (2) read a pre-built data structure from file. I originally wrote:

class DS 
{
    DS(std::string file_name, bool type);
}

where file_name is the file to read and type specifies what we are reading, data or pre-built data structure. This method is not very elegant, as far as I am concerned. I also tried the following:

class DS 
{
    DS(std::string file_name);
    void CreateFromData();
    void ReadExisting();
}

But because modification is not allowed once built, I do not want the user to first call CreateFromData and then ReadExisting .

Are there some design patterns to address this issue?

Here's how I'll do it:

Create two sub classes from a new DataFetch class - CreateFromData and ReadExisting ; all three having getData method. Create another 'Data manager" class which will have instance of DataFetch , It would Data Manager 's responsibility to create appropriate object based on "User" input, you could have two constructors for that. Now, Your DS 's constructor will take Data manager 's object, created in previous step and ask it to filling current DS object, via getData method.

This allows your design to add more types of data fetching later on, whilst removing any coupling from your DS and data fetching .

Essentially, as the user of DS you input a file path and expect to get back a data structure corresponding to the file's content. You should not have to worry about the data format in the file at all. That's an implementation detail of the storage format and should be a part of the loading logic.

So, this is what I would do:

  • Put a format ID at the beginning of each data file, identifying which storage format it uses. Or maybe even different file extensions are sufficient.
  • When reading the file the format ID determines which concrete loading logic is used.
  • Then the user of DS only has to provide the file name. The storage format is transparent.

Ideally you simplify the API and get rid of DS . All your caller sees and needs is a simple function:

// in the simplest case
OutputData load_data_from_file(const std::string& filepath);

// for polymorphic data structures
std::unique_ptr<IOutputData> load_data_from_file(const std::string& filepath);

That fits the use-case exactly: “I have a path to a data file. Give me the data from that file.”. Don't make me deal with file loader classes or similar stuff. That's an implementation detail. I don't care. I just want that OutputData . ;)

If you only have the two current storage formats and that's unlikely to change, don't overcomplicate the logic. A simple if or switch is perfectly fine, for instance:

OutputData load_data_from_file(const std::string& filepath)
{
    const auto format_id = /* load ID from the file */;

    if (format_id == raw) {
        return /* call loading logic for the raw format */;
    }
    else if (format_id == prebuilt) {
        return /* call loading logic for the prebuilt format */;
    }

    throw InvalidFormatId();
}

Should things become more complicated later you can add all the needed polymorphic file loader class hierarchies, factories or template magic then.

Use static factory functions if the constructor signature isn't semantic enough. No need to get fancy with it.

class DS {
private:
    enum class Source { FromExisting, FromData };
    DS(const std::string& path, Source type);

public:
    static DS ReadExisting(const std::string& path) {
        return DS(path, Source::FromExisting);
    }
    static DS CreateFromData(const std::string& path) {
        return DS(path, Source::FromData);
    }
};

/* ... */

DS myData = DS::ReadExisting("...");

Option 1: Enumeration Type

You essentially have two different modes for reading the data, which you differentiate via the parameter bool type . This is bad form for a number of reasons, not the least of which being that it's unclear what the two types are or even what type true refers to vs false .

The simplest way to remedy this is to introduce an enumeration type, which contains a named value for all possible types. This would be a minimalistic change:

class DS
{
    enum class mode
    {
        build, read
    };

    DS(const std::string &file_name, mode m);
};

So then we could use it as:

DS obj1("something.dat", DS::mode::build); // build from scratch
DS obj2("another.dat", DS::mode::read);    // read pre-built

This is the method that I would use, as it's very flexible and extensible if you ever want to support other modes. But the real benefit is clarity at the call site as to what's happening. true and false are often obscure when used as function arguments.

Option 2: Tagged Constructors

Another option to differentiate these functions which is common enough to mention is the notion of tagged constructors. This effectively amounts to adding a unique type for each mode you want to support and using it to overload the constructors.

class DS
{
    static inline struct built_t {} build;
    static inline struct read_t {} read;

    DS(const std::string &file_name, build_t); // build from scratch
    DS(const std::string &file_name, read_t);  // read pre-built
};

So then we could use it as:

DS obj1("something.dat", DS::build); // build from scratch
DS obj2("another.dat", DS::read);    // read pre-built

As you can see, the types build_t and read_t are introduced to overload the constructor. Indeed, when this technique is used we don't even name the parameter because it's purely a means of overload resolution. For a normal method we'd typically just make the function names different, but we can't do that for constructors, which is why this technique exists.

A convenience I added was defining static instances of these two tag types: build and read , respectively. If these were not defined we would have to type:

DS obj1("something.dat", DS::build_t{}); // build from scratch
DS obj2("another.dat", DS::read_t{});    // read pre-built

Which is less aesthetically pleasing. The use of inline is a C++17 feature that makes it so that we don't have to separately declare and define the static variables. If you're not using C++17 remove inline and define the variables in your implementation file as usual for a static member.

Of course, this method uses overload resolution and is thus performed at compile time. This makes it less flexible than the enumeration method because it cannot be determined at runtime, which would conceivably be needed for your project later down the road.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM