开发者

C++ class for application/program settings?

Using Visual Studio C++ with MFC. I'm trying to figure out what would be a good way to store application/program settings. I'm not referring to their persistent storage but to the data structure used in code to hold the settings.

I created a static class called Settings that has several static methods and also nested classes to partition the settings. For example:

class Settings
{
public:
    Settings(void);
    ~Settings(void);

    static void SetConfigFile(const char * path);
    static CString GetConfigFilePath();
    static void Load();
    static void Save();

    class General
    {
    public:
        static CString GetHomePage();
        static void SetHomePage(const char * url);
    private:
        static int homePageUrl_;
    };

private:
    static CString configFilePath_;
};

Then I can access my settings throughout my code like:

Settings::General::GetHomePage();

Now I'm getting into unit testing and I'm starting to realize that static classes are undesirable. So I want to turn this into an instance based class. But I'll have to manage the nested class instances which is trivial but still seems a little cumbersome for testing. The whole intention of the nested classes is simply to group the settings into logical groups. I'm debating whether a string-based settings class would be better, something like settings->get("General.Ho开发者_C百科mePage") although I think I prefer the strong typing of dedicated accessor methods.

So to get to my question what is a good data structure to hold program configuration/settings that supports straightforward unit testing?


You can do this if it works for you. You can ditch the enum and go to const strings or even free-form strings. The enum doesn't really have to be defined in the class either. There are lots of ways to do it.

Another class could do something similar with multiple instances using a template to define the enum type if you wanted to implement categories.

Just an idea.

#include "stdafx.h"
#include <map>
#include <string>
#include <iostream>
using namespace std;


class Settings
{
public:

  typedef enum
  {
    HomePageURL,
    EmailAddress,
    CellPhone
  } SettingName;

private:
  typedef map<SettingName, string> SettingCollection;

  SettingCollection theSettings;


public:

  string& operator[](const SettingName& theName)
  {
    return theSettings[theName];
  }

  void Load ()
  {
    theSettings[HomePageURL] = "http://localhost";
  }

  void Save ()
  {
    // Do whatever here
  }

};


int _tmain(int argc, _TCHAR* argv[])
{
  Settings someSettings;

  someSettings.Load ();

  cout << someSettings [Settings::SettingName::HomePageURL] << endl;


    return 0;
}


I don't think there has to be a conflict between your requirements of: (1) providing type-safe access to configuration variables; and (2) using a "fully.scoped.name" syntax to specify the name of a configuration variable. Surely you could have type-safe operations such as:

const char * getString(const char * fullyScopedName);
int          getInt(const char * fullyScopedName);
bool         getBool(const char * fullyScopedName);

You might find some inspiration by reading Chapters 2 and 3 of the Getting Started Guide (PDF, HTML) for my Config4Cpp library.

Edit: The Config4Cpp documentation I mentioned might provide inspiration for API design, but I belated realised that you might appreciate advice on implementation options in case you decide to write your own configuration class from scratch (rather than use a third-party library like Config4Cpp) ...

Your class should use a std::map to store a collection of fullyScopedName->value mappings. Obviously, the fullyScopedName will be a string, but there are two options for representing the value.

The first option is to represent the value as a string. A type-safe accessor such as getInt() or getBool() will retrieve the string-based value from the map and then parse it to convert it into the desired type. If the parsing fails, then the accessor operation throws an exception. (That is the approach taken by Config4Cpp.)

The second option is to represent value as shown in the pseudocode below:

enum ValueType { STRING_VAL, INT_VAL, BOOL_VAL };
struct Value {
    ValueType         type;
    union {
        const char *  stringVal;
        int           intVal;
        bool          boolVal;
    } data;
};

The implementation of a type-safe accessor can then be coded as follows (pseudocode):

int getInt(const char * fullyScopedName)
{
    Value * v = nameValueMap[fullyScopedName];
    if (v->type != INT_VAL) {
        throw WrongTypeException(...);
    }
    return v->data.intVal;
}


This is the class that I'm using now mostly inspired by Nathan's answer except with templated methods:

class Settings {
public:
    Settings(void);
    virtual ~Settings(void);

    enum SettingName { General_WindowWidth, General_HomePageUrl,
        General_ShowDownloadsWindow, Privacy_RememberPasswords,
        Privacy_RememberHistory };

    virtual void SetConfigFile(const char * path);
    virtual std::string GetConfigFilePath();
    virtual void Load();
    virtual void Save();

    template<class T>
    T Get(SettingName name) {
        return boost::lexical_cast<T>(settings_[name]);
    }

    template<class T>
    void Set(SettingName name, T value) {
        settings_[name] = boost::lexical_cast<std::string>(value);
    }

    void Set(SettingName name, std::string value) {
        settings_[name] = value;
    }

private:
    std::string configFilePath_;
    std::map<SettingName, std::string> settings_;
};
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜