I have the following problem with my configuration class:
Let's say i have something in the lines of:
interface IConfiguration
{
IEnumerable<IItemConfiguration> ItemConfigurations { get; }
}
interface IItemConfiguration
{
IDbConfiguration DbConfiguration { get; }
INotificationConfiguration NotificationConfiguration { get; }
}
interface IDbConfiguration
{
string ConnectionString { get; }
}
interface INotificationConfiguration
{
IEmailConfiguration EmailConfiguration { get; }
IPhoneConfiguration PhoneConfiguration { get; }
}
interface IEmailConfiguration
{
//primitive stuff here
}
interface IPhoneConfiguration
{
//primitive stuff here
}
interface ISmsConfiguration
{
//primitive stuff here
}
Now, I have a separate library in which i implement these interfaces using Xml. This example applies to all interfaces, i have an initialization method that takes an XElement from which all elements are read.
class XmlNotificationConfiguration : INotificationConfiguration
{
private IEmailConfiguration _emailConfiguration;
public IEmailConfiguration EmailConfiguration
{
get { return _emailConfiguration; }
}
private IPhoneConfiguration _phoneConfiguration;
public IPhoneConfiguration PhoneConfiguration
{
get { return _phoneConfiguration; }
}
public bool Initialize(XElement xElement)
{
//parse xElement for the XElement representing the EmailConfiguration
//var emailConfigElement = ...
//parse xElement for the XElement representing the PhoneConfiguration
//var phoneConfigElement = ...
//Coupling here !!!
_emailConfiguration = new XmlEmailConfiguration();
_emailConfiguration.Initialize(emailConfigElement);
//Coupling here !!!
_phoneConfiguration = new XmlPhoneConfiguration();
_phoneConfiguration.Initialize(phoneConfigElement);
return true;
}
}
So basically i've modelled all the configuration to be able to use at a time maybe an xml configuration, maybe a database one, maybe a csv one, etc.
But the issue is that all these implementations are coupled. If i choose to have an xml Configuration, i'll end up with all the inner classes (EmailConfiguration, DbConfiguration, PhoneConfiguration etc) being xml as well. What i want is to be able to have maybe EmailConfiguration read from an xml, PhoneConfiguration read from the db, PhoneConfiguration read from csv.
The main issue i have with this is the Initialize method. For an xml implementation, it takes an XElement as a parameter, for a csv implementation it might take a line, for a db implementation it might take a connection string, an userid etc, so different objects. If it were the same parameter, it would have been very easy: simply add Initialize to the interfaces for the configuration objects, and have factories for each configuration, pass those factories and create the objects. Unfortunatelly this does not apply.
I have 2 solutions until now:
i) Make all the parameter classes (XElement, csv line etc) implement an interface, in which they can all provide their inner data through a dictionary with string values, which i can then deserialize according to the component implementation (deserialize to XElement inside my Xml library, deserialize to csv line inside my csv library etc). Then i will pass factories which will create my components based on some IParameter interface implemented by all parameter classes. This seems very hackish and is also prone to runtime errors.
ii) Go the generics way: Also use factories, but generic ones. But this is really weird, the interfaces will look like:
interface IConfiguration<T, Q, R>
{
bool Initialize(T emailParameters, Q phoneParameters, R dbParameters);
IEnumerable<IItemConfiguration<T,Q,R>> ItemConfigurations { get; }
}
interface IItemConfiguration<T, Q, R>
{
bool Initialize(T emailParameters, Q phoneParameters, R dbParameters);
IDbConfiguration<R> DbConfiguration { get; }
INotificationConfiguration<T, Q> NotificationConfiguration { get; }
}
interface IDbConfiguration<T>
{
bool Initialize(T parameters);
string ConnectionString { get; }
}
interface INotificationConfiguration<T, Q>
{
bool Initialize(T emailParameters, Q phoneParameters);
IEmailConfiguration<T> EmailConfiguration { get; }
IPhoneConfiguration<Q> PhoneConfiguration { get; }
}
interface IEmailConfiguration<T>
{
bool Initialize(T parameters);
//primitive stuff here
}
interface IPhoneConfiguration<T>
{
bool Initialize(T parameters);
//primitive stuff here
}
interface ISmsConfiguration<T>
{
bool Initialize(T parameters);
//primitive stuff here
}
Then i can pass factories again for all the components, but this time generic factories. This seems ugly because of 2 reasons:
a) Sometimes e.g. in INotificationConfiguration T and Q will be the same type if i want to take email and phone both from xml. This seems redundant.
b) If i have more and more nested interfaces (which i will have) for configurations, i'll end up with like 10-15 generics.
So the question is, what would be the best solution. I hope all of the code is clear, if not i will update it.
LE: This i hope the most clear example i have:
using Ninject;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var notificationConfigType = ConfigType.Xml;
var commonObject = "some Common object";
var emailConfigType = ConfigType.Csv;
var phoneConfigType = ConfigType.Xml;
IKernel kernel = new StandardKernel(new [] { new InitModule()});
kernel.Load(Assembly.GetExecutingAssembly());
var builder = kernel.Get<INotificationConfigurationBuilder>();
var notificationConfig = builder.Build(notificationConfigType, commonObject, emailConfigType, phoneConfigType);
}
}
enum ConfigType
{
Csv,
Xml
}
interface INotificationConfiguration
{
bool Initialize(string commonObject, ConfigType emailConfigType, ConfigType phoneConfigType);
IEmailConfiguration EmailConfiguration { get; }
IPhoneConfiguration PhoneConfiguration { get; }
}
interface IEmailConfiguration
{
bool Initialize(string commonObject);
//primitives...
}
class XmlEmailConfiguration : IEmailConfiguration
{
public bool Initialize(string commonObject)
{
//do the actual parsing here
return true;
}
}
class CsvEmailConfiguration : IEmailConfiguration
{
public bool Initialize(string commonObject)
{
//do the actual parsing here
return true;
}
}
interface IPhoneConfiguration
{
bool Initialize(string commonObject);
//primitive stuff here
}
class XmlPhoneConfiguration : IPhoneConfiguration
{
public bool Initialize(string commonObject)
{
//do the actual parsing here
return true;
}
}
class CsvPhoneConfiguration : IPhoneConfiguration
{
public bool Initialize(string commonObject)
{
//do the actual parsing here
return true;
}
}
class XmlNotificationConfiguration : INotificationConfiguration
{
private IEmailConfigurationBuilder _emailBuilder;
private IPhoneConfigurationBuilder _phoneBuilder;
public XmlNotificationConfiguration(IEmailConfigurationBuilder emailBuilder, IPhoneConfigurationBuilder phoneBuilder)
{
_emailBuilder = emailBuilder;
_phoneBuilder = phoneBuilder;
}
private IEmailConfiguration _emailConfiguration;
public IEmailConfiguration EmailConfiguration
{
get { return _emailConfiguration; }
}
private IPhoneConfiguration _phoneConfiguration;
public IPhoneConfiguration PhoneConfiguration
{
get { return _phoneConfiguration; }
}
public bool Initialize(string commonObject, ConfigType emailConfigType, ConfigType phoneConfigType)
{
//normally this would be a processing of the commonObject
var emailCommonObject = "abc";
//normally this would be a processing of the commonObject
var phoneCommonObject = "drf";
_emailConfiguration = _emailBuilder.Build(emailConfigType, emailCommonObject);
_phoneConfiguration = _phoneBuilder.Build(phoneConfigType, phoneCommonObject);
return true;
}
}
class CsvNotificationConfiguration : INotificationConfiguration
{
private IEmailConfigurationBuilder _emailBuilder;
private IPhoneConfigurationBuilder _phoneBuilder;
public CsvNotificationConfiguration(IEmailConfigurationBuilder emailBuilder, IPhoneConfigurationBuilder phoneBuilder)
{
_emailBuilder = emailBuilder;
_phoneBuilder = phoneBuilder;
}
public bool Initialize(string commonObject, ConfigType emailConfigType, ConfigType phoneConfigType)
{
throw new NotImplementedException();
}
public IEmailConfiguration EmailConfiguration
{
get { throw new NotImplementedException(); }
}
public IPhoneConfiguration PhoneConfiguration
{
get { throw new NotImplementedException(); }
}
}
interface IEmailConfigurationBuilder
{
IEmailConfiguration Build(ConfigType type, string commonObject);
}
class EmailConfigurationBuilder : IEmailConfigurationBuilder
{
public IEmailConfiguration Build(ConfigType type, string commonObject)
{
switch (type)
{
case ConfigType.Csv:
var config = new CsvEmailConfiguration();
config.Initialize(commonObject);
return config;
case ConfigType.Xml:
var cfg = new XmlEmailConfiguration();
cfg.Initialize(commonObject);
return cfg;
default:
throw new ArgumentOutOfRangeException();
}
}
}
interface IPhoneConfigurationBuilder
{
IPhoneConfiguration Build(ConfigType type, string commonObject);
}
class PhoneConfigurationBuilder : IPhoneConfigurationBuilder
{
public IPhoneConfiguration Build(ConfigType type, string commonObject)
{
switch (type)
{
case ConfigType.Csv:
var config = new CsvPhoneConfiguration();
config.Initialize(commonObject);
return config;
case ConfigType.Xml:
var cfg = new XmlPhoneConfiguration();
cfg.Initialize(commonObject);
return cfg;
default:
throw new ArgumentOutOfRangeException();
}
}
}
interface INotificationConfigurationBuilder
{
INotificationConfiguration Build(ConfigType type, string commonObject, ConfigType emailConfigType, ConfigType phoneConfigType);
}
class NotificationConfigurationBuilder : INotificationConfigurationBuilder
{
private IEmailConfigurationBuilder _emailBuilder;
private IPhoneConfigurationBuilder _phoneBuilder;
public NotificationConfigurationBuilder(IEmailConfigurationBuilder emailBuilder, IPhoneConfigurationBuilder phoneBuilder)
{
_phoneBuilder = phoneBuilder;
_emailBuilder = emailBuilder;
}
public INotificationConfiguration Build(ConfigType type, string commonObject, ConfigType emailConfigType, ConfigType phoneConfigType)
{
switch (type)
{
case ConfigType.Csv:
var config = new CsvNotificationConfiguration(_emailBuilder, _phoneBuilder);
config.Initialize(commonObject, emailConfigType, phoneConfigType);
return config;
case ConfigType.Xml:
var cfg = new XmlNotificationConfiguration(_emailBuilder, _phoneBuilder);
cfg.Initialize(commonObject, emailConfigType, phoneConfigType);
return cfg;
default:
throw new ArgumentOutOfRangeException();
}
}
}
class InitModule : Ninject.Modules.NinjectModule
{
public override void Load()
{
Bind<IPhoneConfigurationBuilder>().To<PhoneConfigurationBuilder>();
Bind<IEmailConfigurationBuilder>().To<EmailConfigurationBuilder>();
Bind<INotificationConfigurationBuilder>().To<NotificationConfigurationBuilder>();
}
}
}
i) I've simplified things to keep it simple.
ii) Yes, i have some initializations in factories instead of the ninject module, but that i will need to figure out how to fix (I understand that ninject has an extension for factories, hope that will help me).
iii) This example is the most optimistic one, in which both Csv and Xml components need the same input parameters to initialize themselves - the easy solution mentioned in my post.
Unfortunately, in my real life case, Xml and Csv don't have the parameter object in common, thus the 2 solutions from above, of which none is to my liking.
Again, i could simply have some interface ICommonParameter commonObject instead of string commonObject, with both csv and xml implementing this common parameter, the interface exposing some string inside it, which can be then deserialized in xml, or in csv. Feels hackish, why wouldn't i use object for the same thing and then cast it to XElement or CSVLine ? There is almost no advantage of this solution compared to using object. And both this solution and using object for passing the initialization parameter would cause runtime issues.
And the second solution, which would imply exponentially more and more generics (basically each inner config component would require another generic in its parent's definition, and then i'll also need to be adding generic factories).