zoukankan      html  css  js  c++  java
  • 一个WinForm程序配置信息的简单模型和维护工具——设计说明

    1. 背景

    首先,我们的产品是一个桌面程序,目前配置文件使用的是ini文件格式。在产品维护过程中,随着配置项不断扩充,配置文件逐渐地变得宠大和混乱,加之ini文件的层次性和可读性不够强,致使配置文件的可维护性越来越差。客户的增多和配置项的臃肿,让发布程序时维护初始配置的工作变得难以忍受。

    另一方面,在程序中,软件配置信息映射为一个静态类,每个配置项作为一个静态成员。这相当于一个一维结构,其层次甚至还不如ini文件的结构,好歹ini文件还分段了。写代码时,就算有智能感知,要从下拉列表中找到自己需要的那个配置项也是一项考验眼力和键盘上下键质量的工作。更糟糕的是,假如某个开发人员需要增加一个配置项,那么他必须做许多工作:

    1. 在静态类中添加一个与配置项相应的Field和Property;

    2. 实现与此配置相关的读取、保存的逻辑;

    3. 签出项目版本库上的配置文件模板,添加相应的配置项;

    4. 通知程序发布人员发布程序时更新配置文件的结构(对程序发布人员而言,要么获取最新的配置文件模板,再为每个客户重新设置一遍初始值;要么修改已存在的各个客户的配置文件,逐个向其中添加要求的配置项)。

    在我看来,这是一个让人恐惧的工作量;而且其中还有一个隐含的难点就是,必须保证在所有引用配置项的地方(代码中和各个配置文件中),配置项的关键字是一致的。除非强制自己Ctrl+C、Ctrl+V,我想没有什么稳妥的办法可以保证做到这一点。可是我正好是不喜欢Ctrl+C、Ctrl+V的人。

    2. 方案目标

    在经历了数次让我不堪忍受的增加配置项的工作后,我开始考虑改变这一切了。我有足够的理由和自信使我相信我能够让工作更得简单和高效。就不算不能在所有方面改善,也至少能够使大多数地方变得更好。

    本方案意图重新设计一个配置文件方案,彻底改善以上状况。可以想象,有这样一些问题是值得动脑筋的:

    1. 使无论在代码还是配置文件中,配置信息都能够拥有良好的层次结构。

    2. 显然,所有的配置项都基本上简单数据类型的,既然不存在复杂的解析与格式化,又存在于有规律的位置,显然可以用某种方式使配置读取和保存的逻辑不必重写(就算一行代码,也不必)。

    3. 修改配置需要修改代码中的配置类及配置文件结构,并同步已存在的各套用户配置文件,显然有必要简化这项工作。

    4.在维护各用户的配置信息时,版本管理人员需要直接修改配置文件中的值,此过程繁琐且容易出错,若有一个能够工具提供一个更友好的编辑配置信息的界面,则世界俨然会更美好。

    3. 设计思路与方案概要

    本方案的主要思路为:

    1、在程序中使用一个树形层次结构来存储配置信息,通过根节点可以枚举到程序中所有的配置项。

    2、使用.NET Framework的Configuration程序集来构建配置信息的内存结构、使用其配置文件格式以及其内置读写功能。使开发者只需维护程序中的配置信息结构,无需维护配置的读写逻辑及文件格式。从而实现配置文件对开发者透明。

    3、开发一个专用的配置信息维护工具,能够方便的读取程序中的配置信息结构并修改其中信息。使软件发布时,开发者可以通过此工具方便地生成针对不同客户的配置信息。

    4. 详细设计

    4.1 Configuration程序集的引入

    有关Configuration程序集可以参考《.NET Framework Configuration总结》,使用Configuration程序集可以:

    1. 在程序中构造出树状的层次配置信息结构;

    2. 在访问配置信息时支持智能感知;

    3. 不仅提供在代码中组织配置信息的标准方式,也为配置信息的存储提供了标准的方式。在此基础上封装了配置信息的存取逻辑,由此实现了文件结构和存取对用户的透明。

    显然,Configuration符合本方案的部分设计目标,而且,任何一种表达方式和存储结构的发明都是一件需要丰富经验并且耗时的工作。因此,本方案将复用Configuration,然后以此为基础实现其余功能。

    4.2配置信息树的构造

    虽然Configuration对象并不总是与一个配置文件相对应,但对于自定义配置文件,一个Configuration对象只会对应一个配置文件。一个Configuration对象中包含的配置信息可以看做一棵以ConfigurationSectionGroup、ConfigurationSection为节点的树。

    我们希望配置文件可以有多个,而为了枚举和管理的方便,在内存中的配置信息最好组织到一棵树下。因此,必须提供一个容器来包含各个Configuration中的配置信息。

    另外,Configuration对象本身不是配置信息树节点,它应被视为文件的映射或配置信息树的容器。我们希望对使用者而言,对配置文件的存取透明的,配置文件的位置也同样透明。因此,配置信息树应不由Configuration对象构成。

    基于以上考虑,配置信息树的结构设计如下:

    clip_image002

    如图所示,我们将Configuration对象直接包含的ConfigurationSectionGroup作为顶层配置组定义到AppConfiguration中,而将Configuration对象排除在配置树之外。

    在此树中,AppConfiguration为根节点,第一层树节点为顶层配置组,每个顶层配置形成一个子树。每个顶层配置组关联到一个Configuration对象。这样,开发人员和工具可以通过AppConfiguration来枚举所有的配置信息,而不用理会具体的配置文件。

    为了把顶层配置组与Configuration关联起来, AppConfiguration必须保有所有顶层配置组的所在文件、名称等相关信息。为此,我们引入IconfigurationUnitInfo接口,在定义AppConfiguration时,要求为每个顶层配置组创建一个用于描述其域、文件名、配置名(配置文件中的标识符)的IconfigurationUnitInfo对象,然后AppConfiguration根据其中信息来获取相应的顶层配置组。

    各个配置文件映射的Configuration对象由ConfigurationObjectManager管理。AppConfiguration的索引器将根据IconfigurationUnitInfo中的信息来从ConfigurationObjectManager中获取指定的Configuration对象,并从中取配置组信息。

    此处的关键在于AppConfiguration的设计,它采用了类似于ConfigurationSectionGroup和ConfigurationSection的设计方式。ConfigurationSectionGroup的使用方式是创建一个它的派生类,在其中为每个子配置组和子配置段定义一个属性,此属性的get访问器调用基类的索引器,通过子组(段)的关键字来获取相应的子配置组(段)对象。AppConfiguration仿照这个设计,提供了一个索引器(其参数是IconfigurationUnitInfo对象),根据参数中提供的配置组所在文件、名声等信息来从ConfigurationObjectManager获取指定文件对应的Configuration对象,再从中获取配置组信息。使用时,可从AppConfiguration创建一个派生类,在其中为每个顶层配置组定义一个属性,其get访问器简单地调用基类的索引器即可。从整体上来说,通过AppConfiguration、ConfigurationSectionGroup和ConfigurationSection这一组类似的类来构造配置信息树,有利于理解和实现。

    代码
    public class TestAppConfiguration : YedaoqConfiguration.AppConfiguration
        {
            
    public TestAppConfiguration(IClientInfo client)
                : 
    base(client)
            {

            }

            
    private IConfigurationUnitInfo _TestGroup = 
                
    new ConfigurationSectionGroupInfo<ConfigurationSectionGroupTest>("TestGroup", EnumConfigDomain.Application, "TestConfig"true);
            
            
    private IConfigurationUnitInfo _TestSection = 
                
    new ConfigurationSectionInfo<ConfigurationSectionA>("TestSection", EnumConfigDomain.User, "TestConfig"true);

            [Description(
    "应用程序的第一个子配置单元")]
            
    public ConfigurationSectionGroupTest TestGroup
            {
                
    get
                {
                    
    object obj = this[_TestGroup];
                    
    return object.ReferenceEquals(null, obj) ? null : obj as ConfigurationSectionGroupTest;
                }
            }

            [Description(
    "应用程序的第二个子配置单元")]
            
    public ConfigurationSectionA SectionA
            {
                
    get
                {
                    
    object obj = this[_TestSection];
                    
    return object.ReferenceEquals(null, obj) ? null : obj as ConfigurationSectionA;
                }
            }
        }

    4.3 配置结构同步

    当配置信息变化时,需要更新内存中的配置树和已存在的所有配置文件。我们必须先指定一个源做为基准,然后才能通过一些自动化的手段来更新其它数据源。

    有很多理由来使我们选择保存在代码中的配置结构来作为同步的基准:

    1. 修改文件不如在IDE中修改代码方便;

    2. 若以文件为基准,由于文件可能存在多套,因此将需要实现文件至文件的同步和文件至代码的同步程序;

    3. 若以文件为基准进行自动同步且要支持智能感知的话,则必须自动生成代码再替换到项目中去。自动生成代码绝对是复杂的事情,何况还存在那么多种编程语言,而编程更新XML则俨然是一件非常简单的事情。

    理由还有找多很多个,不过上面这三条已经足够了。现在,我们确定了同步方向:从程序模块中用代码描述的配置结构向文件进行同步。

    然后,让我们来看看如何实现这个同步功能。

    实际上,Configuration程序集已经提供了有限的配置结构的同步功能。前面提到使用Configuration可使文件结构和存取对用户透明,这也暗示了当配置信息结构变化时,用户无需去更新配置文件结构。

    Configuration程序集的自动同步功能体现在:

    1. 当用户向自定义ConfigurationSection增加一个新的属性时,即使此属性在配置文件中不存在,在加载配置信息后,该属性会置为默认值,当用户设置此属性时,Configuration会自动将此新属性保存入配置文件。

    2. 当我们向ConfigurationSectionGroup.SectionGroups或ConfigurationSectionGroup.Sections添加一个子组(段)时,相关的组(段)类型信息和数据会自动保存到配置文件中。

    这只是最基础的一些同步功能,其不足之处在于:

    1. 虽然对ConfigurationSectionGroup.SectionGroups和ConfigurationSectionGroup.Sections的更改会自动更新到配置文件,但是,我们实际上并不直接使用这两者来存取子配置段(组),而是为每个子配置组(段)定义一个属性,在其get访问器间接使用SectionGroups和Sections。用户会通过更改自定义ConfigurationSectionGroup中定义的属性来更改配置结构,而不是SectionGroups和Sections,因此,我们需要的是,当用户向ConfigurationSectionGroup添加或变更一个描述子组(段)的属性时,该段的信息能够自动更新到配置文件。

    2. 当配置文件保存的中某配置段(组)的类型在代码中不存在或与代码中定义的类型不一致时,在运行时获取该段(组)将导致异常。出现两种情况是很有可能的,我们有可能修改了代码中配置段的类型名称,并希望程序启动后,Configuratin程序集能够自动更正XML中保存的类型信息。

    对第一个问题,一个简单的解决方法是:当ConfigurationSectionGroup. SectionGroups或ConfigurationSectionGroup.Sections索引器返回空时,自动创建一个包含默认数据的对象,添加到SectionGroups或Sections中。

    对第二个问题,一个简单的解决方法是:当ConfigurationSectionGroup. SectionGroups或ConfigurationSectionGroup.Sections索引器异常时,自动创建一个包含默认数据的对象,替换SectionGroups或Sections中的原有对象。

    综合以上两点,我们应该改写原先的调用索引器的代码:

    代码
            /// <summary>
            
    /// 从配置组集合中获取一个配置组
            
    /// </summary>
            
    /// <typeparam name="T">要获取的配置组类型</typeparam>
            
    /// <param name="groups">配置组集合</param>
            
    /// <param name="groupName">要获取的配置组名称</param>
            
    /// <param name="createWhenNonExist">不存在时创建</param>
            
    /// <param name="replaceWhenTypeConflict">获取到的配置组类型与指定类型不一致时是否覆盖</param>
            
    /// <returns>指定的配置组</returns>
            public static T GetConfigurationSection<T>(ConfigurationSectionGroup fatherConfigGroup, string sectionName, bool createWhenNonExist, bool replaceWhenTypeConflict) where T : ConfigurationSection, new()
            {
                
    if (object.ReferenceEquals(null, fatherConfigGroup) || object.ReferenceEquals(null, sectionName) || sectionName.Length == 0return null;

                ConfigurationSection section 
    = null;
                
    bool typeConflict = false;

                
    try
                {
                    section 
    = fatherConfigGroup.Sections[sectionName];

                    
    if (!object.ReferenceEquals(null, section))
                    {
                        
    if (section is T)
                        {
                            
    return section as T;
                        }
                        
    else
                        {
                            typeConflict 
    = true;
                            section 
    = null;
                        }
                    }
                }
                
    catch
                {
                    typeConflict 
    = true;
                }

                
    //根据参数创建新配置项
                if (createWhenNonExist || (typeConflict && replaceWhenTypeConflict))
                {
                    RemoveConfigUnit(fatherConfigGroup, sectionName);
                    section 
    = new T();
                    fatherConfigGroup.Sections.Add(sectionName, section);
                    
    return section as T;
                }

                
    return null;
            }
    4.4 IAppConfigurationProvider和IClientInfo

    我们已经定义好了在代码中描述一棵配置结构树的方式以及将该结构同步到文件中的去逻辑。现在,让我们回到开头提到的一个事实:我们需要维护给各个客户使用的配置文件。

    然后再考虑以下问题:

    1. 在4.2中提到“应向AppConfiguration提供顶层配置组的文件、名称等信息”,那么,这些信息应该谁来提供呢?

    2. 假如我们有一个新功能需要针对不同的客户配置进行测试,那么,有没有什么方便的方法来切换呢?

    3. 假如我们已经有了一个配置维护工具,那么,它启动后,应从哪里去获取一个包含配置信息的AppConfiguration对象呢?

    基于这些考虑,我们引入IAppConfigurationProvider,它是定义以下功能的接口:

    1. 获取所有当前所有存在配置信息的客户目录;

    2. 获取和指定要使用其配置信息的客户。

    3. 获取指定客户的配置信息。

    顾名思义,IAppConfigurationProvider的实现应向任何可能的使用方提供AppConfiguration,并能够方便的在多套配置信息之间切换。为保持一致性,任何可能的使用者都应通过此接口来获取配置信息。

    由于上述功能中涉及的信息可能由各种方式获取,因此,对于IAppConfigurationProvider,并没有默认实现。

    现在,我们再看另外一个问题,在使用不同客户的配置时,显然,AppConfiguration需要打开不同的配置文件。由于配置文件名称信息存储在IconfigurationUnitInfo中,那么我们需要对不同客户生成不同内容的IconfigurationUnitInfo对象吗?

    我们可以这么考虑,一般情况下,我们会把一套配置文件放到一个目录下,不同客户,其配置文件的名称应是相同的,只是存放目录不同。只要把目录信息剥离出来,对不同的客户,其IconfigurationUnitInfo信息就是一致的。正好,目录只与客户相关,因此,我们再引入IClientInfo,将客户的一些相关信息和配置目录信息存放在其中。

    这样,我们在实现AppConfiguration时,就可以将各个顶层配置组的IconfigurationUnitInfo内容固定,另在AppConfiguration中保存一个IClientInfo对象,AppConfiguration的索引器通过IClientInfo中的目录名和IconfigurationUnitInfo中的文件名来联合确定完整的文件路径。

    4.5 配置维护工具

    现在,万事俱备,现在该考虑怎么实现这个工具了。根据之前的设计,这个工具应具有如下特点:

    1. 是一个独立的工具,不依赖于具体的应用程序;

    2. 能够从应用程序的程序集中分析出配置信息结构;

    3. 能够以更友好、安全的方式读写应用程序的配置信息。

    基于第一条,此工具不应假定了解任何一个项目的配置信息。另外,数据的同步方向是从代码向文件。因此有了第二条,该工具只能从程序集中分析配置信息结构,又因为Configuration通过类型信息来描述配置结构,因此,反射是可行且唯一可行的分析方式。

    然后,该工具应如何获取AppConfiguration呢?4.4中说过IAppConfigurationProvider,显然,应通过该接口来获取。那么,从现有程序集中查找继承自IAppConfigurationProvider的类就可以了,但是,这样还不够好。假如用户实现了几个IAppConfigurationProvider呢?这里可以借鉴一下从Windows Live Writer插件开发中学习到的一个方法,定义一个专用Attribute,要求应用程序的开发者向且仅向真实使用的AppConfigurationProvider添加该属性,这样,工具可以通过查找该属性来确定应用程序使用的AppConfigurationProvider。

    接下来就不用多说了,通过反射将所有SectionGroup和Section按原结构放到TreeView中去。

    最后,怎么实现数据的编辑功能呢?由于配置数据可能有各种类型的,自己实现编辑面板麻烦又费事,PropertyGrid能够支持所有的简单数据类型并且易于扩展、能验证数据有效性。因此,用PropertyGrid来做编辑面板是最合适不过的了。

    需要注意的一点是,将一个对象传给PropertyGrid时,PropertyGrid会显示该类的所有属性。那些继承自基类的并不表示配置项的属性会影响使用感受,因此,又经过一番研究,最终实现了一个自定义ICustomTypeDescriptor,重载了GetProperties接口,使其仅返回非继承的属性。

    5. 后记

    这个小工具是在之前的单位写的,给领导后不久就换工作了,领导当时看起来还是挺满意的,可似乎到现在也没用起来。我明白工作太忙的时候,这种改动确实只能推后。可是,我们的工作,有多少是可以简化,又有多少是自找的啊。有点讽刺的是,在社会各行各业都推行信息化,自动化以此提高工作效率的时候,提供这些服务的公司,却又有多少对此有清醒的认识并把工具的运用视为提高产生力的核心方式呢?又有多少公司的领导还言必称人的主观能动性呢?

    我一直致力于使自己的代码具有更好的可复用性,对我而言,长久的价值才是意义所在。大部分的那些堆积在程序里的毫无规范但却能正确运行的代码,除了给公司带来项目收益和自己的工资,并没有多余的价值。

    要尽快把之前写的那些东西的文档整理出来,然后该开始完全投入到C++这个方向了。

  • 相关阅读:
    爬虫之初试
    [已解决]疑问:关于postgres的to_number()
    随笔
    简单的任意列数均匀布局
    去除VS2010中中文注释下的红色波浪线
    DataTable举例
    DevExpress::XtraBars::BarEditItem获取EditValue值事件
    List<String^>^ 引用空间
    TreeList的VisibleNodesCount,Noes.Count,AllNdoesCount以及焦点节点的删除
    获取工程的exe文件的所在目录
  • 原文地址:https://www.cnblogs.com/yedaoq/p/1830140.html
Copyright © 2011-2022 走看看