我有一个名为DS
的类,它可以(1)从文件读取数据并据此从头开始构建数据结构,或(2)从文件读取预构建的数据结构。我最初写道:
class DS
{
DS(std::string file_name, bool type);
}
其中file_name
是要读取的文件,而type
指定我们正在读取的内容,数据或预构建的数据结构。就我而言,这种方法不是很优雅。我还尝试了以下方法:
class DS
{
DS(std::string file_name);
void CreateFromData();
void ReadExisting();
}
但是因为一旦建立就不允许修改,所以我不希望用户先调用CreateFromData
,然后再调用ReadExisting
。
是否有一些设计模式可以解决此问题?
答案 0 :(得分:0)
这是我要怎么做:
从新的DataFetch
类创建两个子类-CreateFromData
和ReadExisting
;这三个都具有getData方法。创建另一个具有DataFetch
实例的“数据管理器”类,由Data Manager
负责根据“用户”输入创建适当的对象,您可以为此使用两个构造函数。 DS
的构造函数将采用在上一步中创建的Data manager
的对象,并要求它通过getData
方法填充当前的DS对象。
这使您的设计以后可以添加更多类型的数据,同时消除DS
和data fetching
中的任何耦合。
答案 1 :(得分:0)
从本质上讲,作为DS
的用户,您输入了文件路径,并希望获取与文件内容相对应的数据结构。您完全不必担心文件中的数据格式。那是存储格式的实现细节,应该成为加载逻辑的一部分。
所以,这就是我要做的:
DS
的用户只需提供文件名。存储格式是透明的。理想情况下,您可以简化API并摆脱DS
。您的呼叫者看到和需要的只是一个简单的功能:
// 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);
这完全符合用例:“我有一个数据文件的路径。给我该文件中的数据。”。不要让我处理文件加载器类或类似内容。这是实施细节。我不在乎我只想要那个OutputData
。 ;)
如果您只有两种当前的存储格式,并且不太可能更改,请不要使逻辑过于复杂。一个简单的 if 或 switch 很好,例如:
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();
}
如果事情以后变得更复杂,则可以添加所有必需的多态文件加载器类层次结构,工厂或模板魔术。
答案 2 :(得分:0)
如果构造函数签名的语义不够充分,请使用静态工厂函数。无需花哨。
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("...");
答案 3 :(得分:0)
选项1:枚举类型
从本质上讲,您有两种不同的读取数据的模式,可通过参数bool type
进行区分。出于多种原因,这是一种不好的形式,其中最重要的一点是,目前尚不清楚这两种类型分别是true
和false
是什么。
最简单的解决方法是引入枚举类型,该类型包含所有可能类型的命名值。这将是一个简约的更改:
class DS
{
enum class mode
{
build, read
};
DS(const std::string &file_name, mode m);
};
所以我们可以将其用作:
DS obj1("something.dat", DS::mode::build); // build from scratch
DS obj2("another.dat", DS::mode::read); // read pre-built
这是我将要使用的方法,因为如果您想支持其他模式,它非常灵活且可扩展。但是真正的好处是在呼叫站点可以清楚了解正在发生的事情。当将true
和false
用作函数参数时,它们通常是晦涩的。
选项2:标记的构造函数
另一个可以区分这些功能的选项是标记构造函数的概念,这一点很常见。这实际上等于为您要支持的每种模式添加唯一类型,并使用它来重载构造函数。
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
};
所以我们可以将其用作:
DS obj1("something.dat", DS::build); // build from scratch
DS obj2("another.dat", DS::read); // read pre-built
如您所见,引入了build_t
和read_t
类型以重载构造函数。确实,使用这种技术时,我们甚至都没有命名参数,因为它纯粹是重载解析的一种手段。对于通常的方法,我们通常只会使函数名称不同,但对于构造函数则无法做到这一点,这就是存在这种技术的原因。
我添加的一个便利是定义了这两个标记类型的静态实例:分别为build
和read
。如果未定义这些内容,我们将必须输入:
DS obj1("something.dat", DS::build_t{}); // build from scratch
DS obj2("another.dat", DS::read_t{}); // read pre-built
在美学上不太令人满意。 inline
的使用是C ++ 17的一项功能,使我们不必单独声明和定义静态变量。如果您不使用C ++ 17,请删除inline
并像往常一样为静态成员在实现文件中定义变量。
当然,此方法使用重载解析,因此在编译时执行。这使它不如枚举方法灵活,因为它无法在运行时确定,这可能是以后项目需要的。