【问题标题】:c# programming code optimizationc# 编程代码优化
【发布时间】:2012-04-01 22:36:58
【问题描述】:

我有大约 20 个类派生自 ConvertApi 类。每个类都从父类共享Convert 方法,并具有唯一的属性SupportedFiles。我使用这些类进行文件操作,我的代码看起来像

if (fileEx=="txt")
   new Text2Pdf().Convert(file)
else
  if (fileEx=="doc")
    new Word2Pdf().Convert(file)
     else
    //and so on..

我知道这些代码可以优化,因为 if 运算符重复了 20 次,这看起来很糟糕,但找不到办法做到这一点。谁能帮帮我?

class Text2Pdf : ConvertApi
{
  enum SupportedFiles { txt, log }; 
}

class Word2Pdf : ConvertApi
{
  enum SupportedFiles { doc, docx }; 
}

class Excel2Pdf : ConvertApi
{
  enum SupportedFiles { xls, xlsx }; 
}

class ConvertApi
{
 public void Convert(....);
}

【问题讨论】:

  • 首先,把各种if (fileEx="txt")改成if (fileEx=="txt")。编辑:您已经编辑了您的问题。现在很好。
  • C#中缺少虚拟类方法的一种情况很烦人。
  • 那些枚举真的是属性吗?不是每个类的嵌套枚举类型吗?
  • 我添加了枚举作为示例,无论在我的情况下使用什么。我只是想说明问题。
  • 你覆盖了所有 20 个类中的 Convert 方法?

标签: c# class


【解决方案1】:

使用switch 怎么样?

switch(fileEx){
    case "txt": new Text2Pdf().Convert(file); break;
    case "doc": new Word2Pdf().Convert(file); break;
}

【讨论】:

  • 不推荐 - 这是一个anti-pattern
  • 您应该利用 OO 多态性。即ConvertAPI x = new Text2Pdf(); x.Convert(file);,但这是一个简短的答案,更长的答案是使用工厂或依赖注入,但我无法在这里详细说明,因为我在手机上。
【解决方案2】:

在类的构造函数中传入支持的文件时使用依赖注入。

【讨论】:

    【解决方案3】:

    假设每个转换器都是无状态的,听起来你只想要一个Dictionary<string, ConvertApi>

    private static readonly Dictionary<string, ConvertApi> ConverterByType =
        new Dictionary<string, ConvertApi>
    {
        { "txt", new Text2PdfConverter() },
        { "doc", new Word2PdfConverter() },
        ...
    };
    
    ...
    
    ConvertApi converter;
    if (!ConverterByType.TryGetValue(fileEx, out converter))
    {
        // No converter available... do whatever
    }
    else
    {
        converter.Convert(file);
    }
    

    (对于支持多个扩展的任何转换器,字典初始化最终会创建比您真正需要的更多的转换器,但这是另一回事。)

    如果您每次都需要一个新的转换器,请将其设为 Dictionary&lt;string, Func&lt;ConvertApi&gt;&gt; 并将其填充为:

    { "txt", () => new Text2PdfConverter() },
    { "doc", () => new Word2PdfConverter() },
    

    ...然后在需要时调用委托来获取转换器。

    当然,这会将所有初始化都放在一个地方 - 您可能想要一种方法让转换器注册他们使用某种类型的“转换器提供者”理解的扩展。

    【讨论】:

    • 如果转换器支持多个文件扩展名会怎样? :)
    • @Moo-Juice:正在为此进行编辑 - 您最终会得到多个实例,除非您采取措施避免它。这绝对是可以避免的,但我会先选择最简单的选项。
    • 这个解决方案在我的情况下不起作用,因为我失去了访问子类属性的可能性。您的代码返回 ConvertApi 父类,但我需要打赌子类,因为子类将包含其他属性和方法。
    • @JonSkeet 但是他仍然在用每种类型填充一个集合。每当制作新的 ConvertApi 或其中一个支持新的扩展时,都必须更新代码。我试图在my attempt 中解决这个问题
    • @Baboon:是的,这就是最后一段的内容。
    【解决方案4】:

    在你的基类中,有这样的东西:

    public abstract class ConvertApi
    {
        protected abstract string[] SupportedFilesImpl();
    
        public bool Supports(string ext)
        {
            return SupportedFilesImpl.Contains(ext);
        }
    }
    

    现在,您的派生类可以实现此方法:

    public class Text2PDF : ConvertApi
    {
        protected override string[] SupportedFilesImpl { return new string[] { "txt", "log"}; }
    }
    
    public class Doc2PDF : ConvertApi
    {
        protected override string[] SupportedFilesImpl { return new string[] { "doc", "docx"}; }
    }
    

    ... 对于其余的转换器,依此类推。然后把这些放在一个列表中......

    List<ConvertApi> converters = new List<ConvertApi>();
    converters.Add(new Text2PDF());
    converters.Add(new Doc2PDF());
    

    (注意,我可能会有一个包含这些的类,而不仅仅是一个列表,但无论如何)。现在,寻找转换器:

    foreach(ConvertApi api in converters)
    {
        if(api.Supports(fileExt))
        {
            // woo!
            break;
        }
    }
    

    【讨论】:

    • 此解决方案不适用于我的情况,因为我失去了访问子类属性的可能性。
    • @Tomas,如果此时您需要访问子属性,那么您的设计就完蛋了。 :)
    • 我刚刚使用动态关键字和您提供的代码解决了这个问题。
    【解决方案5】:

    您需要在这里使用abstract factory pattern。您的所有文本转换器都应从实现您的 Convert 方法的通用接口 ITextConverter 派生。文件扩展名将是您工厂的参数。下面是一个示例(“即时”键入,有时从我的源代码中复制粘贴代码,因此可能存在拼写错误。这里的目标是为您提供一个灵活实现的总体思路)。

    public interface IFileConverter
    {
        bool Convert(string filePath);
    }
    
    
    public static class FileConverterFactory
    {
        public static IFileConverter Create(string extension)
        {
            extension = type.ToUpper();
    
            Dictionary<string, ConverterConfig> availableConverters = GetConvertersConfig();
    
            if (!availableConverters.ContainsKey(extension))
                throw new ArgumentException(string.Format("Unknown extenstion type '{0}'. Check application configuration file.", extension));
    
            ConverterConfig cc = availableConverters[extension];
            Assembly runnerAssembly = Assembly.LoadFrom(cc.Assembly);
            Type converterType = runnerAssembly.GetType(cc.Class);
    
            IFileConverter converter = (IFileConverter) Activator.CreateInstance(converterType);
    
            return converter;
        }
    
    
        private static Dictionary<string, ConverterConfig> GetConvertersConfig()
        {
            var configs = (Dictionary<string, ConverterConfig>) ConfigurationManager.GetSection("ConvertersConfig");
    
            return configs;
        }
    }
    
    
    public class ConvertersConfigHandler : IConfigurationSectionHandler
    {
        public object Create(object parent, object configContext, XmlNode section)
        {
            Dictionary<string, ConverterConfig> converters = new KeyedList<string, ConverterConfig>();
            XmlNodeList converterList = section.SelectNodes("Converter");
    
            foreach (XmlNode converterNode in converterList)
            {
                XmlNode currentConverterNode = converterNode;
    
                ConverterConfig cc = new ConverterConfig();
                cc.Type = XML.GetAttribute(ref currentConverterNode, "Type").ToUpper();
                cc.Assembly = XML.GetAttribute(ref currentConverterNode, "Assembly");
                cc.Class = XML.GetAttribute(ref currentConverterNode, "Class");
    
                converters[cc.Type] = cc;
            }
    
            return converters;
        }
    }
    
    
    public class ConverterConfig
    {
        public string Type = "";
        public string Assembly = "";
        public string Class = "";
    }
    
    
    public class TextConverter : IFileConverter
    {
       bool Convert(string filePath) { ... }
    }
    
    
    public class PdfConverter : IFileConverter
    {
       bool Convert(string filePath) { ... }
    }
    

    在您的 app.config 文件中,将其添加到 configSections:

    <section name = "ConvertersConfig" type = "ConvertersConfigConfigHandler, MyAssembly" />
    

    在您的 configSections 下方:

    <ConvertersConfig>
        <Converter Type="txt" Assembly="MyAssembly" Class="MyAssembly.TextConverter" />
        <Converter Type="pdf" Assembly="MyAssembly" Class="MyAssembly.PdfConverter" />
    </ConvertersConfig>
    

    然后调用将是这样的:

    IFileConverter converter = FileConverterFactory.Create("txt");
    converter.Convert("c:\temp\myfile");
    

    编辑代码以提供更“通用”的解决方案。

    【讨论】:

    • 最好,恕我直言,如果ITextConverter 在这里定义了一个属性来获取它能够转换的文件,否则“可用”转换本质上与工厂相关联,当它是了解此信息的转换器本身。
    • @Moo-Juice:你是对的,当然。只需即时键入代码以显示一个简单的示例。事实上,如果我必须自己实现这个东西,我会使用依赖注入+抽象工厂。
    【解决方案6】:

    更慢,但更动态...使用对象工厂。这是一篇似乎符合您需求的好文章。

    http://www.codeproject.com/Articles/12986/Generic-Object-Factory

    文章中的重要内容:

    using System.Collections.Generic;
    
    public struct Factory < KeyType, GeneralProduct > 
    {
        //Register a object with the factory
        public void> Register< SpecificProduct >(KeyType key)
            where SpecificProduct : GeneralProduct, new()
        {
            if( m_mapProducts == null )
                    {
                m_mapProducts = new SortedList< KeyType, CreateFunctor >(); 
            }
            CreateFunctor createFunctor = Creator<SpecificProduct>; 
            m_mapProducts.Add(key, createFunctor);
        }
    
        //Create a registered object 
        public GeneralProduct Create( KeyType key )
        {
            CreateFunctor createFunctor = m_mapProducts[ key ];
            return createFunctor(); 
        }
    
        private GeneralProduct Creator < SpecificProduct >() 
            where SpecificProduct : GeneralProduct, new()
        {
            return new SpecificProduct();
        }
    
        private delegate GeneralProduct CreateFunctor(); 
    
        private SortedList<KeyType, CreateFunctor>  m_mapProducts;
    }
    

    用法:

    class Fruit
    { 
    } 
    
    class Apple : Fruit 
    { 
    } 
    
    class Orange : Fruit 
    { 
    } 
    
    class TestClass
    { 
        static void Main(string[] args) 
        { 
            General.Factory< string, Fruit > factory; 
    
            //Register
            factory.Register< Apple >( "Apple" ); 
            factory.Register< Orange>( "Orange" ); 
    
            //Create 
            Fruit fruit1 = factory.Create("Apple"); 
            Fruit fruit2 = factory.Create("Orange"); 
        } 
    }
    

    【讨论】:

    • 你能解释一下链接的内容,所以在链接失效的3年内,有人可以从你的帖子中得到一些帮助吗?
    • 你说的太对了...当我点击死链接时我讨厌它,我正要创建一个。哈哈
    【解决方案7】:
    1. C# 支持字符串的switch-case 运算符,即您的代码可以重写为

      switch (fileEx)
      {
          case "txt" : new Text2Pdf().Convert(file); break;
          case "doc": new Word2Pdf().Convert(file); break;
          ...
      }
      
    2. 如果您更改类的名称以对应支持的扩展,那么您将能够使用反射构造它们,如下所示(为简洁起见,省略了错误检查):

          var t = Type.GetType(fileEx + "2Pdf");
          var tConstructor = t.GetConstructor(Type.EmptyTypes);
          var tInstance = tConstructor.Invoke(new object[0]);
          ((ConvertApi)tInstance).Convert(...);
      

    这可能需要一些额外的工作(即为每个扩展创建一个单独的类,从某个基类派生它们 - 例如 Doc2Pdf 和 Docx2Pdf 都从 Word2Pdf 派生)。

    优点是您不必再接触这部分代码。如果您打算为插件编写一些接口,那么它可能会派上用场。

    上面的代码还假设您的 ConvertApi 类都具有默认的无参数构造函数。

    【讨论】:

      【解决方案8】:

      你可以使用一些反射:

      ConvertApi FindMatchingConverter(string _FileExt)
              {
                  //Get all the converters available.
                  List<ConvertApi> converters = this.GetType()
                                                 .Assembly
                                                 .GetExportedTypes()
                                                 .Where(type => type.IsAssignableFrom(typeof(ConvertApi)))
                                                 .ToList();
      
                  //Loop and find which converter to use
                  foreach (var converter in converters)
                  {
                      if (converter.SupportedFiles.Contains(_FileExt))
                          return Activator.CreateInstance(converter);
                  }
                  throw new Exception("No converter found");
              }
      

      然后您只需在返回的 ConvertApi 上调用 Convert()
      当然,这需要您在基类中添加一个名为 SupportedFiles 的virtual List&lt;String&gt;

      这使它看起来像

      public abstract class ConvertApi
      {
          public abstract void Convert();
      
          public virtual List<String> SupportedFiles {get;set;}
      }
      

      【讨论】:

      • Assembly.GetExportedTypes() 返回一个由Type 组成的数组,而Type 不是ConvertApi。这个想法很好,但正确的实现会有点复杂。
      • 仍然,您将List&lt;Type&gt; 分配给List&lt;ConvertApi&gt; converters。这将引发编译器错误“无法分配...不兼容的类型”。要正确执行此操作,您可以返回 Type 或首先实例化返回对象,类似于我的答案代码。
      • @SergeyKudriavtsev 再次正确,我将答案更新为使用 Activator.CreateInstance() 并使用 virtual List&lt;String&gt; 而不是枚举,因为枚举不能虚拟化。
      猜你喜欢
      • 2015-04-21
      • 2012-11-13
      • 1970-01-01
      • 2013-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-26
      • 2013-03-11
      相关资源
      最近更新 更多