C# Static constructors design problem - need to specify parameter
I have a re-occurring design problem with certain classes which require one-off initialization with a parameter such as the name of an external resource such as a config file.
For example, I have a corelib project which provides application-wide logging, configuration and general helper methods. This object could use a static constructor to initialize itself but it need access to a config file which it can't find itself.
I can see a couple of solutions, but both of these don't seem quite right:
1) Use a constructor with a parameter. But then each object which requires corelib functionality should also know the name of the config file, so this has to be passed around the application. Also if I implemented corelib as a singleton I would also have to pass the config file as a parameter to the GetInstance method, which I believe is also not right.
2) Create a static property or method to pass through the config file or other external parameter.
I have sort of used the latter method and created a Load method which initializes an inner class which it passes through the config file 开发者_开发百科in the constructor. Then this inner class is exposed through a public property MyCoreLib.
public static class CoreLib
{
private static MyCoreLib myCoreLib;
public static void Load(string configFile)
{
myCoreLib = new MyCoreLib(configFile);
}
public static MyCoreLib MyCoreLib
{
get { return myCoreLib; }
}
public class MyCoreLib
{
private string configFile;
public MyCoreLib(string configFile)
{
this.configFile = configFile;
}
public void DoSomething()
{
}
}
}
I'm still not happy though. The inner class is not initialized until you call the load method, so that needs to be considered anywhere the MyCoreLib is accessed. Also there is nothing to stop someone calling the load method again.
Any other patterns or ideas how to accomplish this?
You need a common location to store this. You could use the app.config even if this is a seperate assembly by defining a config section in the library assembly and referencing it you proceess app.config. Or you could just add a generic setting to appSettings and reference that without using strongly typed settings. If the value is user entered then you could use isolated storage. Finally you could put it in a well known location in the registry at install time.
For code the following is encapsulted better
public interface ICoreLib
{
void SomeMethod();
}
public static class CoreLibManager
{
private static ICoreLib coreLib;
private static volatile bool initialized;
private static readonly object lockObject = new object();
public static ICoreLib CoreLib
{
get
{
Inititialize();
return coreLib;
}
}
/// <summary>
/// The inititialize.
/// </summary>
private static void Inititialize()
{
if (initialized)
{
lock (lockObject)
{
if (!initialized)
{
string configFile = // Fech from common location
coreLib = new MyCoreLib(configFile);
initialized = true;
}
}
}
}
/// <summary>
/// The my core lib.
/// </summary>
private class MyCoreLib : ICoreLib
{
public MyCoreLib(string configPath)
{
}
public void SomeMethod()
{
}
}
}
When you have global state like this that you need to initialize and it requires outside input to complete initialization (such as a config file), then you're stuck with the outside code that knows about the input having to call Load or Initialize to initialize your global state. There's no way around this.
The issue that you've correctly observed, however, is that anybody could try to use the global state before it has been properly Initialized, which is the downside to having it exposed in this way. The way you get around this is by refactoring all of the stateful parts of your global library into an instance class and passing references to that instance through your application. Because you control when it is created and initialized, you can now ensure it has valid state before you pass it along. You trade off the convenience of global state for the better insulation you're after.
You can use the .net configuration system to do this. The simplest way to do this is to use the <appsettings>
element in your web.config file, or in your appname.exe.config file. Use:
ConfigurationManager.AppSettings["property_name"]
to access a property. Whether your code is running in a web context or as a windows app, the configuration system will find the config file and load the values you need.
You can also build a more complex system with type-safe config values and hierarchical data, but my needs were pretty simple, so I didn't explore this after the first headache set in :)
You could make the class a singleton, thus ensuring there's only ever one instance of it while also allowing for the constructor parameter.
There won't be much alternatives.
Either you pass stuff around (in that case I'd not pass the string around but create a concrete (non-static) CoreLib and pass that around or you do what you suggested.
Don't forget to hide the constructor for MyCoreLib. Currently it is public which is likely unintended.
OK, thanks everyone for your assistance. I've refactored the CoreLib project and broken out the config handling into a separate project. Now we have a solution-wide shared class for config management. The class can take care of itself with a user setting, which is exposed via the static property ConfigFile. This property also persists the modified file location if the user changes via some config dialog. Also the initialized flag will be reset if the config file changes.
public interface IConfig
{
void SomeMethod();
}
public static class ConfigurationManager
{
private static IConfig config;
private static volatile bool initialized;
private static readonly object lockObject = new object();
public static string ConfigFile
{
get { return Properties.Settings.Default.ConfigFile; }
set
{
if (Properties.Settings.Default.ConfigFile == value) return;
lock (lockObject)
{
Properties.Settings.Default.Save();
initialized = false;
}
}
}
public static IConfig Config
{
get
{
Inititialize();
return config;
}
}
private static void Inititialize()
{
lock (lockObject)
{
if (initialized) return;
config = new Configuration(Properties.Settings.Default.ConfigFile);
initialized = true;
}
}
}
internal class Configuration : IConfig
{
public ClientConfig(string configFile)
{
// Parse & validate config file
}
public void SomeMethod()
{
}
}
So now in the start-up we first validate the persisted ConfigFile setting, then attempt to access the Configuration instance via the manager's Config property. Any parsing exceptions can be handled here and dealt with accordingly. Then it is up to the developer to handle any exceptions for IConfig's methods.
if (!System.IO.File.Exists(ConfigurationManager.ConfigFile))
{
// Display config file locator dialog
ConfigurationManager.ConfigFile = someDialog.FileName;
}
try
{
IConfig config = ConfigurationManager.Config;
}
catch
{
// Error parsing config file
}
精彩评论