【问题标题】:Decoupling configuration subcomponents within the same configuration在同一配置中解耦配置子组件
【发布时间】:2015-11-10 15:13:32
【问题描述】:

我的配置类有以下问题:

假设我有以下内容:

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
}

现在,我有一个单独的库,我在其中使用 Xml 实现这些接口。这个例子适用于所有接口,我有一个初始化方法,它接受一个 XElement,从中读取所有元素。

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;
    }
}

所以基本上我已经对所有配置进行了建模,以便能够一次使用,可能是 xml 配置,可能是数据库配置,可能是 csv 配置,等等。

但问题是所有这些实现都是耦合的。如果我选择使用 xml 配置,我最终会得到所有内部类(EmailConfiguration、DbConfiguration、PhoneConfiguration 等)也是 xml。 我想要的是能够从 xml 读取 EmailConfiguration,从 db 读取 PhoneConfiguration,从 csv 读取 PhoneConfiguration。

我遇到的主要问题是 Initialize 方法。对于 xml 实现,它需要一个 XElement 作为参数,对于 csv 实现它可能需要一行,对于一个 db 实现它可能需要一个连接字符串、一个用户 ID 等,因此不同的对象。如果它是相同的参数,那将非常简单:只需将 Initialize 添加到配置对象的接口,并为每个配置都有工厂,传递这些工厂并创建对象。不幸的是,这不适用。

到目前为止,我有 2 个解决方案:

i) 使所有参数类(XElement、csv line 等)实现一个接口,在该接口中它们都可以通过带有字符串值的字典提供其内部数据,然后我可以根据组件实现对其进行反序列化(反序列化为我的 Xml 库中的 XElement,反序列化为我的 csv 库中的 csv 行等)。然后我将传递工厂,这些工厂将基于所有参数类实现的一些 IParameter 接口创建我的组件。这看起来很 hackish,而且还容易出现运行时错误。

ii) 采用泛型方式: 也使用工厂,但通用的。但这真的很奇怪,界面看起来像:

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
}

然后我可以再次为所有组件传递工厂,但这次是通用工厂。 这看起来很难看,原因有两个:

a) 有时,例如如果我想从 xml 获取电子邮件和电话,则在 INotificationConfiguration T 和 Q 将是相同的类型。这似乎是多余的。

b) 如果我有越来越多的嵌套接口(我将拥有)用于配置,我最终会得到 10-15 个泛型。

所以问题是,最好的解决方案是什么。我希望所有代码都清楚,否则我会更新它。

LE:我希望这是我最清楚的例子:

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) 我已经简化了一些事情以保持简单。

ii) 是的,我在工厂而不是 ninject 模块中有一些初始化,但我需要弄清楚如何修复(我知道 ninject 有工厂扩展,希望对我有所帮助)。

iii) 这个例子是最乐观的一个,其中 Csv 和 Xml 组件都需要相同的输入参数来初始化自己 - 我的帖子中提到的简单解决方案。

不幸的是,在我的现实生活中,Xml 和 Csv 没有共同的参数对象,因此上面的 2 个解决方案,其中没有一个是我喜欢的。

再一次,我可以简单地使用一些接口 ICommonParameter commonObject 而不是字符串 commonObject,csv 和 xml 都实现了这个公共参数,接口在其中公开了一些字符串,然后可以在 xml 或 csv 中反序列化。感觉 hackish,为什么我不将 object 用于同一件事,然后将其转换为 XElement 或 CSVLine ?与使用对象相比,这种解决方案几乎没有优势。而且这种解决方案和使用对象传递初始化参数都会导致运行时问题。

第二个解决方案意味着越来越多的泛型(基本上每个内部配置组件都需要在其父级定义中使用另一个泛型,然后我还需要添加泛型工厂)。

【问题讨论】:

  • 您会在同一个应用程序中同时需要 XmlEmailConfiguration 和 CsvEmailConfiguration 吗?
  • @wigy 这是一个很好的问题,直到现在我还没有考虑过,但是不,现在我不需要同时使用 XmlEmailConfiguration 和 CsvEmailConfiguration。这也适用于所有接口。
  • 认为最终结果是一个普通的旧数据类,而不是使用包含数据的对象也进行解析的所有这些接口?我觉得这会让你建立一个更简单的层次结构(接口更少)。一般来说,为 data 创建一个接口对我来说似乎很奇怪……我将接口用于功能而不是数据形状。
  • @31eee384 你好,最终的结果是主要使用ninject内部的这些接口,并有不同的ninject配置,以便我可以:a)随时切换组件类型(xml等)b ) 能够在任何时候轻松伪造任何配置。这只是我在尝试 IoC 和 DI,所以我可能在拥有过多接口方面是错误的。如果我按照你的方式进行,我会将解析的实现放在哪里?
  • @ImmoralWombat:关于将这个问题移至正确站点的标志,我不能 100% 确定 Code Review 会比 Stack Overflow 更好。见meta.stackexchange.com/questions/129598/…

标签: c# generics interface decoupling


【解决方案1】:

我肯定会将配置项的创建与其客户端使用的接口分开,因此Initialize(T parameters) 不应在任何地方被IConfiguration 暴露。

拥有多个 IItemConfiguration 实例,如果您想将子配置分离到它们自己的工厂中,则至少需要这些项目的内部标识符。如果您负担得起,请尝试为整个配置创建一个工厂。

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; }
        }
    }
}

【讨论】:

  • 您好,我已对我的原始帖子进行了编辑。希望我现在想要的更清楚。这并不能解决我的问题,因为我的 ninject 模块之外仍然会有“新”操作员,我想避免这种情况(至少我想将“新”操作员保留在 ninject 和我的工厂中)跨度>
猜你喜欢
  • 1970-01-01
  • 2020-10-16
  • 2015-08-03
  • 2015-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多