Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where to keep options values, paths to important files, etc [closed]

Tags:

java

I'm creating a program that requires some options values to be set along with some paths to image files, path to a SQLite database, some information about the text on various buttons, information about which database to use (SQLite / MySQL for example), etc.

So far, I had a helper class in which I made everything static and I accessed it from everywhere in the program, but it was becoming a mess and I read that this is sort of bad practice since it doesn't follow object-oriented programming guidelines.

So what I did is make that class a singleton with its reference in my controller object. To access my options, I now call controller.getOptionName instead of calling Helper.getOptionName directly.

I have a feeling I'm approaching this the wrong way, especially because from what I've read, if many of my objects depend on a single class (the helper class), I'm not separating everything enough.

I don't know what I should be doing instead, is there a "standard" on where to keep all my options? I thought about using a XML file or something along those lines, but I'm going to end up having to access it from everywhere anyway, so it feels like this would create the same problem.

like image 291
Adam Smith Avatar asked Jul 28 '12 15:07

Adam Smith


2 Answers

The Problem: Configuration Sprawl Over time, programs gain features and options. When they connect to external systems and services (e.g. databases, event brokers, cloud/web services), they must also keep a growing set of configs and credentials for those services handy.

The traditional places to store this information at runtime are global variables and OS environment variables. Both suck. Config data logically is "global environment" or context in which the app is running, but you can't easily depend on either global or environment variables.

The other traditional mechanism, config files--whether XML, INI, .properties, or whatever--help store config data between runs, but do nothing to organize configs/options for the program code, or during its execution.

You can clean things up a bit by making options into properties of your application's classes. This is the traditional "next step." Unfortunately, it can take a lot of up-front "what goes where??" thinking. IMO, more than it's worth. Even if you get those choices right, it won't last. If you have a reasonably feature-ful app, the number of options and settings will become overwhelming over time. You'll spend a lot of time hand-coding defaults and options in object constructors and the arguments/code of other methods. Trying to perfectly partition configurations among classes not only expends effort, it can lead to highly interdependent classes. So much for clean encapsulation!

I've banged my head against this particular wall often, especially when aiming for code that has "reasonable" or "intelligent" defaults and behaviors for everything, that allows users to override defaults at any time, and that presents a simple interface that doesn't require understanding the complete interplay of app classes and components to use its services.

Solution: Delegate to a Config Object The best solution I've found is to encapsulate option/config data into its own designated object. A fancy way of describing this pattern: For configuration, settings, and option data, use delegation rather than inheritance or composition.

How you build a config mapping depends on the language you're working in. In many languages, constructing your own Config object gives you a nice "look":

if opts.verbose:
    print "..."

which I find more readable than the more explicit "getter" opts.get("verbose") or "indexer" opts['verbose'] ways of accessing a property. But you usually don't have to make your own Config class, which basically is just a mapping.

The Easy Way ● Use a generic mapping: e.g. in Python a dict, in Perl a %hash, in Java a Dictionary or HashMap. Even better, there are extensions of these designed for, or especially suited to, configuration data. In Python, e.g., I use stuf and TreeDict for their simple dot-access and other nice properties. In Java, Properties is a similar specific-for-configs extension. E.g.:

from stuf import stuf   # stuf has attributes!

opts = stuf(
    show_module=False,  # comment explaining what show_module means
    where=True,         # ...
    truncate=False,     # ...
    everything=False,   # ...
    allvars=False,      # ...
    allkeys=False,      # ...
    yaml=False,         # ...
    on=True,            # ...
    ret=None,           # ...
)

if opts.truncate:
   ...

This way, all your config and option data is in one place, neatly accessible, and clearly delineated from all of the other program, class, instance, and function/method data it's used side-by-side with. This helps maintain clarity over time, as the program evolves. You can quickly determine "Is this part of the core data? Or is it related to the context in which the core data is being processed?"

To make things even better, if you pre-load config data from a config file, load or copy those values directly into your config object. And if you take arguments from the command line, load or copy those values directly into your config object. Now you have one unified source of all the "what does the user want me to do, with what options and settings?" information.


TL;DR - 90% of apps or services are just fine with a simple config/options mapping. Everything that follows is for advanced use cases. Because this was a design/patterns question, here's why this approach isn't a one-off, but extends to successively more sophisticated/intricate use cases.

Per-Instance Config ● You can have multiple levels of config/option data. The most common use for this would be defaults set at a class or module level, then potentially different options for each instance. A server app might have an instance per user, with each user/instance needing its own customized settings. The config map is copied at instance creation/initialization, either automatically or explicitly.

●● Multiple Config Objects ●● You can partition config/option data into several config objects, if that makes sense. For example, you might partition options for data retrieval from those for data formatting. You can do this at the start of the design, but need not. You can start with one monolithic config object, then refactor over time (generally, as you start to refactor the underlying functions). Obviously you don't want to "go crazy" adding config objects, but you can have a few without adding much program complexity. If you partition config objects, you can proxy multiple config "domains" through a single API--giving you quality information decomposition internally, but a very simple outward appearance.

Chain Gang ◆ More elegant than copying config data per instance: Use chainable or hierarchical mapping (e.g. in Python, ChainMap) that lets you "overlay" the values of one mapping with those of another (similar to "copy-on-write" schemes, or "union" and "translucent" file systems). Instance options then refer directly to class/default options--unless they are explicitly set, in which case they're instance-specific. Advantage: If class/default/global settings are changed during program execution, subsequent instance method invocations will "see" the changed defaults and use them (as long as they haven't been overridden at the instance level).

◆◆ Transient Config ◆◆ If you need configs/options changeable "on the fly"--say for the scope of a given method invocation--the method can extend the instance option chained mapping. In Python, that's what ChainMap.new_child() does. It sounds complicated, but as far as the method code is concerned, it's drop-dead-simple. There's still just a single config object to refer to, and whatever it says is the option, use it.

◆◆◆ Arbitrary Duration Overlay ◆◆◆ There's nothing magical about the temporal scope of method invocation. With the proper setup, any level of configuration can be transiently overlaid for as long as needed. For example, if there's some period during program run you'd like to turn on debugging, logging, or profiling, you can turn that on and off whenever you like--just for certain instances, or for all of them at once. This hors catégorie usage requires a Config object slightly beyond stock ChainMap--but not by much (basically just a handle to the chain mapping).

Happily, most code doesn't come close to needing these "black diamond" levels of config sophistication. But if you want to go three or four levels deep, delegating to separate config objects will take you there in a logical way that keeps code clean and orderly.

like image 71
Jonathan Eunice Avatar answered Oct 12 '22 23:10

Jonathan Eunice


I would recommend placing your options into some sort of file (whether it be xml or a .properties) especially if the values can change (such as a database username, etc). I would also say that you can break up these configuration files by component. Just like you break up your code, the components that need database information likely do not need image paths. So you can have a file for database info, image path info, etc. Then have your components load the file that they need.

In the java-specific case, you can put these files on your classpath and have your components reference them. I like using .properties files because java has a nice class, Properties, for dealing with them.

So here's a tiny example for an image provider that gives you a BufferedImage, given the filename.

image.properties

 icon.path=/path/to/my/icons
 background.path=/path/to/my/backgrounds

Make sure this file is on your classpath. Then here's my provider class

 public class ImageProvider {
    private static final String PROP_FILE = "image.properties"; 
    private static final String ICON_PATH = "icon.path";

    private Properties properties; 

    public ImageProvider() {
      properties = new Properties();
      properties.load(getClass().getClassLoader().getResourceAsStream(PROP_FILE));
    }

    public BufferedImage getIcon(String icon) {
      return ImageIO.read(properties.getProperty(ICON_PATH) + icon);
    }
}
like image 43
jeff Avatar answered Oct 12 '22 23:10

jeff