在这种情况下应使用哪种设计模式?

时间:2020-01-11 04:48:44

标签: c++ design-patterns

我有一个名为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

是否有一些设计模式可以解决此问题?

4 个答案:

答案 0 :(得分:0)

这是我要怎么做:

从新的DataFetch类创建两个子类-CreateFromDataReadExisting;这三个都具有getData方法。创建另一个具有DataFetch实例的“数据管理器”类,由Data Manager负责根据“用户”输入创建适当的对象,您可以为此使用两个构造函数。 DS的构造函数将采用在上一步中创建的Data manager的对象,并要求它通过getData方法填充当前的DS对象。

这使您的设计以后可以添加更多类型的数据,同时消除DSdata fetching中的任何耦合。

答案 1 :(得分:0)

从本质上讲,作为DS的用户,您输入了文件路径,并希望获取与文件内容相对应的数据结构。您完全不必担心文件中的数据格式。那是存储格式的实现细节,应该成为加载逻辑的一部分。

所以,这就是我要做的:

  • 在每个数据文件的开头放置一个格式ID,以标识其使用的存储格式。也许甚至不同的文件扩展名也足够。
  • 读取文件时,格式ID决定使用哪种具体的加载逻辑。
  • 然后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进行区分。出于多种原因,这是一种不好的形式,其中最重要的一点是,目前尚不清楚这两种类型分别是truefalse是什么。

最简单的解决方法是引入枚举类型,该类型包含所有可能类型的命名值。这将是一个简约的更改:

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

这是我将要使用的方法,因为如果您想支持其他模式,它非常灵活且可扩展。但是真正的好处是在呼叫站点可以清楚了解正在发生的事情。当将truefalse用作函数参数时,它们通常是晦涩的。

选项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_tread_t类型以重载构造函数。确实,使用这种技术时,我们甚至都没有命名参数,因为它纯粹是重载解析的一种手段。对于通常的方法,我们通常只会使函数名称不同,但对于构​​造函数则无法做到这一点,这就是存在这种技术的原因。

我添加的一个便利是定义了这两个标记类型的静态实例:分别为buildread。如果未定义这些内容,我们将必须输入:

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并像往常一样为静态成员在实现文件中定义变量。

当然,此方法使用重载解析,因此在编译时执行。这使它不如枚举方法灵活,因为它无法在运行时确定,这可能是以后项目需要的。