您的位置:首页 > 运维架构

一个WinForm程序配置信息的简单模型和维护工具——设计说明

2010-09-18 13:15 543 查看

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对象构成。

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

代码

/// <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 == 0) return 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++这个方向了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐