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:
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.