17

I'm writing a medium size OOP application in C++ as a way to practice OOP principles.

I have several classes in my project, and some of them need to access run-time configuration parameters. These parameters are read from several sources during application start-up. Some are read from a config file in the users home-dir, some are command line arguments (argv).

So I created a class ConfigBlock. This class reads all the parameter sources and stores it in an appropriate data structure. Examples are path- and filenames which can be changed by user in the config file, or the --verbose CLI flag. Then, one can call ConfigBlock.GetVerboseLevel() in order to read this specific parameter.

My question: Is it good practice to collect all such runtime config data in one class?

Then, my classes need access to all these parameters. I can think of several ways to achieve this, but I'm not sure which one to take. A class' constructor can be a given a reference to my ConfigBlock, like

public:
    MyGreatClass(ConfigBlock &config);

Or they just include a header "CodingBlock.h" which contains a definition of my CodingBlock:

extern CodingBlock MyCodingBlock;

Then, only the classes .cpp file needs to include and use the ConfigBlock stuff.
The .h file does not introduce this interface to the user of the class. However, the interface to ConfigBlock is still there, however, it's hidden from the .h file.

Is it good to hide it this way?

I want the interface to be as small as possible, but in the end, I guess every class which needs config parameters has to have a connection to my ConfigBlock. But, what should this connection look like?

0

4 Answers 4

14

I'm quite the pragmatist, but my main concern here is that you might be allowing this ConfigBlock to dominate your interface designs in a possibly bad way. When you have something like this:

explicit MyGreatClass(const ConfigBlock& config);

... a more appropriate interface might be like this:

MyGreatClass(int foo, float bar, const string& baz);

... as opposed to just cherry-picking these foo/bar/baz fields out of a massive ConfigBlock.

Lazy Interface Design

On the plus side, this kind of design makes it easy to design a stable interface for your constructor, e.g., since if you end up needing something new, you can just load that into a ConfigBlock (possibly without any code changes) and then cherry-pick whatever new stuff you need without any kind of interface change, only a change to the implementation of MyGreatClass.

So it's both kind of a pro and con that this frees you of designing a more carefully thought-out interface that only accepts inputs it actually needs. It applies the mindset of, "Just give me this massive blob of data, I'll pick out what I need from it" as opposed to something more like, "These precise parameters are what this interface needs to work."

So there are definitely some pros here, but they might be heavily outweighed by the cons.

Coupling

In this scenario, all such classes being constructed from a ConfigBlock instance end up having their dependencies look like this:

enter image description here

This can become a PITA, for example, if you want to unit test Class2 in this diagram in isolation. You might have to superficially simulate various ConfigBlock inputs containing the relevant fields Class2 is interested in to be able to test it under a variety of conditions.

In any kind of new context (whether unit test or whole new project), any such classes can end up becoming more of a burden to (re)use, as we end up having to always bring ConfigBlock along for the ride, and setting it up accordingly.

Reusability/Deployability/Testability

Instead if you design these interfaces appropriately, we can decouple them from ConfigBlock and end up with something like this:

enter image description here

If you notice in this above diagram, all the classes become independent (their afferent/outgoing couplings reduce by 1).

This leads to a lot more independent classes (at least independent of ConfigBlock) which can be a lot easier to (re)use/test in new scenarios/projects.

Now this Client code ends up being the one that has to depend on everything and assemble it all together. The burden ends up being transferred to this client code to read the appropriate fields from a ConfigBlock and pass them into the appropriate classes as parameters. Yet such client code is generally narrowly-designed for a specific context, and its potential for reuse is typically going to be zilch or close anyway (it might be your application's main entry point function or something like that).

So from a reusability and testing standpoint, it can help to make these classes more independent. From an interface standpoint for those using your classes, it can also help to explicitly state what parameters they need instead of just one massive ConfigBlock which models the whole universe of data fields required for everything.

Conclusion

In general, this kind of class-oriented design which depends on a monolith which has everything needed tends to have these kinds of characteristics. Their applicability, deployability, reusability, testability, etc. can get significantly degraded as a result. Yet they can kind of simplify the interface design if we attempt a positive spin on it. It's up to you to measure those pros and cons and decide whether the trade-offs are worth it. Typically it's much safer to err against this kind of design where you're cherry-picking from a monolith in classes that are generally intended to model a more general and widely-applicable design.

Last but not least:

extern CodingBlock MyCodingBlock;

... this is potentially even worse (more skewed?) in terms of the characteristics described above than the dependency injection approach, as it ends up coupling your classes not only to ConfigBlocks, but directly to a specific instance of it. That further degrades applicability/deployability/testability.

My general advice would to err on the side of designing interfaces which don't depend on these kinds of monoliths to provide their parameters, at least for the most generally-applicable classes you design. And avoid the global approach without dependency injection if you can unless you really have a very strong and confident reason not to avoid it.

1

Usually the configuration of an application is consumed primarily by factory objects. Any object relying on configuration should be generated from one of those factory objects. You could utilize the Abstract Factory Pattern to implement one class that takes in the whole ConfigBlock object. This class would expose public methods to return other factory objects, and would only pass in the part of the ConfigBlock relevant to that particular factory object. That way the config settings "trickle down" from the ConfigBlock object to its members, and from the Factory factory to the factories.

I'll use C# since I know the language better, but this should be easily transferable to C++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

After that you need some sort of class that acts as the "application" that gets instantiated in your main procedure:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

As one last note, this is known as a "provider object" in .NET speak. Provider objects in .NET seem to marry configuration data to factory objects, which is essentially what you want to do here.

See also Provider Pattern for Beginners. Again, this is geared towards .NET development, but with C# and C++ both being object oriented languages, the pattern should be mostly transferable between the two.

Another good read that is related to this pattern: The Provider Model.

Lastly, a critique of this pattern: Provider is not a pattern

2
  • All is good, except the links to the provider models. The reflection is not supported by c++, and that will not work. Dec 14, 2015 at 14:54
  • @BЈовић: Correct. Class reflection doesn't exist, however you can build in a manual workaround, which basically devolves to a switch statement or an if statement testing against a value read from the config files. Dec 14, 2015 at 15:11
1

Short answer:

You don't need all settings for each of modules/classes in your code. If you do, then there is something wrong with your object-oriented design. Especially in case of unit testing setting all of variables that you don't need and passing that object would not help with reading or maintaining.

2
  • This way i can gather the parser code (parse command line and config files) in one central location. Then, each class can cherry-pick their relevant parameters from there. Whats a good design in your opinion?
    – lugge86
    Dec 14, 2015 at 16:23
  • Maybe I just wrote it wrong - I mean you have (and its a good practice) to have a general abstraction with all settings got from configuration file/environment variables - which might be your ConfigBlock class. The point here is to not providing all, in this case, context of system state, just particular, required values to do so.
    – Dawid Pura
    Dec 15, 2015 at 14:15
0

First question: is it good practise to collect all such runtime config data in one class?

Yes. It is better to centralize runtime constants and values and the code to read them.

A class' constructor can be a given a reference to my ConfigBlock

This is bad: most of your constructors will not need most of the values. Instead, create interfaces for everything that is not trivial to construct:

old code (your proposal):

MyGreatClass(ConfigBlock &config);

new code:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

instantiate a MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Here, current_config_block is an instance of your ConfigBlock class (the one that contains all your values) and the MyGreatClass class receives a GreatClassData instance. In other words, only pass to constructors the data they need, and add facilities to your ConfigBlock to create that data.

Or they just include a header "CodingBlock.h" which contains a definition of my CodingBlock:

 extern CodingBlock MyCodingBlock;

Then, only the classes .cpp file needs to include and use the ConfigBlock stuff. The .h file does not introduce this interface to the user of the class. However, the interface to ConfigBlock is still there, however, it's hidden from the .h file. Is it good to hide it this way?

This code suggests that you will have a global CodingBlock instance. Don't do that: normally you should have an instance declared globally, in whatever entry point your application uses (main function, DllMain, etc) and pass that around as an argument wherever you need (but as explained above, you should not pass the entire class around, just expose interfaces around the data, and pass those).

Also, don't tie your client classes (your MyGreatClass) to the type of CodingBlock; This means that, if your MyGreatClass takes in a string and five integers, you will be better off passing in that string and integers, than you will be passing in a CodingBlock.

1
  • I think it's a good idea to decouple factories from the config. It's unsatisfactory that the config implementation should know how to instantiate components, since this necessarily results in a 2-way dependency where previously, only a 1-way dependency existed. This has great implications when extending your code, especially when using shared libraries where the interfaces really matter Dec 14, 2015 at 19:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.