将“一堆东西”实用程序项目分离为具有“可选”依赖项的各个组件


26

在使用C#/。NET进行一系列内部项目的多年中,我们已经使一个库有机地发展成为一大堆东西。它被称为“ Util”,我敢肯定你们中的许多人在您的职业生涯中见过这些野兽之一。

该库的许多部分都是非常独立的,可以分成单独的项目(我们希望将其开源)。但是,在将它们作为单独的库发布之前,需要解决一个主要问题。基本上,在这些库之间有很多我称之为“可选依赖项”的情况。

为了更好地说明这一点,请考虑一些适合成为独立库的模块。CommandLineParser用于解析命令行。XmlClassify用于将类序列化为XML。PostBuildCheck对已编译的程序集执行检查,如果失败则报告编译错误。ConsoleColoredString是彩色字符串文字的库。Lingo用于翻译用户界面。

这些库中的每一个都可以完全独立使用,但是如果将它们一起使用,则将具有有用的额外功能。例如,将CommandLineParser和都XmlClassify公开需要的构建后检查功能PostBuildCheck。同样,CommandLineParser允许选项文件使用彩色字符串字面量require来提供ConsoleColoredString,并且它通过支持翻译文档Lingo

因此,关键区别在于这些是可选功能。可以将命令行解析器与纯色的无色字符串一起使用,而无需翻译文档或执行任何生成后检查。或者可以使文档可翻译但仍然没有颜色。或既彩色又可翻译。等等。

通过查看该“ Util”库,我发现几乎所有潜在可分离的库都具有将其绑定到其他库的此类可选功能。如果我实际上需要将这些库作为依赖项,那么这些东西根本就不会被弄乱:如果您只想使用一个库,则基本上仍然需要所有库。

是否存在任何建立的方法来管理.NET中的此类可选依赖项?


2
即使这些库相互依赖,将它们分成一致但独立的库(每个库都包含广泛的功能类别)仍可能会有一些好处。
罗伯特·哈维

Answers:


20

缓慢重构。

预计此过程需要一些时间才能完成,并且可能需要多次迭代才能完全删除Utils程序集。

总体方法:

  1. 首先花一些时间,想一想完成后这些实用程序集的外观。不必太担心您现有的代码,请考虑最终目标。例如,您可能希望拥有:

    • MyCompany.Utilities.Core(包含算法,日志记录等)
    • MyCompany.Utilities.UI(绘图代码等)
    • MyCompany.Utilities.UI.WinForms(与System.Windows.Forms相关的代码,自定义控件等)
    • MyCompany.Utilities.UI.WPF(与WPF相关的代码,MVVM基类)。
    • MyCompany.Utilities.Serialization(序列化代码)。
  2. 为这些项目中的每一个创建空项目,并创建适当的项目引用(UI引用Core,UI.WinForms引用UI)等。

  3. 将任何低下的成果(不受依赖问题困扰的类或方法)从您的Utils程序集移动到新的目标程序集。

  4. 获得NDepend和Martin Fowler的Refactoring的副本,开始分析您的Utils程序集,以开始进行更严格的工作。两种有用的技术:

    • 在许多情况下,您将需要通过接口,委托或事件的方式来反转控制
    • 如果是“可选”依赖项,请参见下文。

处理可选接口

程序集引用了另一个程序集,或者没有。在非链接程序集中使用功能的唯一其他方法是通过接口进行加载,该接口是通过从公共类反射来加载的。不利的一面是您的核心程序集将需要包含所有共享功能的接口,但不利的一面是,您可以根据需要部署实用程序而无需“一堆”的DLL文件,具体取决于每种部署方案。下面以彩色字符串为例,说明如何处理这种情况:

  1. 首先,定义核心程序集中的通用接口:

    在此处输入图片说明

    例如,IStringColorer界面如下所示:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. 然后,使用特征在装配体中实现接口。例如,StringColorer该类如下所示:

    using MyCompany.Utilities.Core.OptionalInterfaces;
    namespace MyCompany.Utilities.Console
    {
        class StringColorer : IStringColorer
        {
            #region IStringColorer Members
    
            public string Decorate(string s)
            {
                return "*" + s + "*";   //TODO: implement coloring
            }
    
            #endregion
        }
    }
    
  3. 创建一个PluginFinder(可以在这种情况下使用InterfaceFinder作为更好的名称)类,该类可以从当前文件夹中的DLL文件中找到接口。这是一个简单的例子。根据@EdWoodcock的建议(我同意),当您的项目增长时,我建议使用一种可用的依赖注入框架(想到带有UnitySpring.NET的Common Serivce Locator)来实现更健壮的实现,并提供更高级的“发现我”该功能”功能,也称为“ 服务定位器模式”。您可以对其进行修改以适合您的需求。

    using System;
    using System.Linq;
    using System.IO;
    using System.Reflection;
    
    namespace UtilitiesCore
    {
        public static class PluginFinder
        {
            private static bool _loadedAssemblies;
    
            public static T FindInterface<T>() where T : class
            {
                if (!_loadedAssemblies)
                    LoadAssemblies();
    
                //TODO: improve the performance vastly by caching RuntimeTypeHandles
    
                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsClass && typeof(T).IsAssignableFrom(type))
                            return Activator.CreateInstance(type) as T;
                    }
                }
    
                return null;
            }
    
            private static void LoadAssemblies()
            {
                foreach (FileInfo file in new DirectoryInfo(Directory.GetCurrentDirectory()).GetFiles())
                {
                    if (file.Extension != ".DLL")
                        continue;
    
                    if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.Location == file.FullName))
                    {
                        try
                        {
                            //TODO: perhaps filter by certain known names
                            Assembly.LoadFrom(file.FullName);
                        }
                        catch { }
                    }
                }
            }
        }
    }
    
  4. 最后,通过调用FindInterface方法在其他程序集中使用这些接口。这是一个示例CommandLineParser

    static class CommandLineParser
    {
        public static string ParseCommandLine(string commandLine)
        {
            string parsedCommandLine = ParseInternal(commandLine);
    
            IStringColorer colorer = PluginFinder.FindInterface<IStringColorer>();
    
            if(colorer != null)
                parsedCommandLine = colorer.Decorate(parsedCommandLine);
    
            return parsedCommandLine;
        }
    
        private static string ParseInternal(string commandLine)
        {
            //TODO: implement parsing as desired
            return commandLine;
        }
    

    }

最重要的是: 在每次更改之间进行测试,测试和测试。


我添加了示例!:-)
凯文·麦考密克

1
该PluginFinder类可疑地看起来像一个自己动手的自动DI处理程序(使用ServiceLocator模式),但这是合理的建议。也许最好将OP指向Unity之类的东西,因为那样在库中特定接口的多种实现(StringColourer与StringColourerWithHtmlWrapper或其他)不会有问题。
Ed James

@EdWoodcock很好,Ed,我不敢相信我在编写本文时没有想到服务定位器模式。PluginFinder肯定是一个不成熟的实现,并且DI框架肯定可以在这里工作。
凯文·麦考密克

我已将您的努力奖励给您,但我们不会走这条路。共享接口的核心程序集意味着我们仅成功撤消了实现,但是仍然存在一个包含一堆鲜为人知的,几乎不相关的接口(通过可选依赖项进行关联,如前所述)的库。现在的设置非常复杂,而对于如此小的库却没有什么好处。对于庞大的项目而言,额外的复杂性可能值得,但并非如此。
罗曼·斯塔科夫

@romkyns那你走哪条路?保留原样?:)
2012年

5

您可以使用在附加库中声明的接口。

尝试使用依赖项注入(MEF,Unity等)来解析合同(通过接口进行分类)。如果未找到,请将其设置为返回空实例。
然后检查实例是否为空,在这种情况下,您无需执行其他功能。

使用MEF尤其容易,因为它是教科书使用的。

它将使您能够编译库,但需要将它们拆分成n + 1个dll。

HTH。


这听起来几乎是正确的-如果不是仅用于一个额外的DLL,则基本上就像一堆原始材料的骨架。所有的实现都被拆分了,但是还剩下一堆“骨架”。我想这有一些优点,但是我不相信这些优点超过了这套特定图书馆的所有费用……
Roman Starkov 2012年

另外,包括整个框架完全是一个后退。该库按原样大约是那些框架之一的大小,完全没有好处。如果有的话,我会稍作反思,以查看实现是否可用,因为它只能在零到一之间,并且不需要外部配置。
罗曼·斯塔科夫

2

我以为我会发布迄今为止我们提出的最可行的选择,以了解想法。

基本上,我们将每个组件分成零引用的库;所有需要引用的代码都将放入#if/#endif具有适当名称的块中。例如,CommandLineParser句柄中ConsoleColoredString的代码将放入#if HAS_CONSOLE_COLORED_STRING

希望只包含的任何解决方案CommandLineParser都可以轻松实现,因为没有更多的依赖关系。但是,如果解决方案还包括ConsoleColoredString项目,那么程序员现在可以选择:

  • 加在参考CommandLineParserConsoleColoredString
  • 添加HAS_CONSOLE_COLORED_STRING定义到CommandLineParser项目文件。

这将使相关功能可用。

这有几个问题:

  • 这是仅源解决方案;库的每个使用者都必须将其作为源代码包括在内;它们不能只包含二进制文件(但这不是我们的绝对要求)。
  • 库的库项目文件得到了几个解决方案特定的编辑,并且这种更改如何提交给SCM尚不十分清楚。

不太漂亮,但仍然是我们想到的最接近的。

我们考虑的另一种想法是使用项目配置,而不是要求用户编辑库项目文件。但这在VS2010中绝对不可行,因为它会不必要地将所有项目配置添加到解决方案中



1

完全公开,我是Java专家。因此,我知道您可能不是在寻找我将在这里提及的技术。但是问题是相同的,所以也许它将为您指明正确的方向。

在Java中,有许多构建系统支持集中式工件存储库的思想,该存储库中存储了已构建的“工件”-据我所知,这有点类似于.NET中的GAC(请过分理解我的无知)但这还不止于此,因为它可用于在任何时间点生成独立的可重复构建。

无论如何,受支持的另一个功能(例如,在Maven中)是可选依赖项的概念,然后依赖于特定版本或范围,并可能排除传递性依赖项。在我看来,这听起来像您在寻找什么,但我可能错了。与认识Java的朋友一起从Maven看一下这个有关依赖管理的简介页,看看问题是否听起来很熟悉。这将允许您构建应用程序并在有或没有这些依赖项的情况下构建它。

如果您需要真正的动态,可插拔的体系结构,也可以使用这些结构。OSGI是一种尝试解决这种形式的运行时依赖关系解析的技术。这是Eclipse插件系统背后的引擎。您会看到它可以支持可选的依赖项以及最小/最大版本范围。这种级别的运行时模块化对您以及您的开发方式施加了很多约束。大多数人可以通过Maven提供的模块化程度来解决。

您可能会想到的另一个可能更容易实现的数量级想法是使用“管道和过滤器”样式的体系结构。这很大程度上是使UNIX拥有这样一个长期成功的生态系统的原因,该生态系统已经生存了半个世纪并不断发展。查看有关.NET中管道和过滤器的这篇文章,获取有关如何在框架中实现这种模式的一些想法。


0

约翰·拉科斯(John Lakos)所著的“大型C ++软件设计”也许很有用(当然C#和C ++还是不一样,但是您可以从本书中借鉴有用的技术)。

基本上,将使用两个或多个库的功能重构并移动到依赖于这些库的单独组件中。如果需要,请使用不透明类型等技术。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.