-1

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).

ImmoralWombat
  • 395
  • 2
  • 11
  • 2
    Will you ever need both an XmlEmailConfiguration and a CsvEmailConfiguration in the same application? – wigy Nov 10 '15 at 15:21
  • @wigy That is a good question, that i haven't thought about until now, but no, for now i won't need an XmlEmailConfiguration and a CsvEmailConfiguration at the same time. This applies to all interfaces as well. – ImmoralWombat Nov 10 '15 at 15:34
  • Considered having the end result be a plain old data class, instead of using all these interfaces where the object that contains the data also does the parsing? I feel like this would let you build a much simpler hierarchy (with many fewer interfaces). In general it seems odd to me to make an interface for *data*... I use interfaces for capabilities rather than data shape. – 31eee384 Nov 10 '15 at 15:40
  • @31eee384 Hi, the end result is to mainly use these interfaces inside ninject, and to have different ninject configurations so that i can: a) switch between component types at any time (xml, etc) b) be able to fake at any point any configuration easily.This is just me experimenting with IoC and DI, so i may be wrong in having excessive interfaces. If I am to go your way, where would i put the implementation for parsing ? – ImmoralWombat Nov 10 '15 at 15:46
  • I'm assuming your solution would be to have plain old data classes for e.g. DbConfiguration, EmailConfiguration, but have the parsing being done externally within strategies ? If i create an interface for e.g. IDbConfigurationParser, I fear i will reach the same issues, again because of the XElement which is not interchangeable with e.g. CSVLine – ImmoralWombat Nov 10 '15 at 15:52
  • I've reconsidered a few things from my last comment (that I deleted): I still think that it makes sense for the data itself to be a plain old class, but if you're using some sort of dependency injection you should really go to town with it--I don't see any evidence of DI in the question now. If I remember (and the question is clarified) I'll add an answer about how I'd rearrange it with DI, but I don't know any mainstream DI libraries so I'll have to make something up. – 31eee384 Nov 10 '15 at 19:50
  • @31eee384 I've updated my original post. Hope it is clear now. – ImmoralWombat Nov 10 '15 at 20:54
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94818/discussion-between-31eee384-and-immoralwombat). – 31eee384 Nov 11 '15 at 15:27
  • 1
    @ImmoralWombat: Regarding your flag on moving this question to the correct site, I'm not 100% sure Code Review will be any better than Stack Overflow. See http://meta.stackexchange.com/questions/129598/which-computer-science-programming-stack-exchange-do-i-post-in. – Matt Nov 19 '15 at 21:19

1 Answers1

0

I would definitely separate creation of the configuration items from the interfaces used by their clients, so Initialize(T parameters) should not be exposed anywhere by IConfiguration.

Having multiple IItemConfiguration instances, you would need at least an internal identifier for those items if you wanted to separate sub-configurations into their own factories. If you could afford, just try having a single factory for the whole configuration.

namespace ConfigurationExperiment
{
    using System.Collections.Generic;
    using System.Xml.Linq;
    using System.Xml.XPath;

    public interface IConfiguration
    {
        IEnumerable<IItemConfiguration> ItemConfigurations { get; }
    }

    public interface IItemConfiguration
    {
        IDbConfiguration DbConfiguration { get; }
        INotificationConfiguration NotificationConfiguration { get; }
    }

    public interface IDbConfiguration
    {
        string ConnectionString { get; }
    }

    public interface INotificationConfiguration
    {
        IEmailConfiguration EmailConfiguration { get; }
        IPhoneConfiguration PhoneConfiguration { get; }
        ISmsConfiguration SmsConfiguration { get; }
    }

    public interface IEmailConfiguration
    {
        //primitive stuff here
    }

    public interface IPhoneConfiguration
    {
        //primitive stuff here
    }

    public interface ISmsConfiguration
    {
        //primitive stuff here
    }

    public interface IConfigurationRepository
    {
        IConfiguration GetConfiguration();
    }

    internal class ConfigurationRepository : IConfigurationRepository
    {
        private readonly XElement _configurationElement;

        public ConfigurationRepository(XElement configurationElement)
        {
            _configurationElement = configurationElement;
        }

        public IConfiguration GetConfiguration()
        {
            var result = new Configuration();
            foreach (var itemElement in _configurationElement.XPathSelectElements("//item"))
            {
                var item = new ItemConfiguration();
                //item.DbConfiguration = ...;
                //item.NotificationConfiguration = ...;
                result.Add(item);
            }
            return result;
        }

        private class Configuration : IConfiguration
        {
            private readonly List<ItemConfiguration> _itemConfigurations;

            public Configuration()
            {
                _itemConfigurations = new List<ItemConfiguration>();
            }

            public IEnumerable<IItemConfiguration> ItemConfigurations
            {
                get { return _itemConfigurations; }
            }

            public void Add(ItemConfiguration item)
            {
                _itemConfigurations.Add(item);
            }
        }

        private class ItemConfiguration : IItemConfiguration
        {
            public IDbConfiguration DbConfiguration { get; set; }
            public INotificationConfiguration NotificationConfiguration { get; set; }
        }
    }
}
wigy
  • 2,174
  • 19
  • 32
  • Hi, I've added an edit to my original post. Hopefully what i want is more clear now. This does not solve my problem, because i will still have "new" operators outside of my ninject modules, which i want to avoid (at the very least i want to keep the "new" operators inside just ninject and my factories) – ImmoralWombat Nov 10 '15 at 20:58