O/X Mapping 的故事

  
记得04年春夏之交,我在华东出差。在从南京开往杭州的火车上,一位同事提出了一个想法。他说我们能不能做一个新的程序配置的界面,就像现在很多软件里面流行的那样,左边是个树型目录,选中树型目录上不同节点的时候,右边就分别显示这个节点对应的配置内容。后来在杭州还看到afga的影像工作站软件,也提供了类似的界面。我们这些小公司在设计软件的时候,国外的一些行业巨头常常是我们的模仿对象。
 
 
当时我在想,如果只是把这样一个界面交互的方式做出来,其实非常容易。但这是不够的,这样一种由树型目录控制配置面板的交互方式,背后隐藏的其实是一种动态的机制。即不管将来会有多少个的产品或者模块,每个模块又有多少个的配置项目,都可以很容易添加到这个配置体系里来。
 
然而,当时我们读写配置信息的方式依旧是十分落后的,难以满足这种动态扩展的要求。( 事实上,不断地扩展配置常常被认为是产品客户化阶段应付千变万化的客户需求的最好办法,因为我们可以不会再被不同客户间存在矛盾的需求的情况困扰,当然不断扩展配置项目会让系统维护变得非常复杂,真正的最佳实践还是在产品初始阶段充分理解客户需求,准确识别变与不变项,以及实施阶段在管理或商务上对客户化范围进行必要的控制。200706注)即在程序里面定义一些静态的结构,比如一个singleton模式的class,这个class里面的成员就是配置信息。下面就是当时的一个保存网络通信参数配置的例子。
 
public   class  MyConnection
{
    
private string _ip = “127.0.0.1”;
    
public string IP getreturn _ip; } set{ _ip = value; } }
    
private int _port = 8080;
    
public int Portgetreturn _port; } set{ _port = value; } }
}

然后就用System.XML里面提供的API编写一个解析器,专门用于把class MyConnection的实例编码成下面的XML,并且能解析这个XML,把IP和Port的值重新读上来。

< MyConnection >
    
< IP > 127.0.0.1 </ IP >
    
< Port > 8080 </ Port >
</ MyConnection >

下面是MyConnection的解析器的代码片断,如果XML中节点比较多,或者有多级的子节点,下面的foreach和switch..case就会变得极其复杂,而且难于扩展。

MyConnection cfgInfo  =   new  MyConnection();
XmlDocument xmlDoc 
=   new  XmlDocument();
xmlDoc.LoadXml (strXml);
XmlNodeList nodeList
= xmlDoc.GetElementsByTagName ( " MyConnection " );

foreach  (XmlNode node  in  nodeList)
{
    XmlNodeReader reader 
= new XmlNodeReader (node);
    
while (reader.Read())
    
{
        
if(reader.NodeType==XmlNodeType.Element)
        
{
            
string strEleName=reader.Name;
            
switch(strEleName)
            
{
                
case "IP":
                    cfgInfo.IP 
= reader.ReadString();
                    
break;
                
case "Port":
                    cfgInfo.Port 
= int.Parse(reader.ReadString());
                    
break;
            }

        }

    }

}

这里注明一下,其实从刚开始的时候,我们就没打算用System.Configuration来读写配置文件。因为它有个难以忍受的缺陷,就是在编码的时候必须把像IP,Port这样的key作为字符串硬敲在代码里面。我们的配置项目非常多,没有人记得这么多的key,所以还是决定把它们作为类的成员,至少这样可以用VS.Net提供的代码智能感知的功能把这些key罗列出来。

尽管这样,要实现一个可以动态扩展的配置工具,还是远远不够的。首先必须有一个插件体系,把不同的配置模块加载到界面上,而不需要关心这些配置模块内部是如何实现的。其实这个并不难,.NET提供了很方便的反射和动态程序集加载机制。但这其实只是目前在配置方面面临问题的很小一个方面,我们更加迫切需要的是打破为专门的配置信息编写专门的解析器的局面。因为需要配置的项目太多了,而且每进行一次客户化开发,都可能要增加好几条甚至数十条配置项目。如果不同的配置信息读写器可以在同一个平台上实现快速开发,扩展配置项目的时候也只需要修改很少的代码,就能从根本上提高配置工具开发的效率和质量。
 
好的,首先我们规划一个美好的愿景吧:将来我只要定义好这么一个MyConnection类,这个类的定义本身就携带了它的成员及其类型的信息,这些信息足以进行XML的编码和解码。实例化以后我们只要调用一个ToXmlString(),它就会自动生成一个像上面那样的字符串。然后把这个字符串传给一个类厂,这个类厂也可以构造出MyConnection类的一个实例出来。这样一来,如果有新的配置项目,比如现在要保存一个Url,就只需要在MyConnection里面多定义一个字符串属性,它就会自动编码/解码到XML文件中。比如:
 
MyConnection cfgInfo  =   new  MyConnection();
cfgInfo.Url 
=  “http: // www.google.com”;
cfgInfo.IP  =  “ 127.0 . 0.1 ”;
cfgInfo.Port 
=   8080 ;
string  strXml  =  cfgInfo.ToXmlString();

MyConnection cfgInfor 
=  MyConfigFactory.Create( strXml,  typeof (MyConnection) );
Console.Write( cfgInfor.IP 
+  “ “  +  cfgInfor.Port );
< MyConnection >
    
< Url >  http://www.google.com  </ Url >
    
< IP > 127.0.0.1 </ IP >
    
< Port > 8080 </ Port >
</ MyConnection >

咦,这不就是XML的序列化和反序列化么?我们才发现,原来读写配置信息的逻辑本质,就是XML和可编程对象之间的转换,只不过以前我们要为MyConnection编写专用的XML解析器,现在要实现的动态解析器,就是要把原来的静态机制设计成动态机制即可。

  • 首先是的类成员访问机制:动态解析器需要在事先不知道类的定义的情况下,在运行时进行查询和访问其成员,这个问题可以用反射来解决。
  • 然后就是类型动态转换机制:从各种CLR类型变成字符串很容易,Object.ToString()一下就可以,但从字符串变回这些类型,只有有限的几种TypeConverter,那我们先不追求完美,先实现几种常见类型的转换。
  • 再者,从XML里读到一个字符串的值,你如何判断应该转换成那种类型呢?难道一定要在XML里面同时记录这种类型的信息么?比如<Port type=”int”>8080</Port>。其实未必,只要“Port”这个节点名称跟某个类里面的成员名称是一一对应的,就算XML节点打乱了顺序,也同样可以通过反射从metadata里面获得Port这个成员的类型。
太好了,有了metadata和反射技术,程序真的可以变得无所不能。于是我开始编码,实现第一个动态解析类。在编码之前,先想想一个简单的逻辑结构,来容纳上面这样一个设计原理。当然最容易想到的就是定义一个基类,比如叫做XObject,有两个方法ToXmlString()和LoadXmlString(),派生于这个类的所有子类都可以定义自己的公共属性,然后用这两个方法把这些公共属性的值保存到XML里面并从XML中重新读取出来。
 
哦,不对。为了方便今后扩展,只在XObject里面公开ToXmlString()的方法,装载XML的工作还是交给类厂XObjectManager来做。这样一来,就算输入了非法的XML,或者解析过程中发生什么异常,XObjectManager也都可以好好照应一下,确保XObject永远都是健健康康的。
 
public   abstract   class  XObject
{
    public 
abstract string ToXmlString();
    
internal abstract void LoadXmlNode( XmlNode node );
}


public   class  XObjectManager
{
    
public static XObject CreateObject( string xmlString, Type objType )
    
{
        XmlDocument xmlDoc 
= new XmlDocument();
        xmlDoc.LoadXml( xmlString );
        XmlNodeList nodelist 
= xmlDoc.GetElementsByTagName( objType.Name );
        
object item = objType.Assembly.CreateInstance( objType.ToString() );
        objType.InvokeMember( 
" LoadXmlNode", BindingFlags.NonPublic | 
                BindingFlags.Instance 
| BindingFlags.InvokeMethod,
                
null, item, new object[]{nodelist[0]} );
        
return item;
    }

}

 
确实,有了基本的想法之后,更多工程上的重要细节都是在编码的时候慢慢才发现的。比如我很快就发现自己不得不做一些规定。比如序列化后得到的字符串中,XML的节点名要与类的名称相同,这样一个对象就对应一个XML节点,对象的公共属性就作为子节点。这样不仅使得XML更加易读,下面实现XML动态编码和解码的代码,编写起来也容易多了。
 
public  string  ToXmlString()
{
    StringBuilder sb 
= new StringBuilder();
    String tName 
= this.GetType().Name;
    sb.Append( “
<” + tName + “>” );

    PropertyInfo[] list 
= this.GetType().GetProperties
        ( BindingFlags.Instance 
| BindingFlags.Public );
    
foreach( PropertyInfo prop in list )
    
{
        
string pName = prop.Name;
        sb.Append( “
<” + pName + “>” );
        
object val = this.GetType().InvokeMember(pName,
            BindingFlags.DeclaredOnly 
|    BindingFlags.Public | 
            BindingFlags.Instance 
| BindingFlags.GetProperty,
            
nullthisnew object[]{} );
        sb.Append( val.ToString() );
        sb.Append( “
</” + pName + “>” );
    }


    sb.Append( “
</” + tName + “>” );
    
return sb.ToString();
}


internal   void  LoadXmlNode( XmlNode node )
{
    String tName 
= this.GetType().Name;
    XmlNodeReader reader 
= new XmlNodeReader (node);
    
while (reader.Read())
    
{
        String nodeName 
= reader.Name;
        
if( tName == nodeName && reader.NodeType == XmlNodeType.EndElement ) break;
        PropertyInfo [] pilist 
= this.GetType().GeProperty(nodeName);
        Type type 
= pilist[0].PropertyType;
        
if( type == typeofint ) )
        
{
            
this.GetType().InvokeMember( name, 
                    BindingFlags.DeclaredOnly 
| BindingFlags.Public | 
                    BindingFlags.Instance 
| BindingFlags.SetProperty,
                    
nullthisnew object[]int.Parse(newvalue) } );
        }

    }

}

程序像是一种有生命的东西,只要有拥有一颗好的种子,它就会不断地生根发芽,越长越大。完成这个最初的原型之后,我立即向下一个里程碑进军。即实现对嵌套对象的解析,以及XObjectCollection集合。其中的代码比较罗嗦,但基本都是沿用前面的思路,所以下面只是给出调用的例子。

public   class  MyApplication : XObject
{
    
private string _url = “http://www.google.com”;
    public string Url getreturn _url; } set{ _url = value; } }
    
private MyConnection _server = new MyConnection();
    
public int Servergetreturn _server; } set{ _server = value; } }
    
private MyConnectionList _clients = new MyConnectionList ();
    
public int Clientsgetreturn _ clients; } set{ _ clients = value; } }
}


public   class  MyConnectionList : XObjectCollection
{
    
public MyConnectionList() : basetypeof(MyConnection ) )
}

< MyApplication >
    
< Url >  http://www.google.com  </ Url >
    
< Server >
        
< IP > 127.0.0.1 </ IP >
        
< Port > 8080 </ Port >
    
</ Server >
    
< Client >
        
< MyConnection >
            
< IP > 127.0.0.1 </ IP >
            
< Port > 8080 </ Port >
        
</ MyConnection >
        
< MyConnection >
            
< IP > 127.0.0.1 </ IP >
            
< Port > 8080 </ Port >
        
</ MyConnection >
    
</ Client >
</ MyApplication >

实现这两个特性以后,我终于可以兴致勃勃地向同事们推介这套XML解析库了。一年以后,公司里几乎所有基于.NET开发的产品都使用了这种方式进行配置文件的读写,我再一次用代码证明了自己的存在。同时,我从开始也已经意识到,这种Object/XML Mapping的机制的应用远远不止于配置文件的读写,在Web Service以及一些医疗行业标准(比如HL7、CDA)的实现中也有很大用处,后来甚至还用来映射/解析XSL和XHTML这些格式更复杂的文本。因为它在定义托管类型的字段、属性、标签、以及类型之间聚合关系时候,就直接控制了XML节点的生成,从而把类的定义本身变成了一个XML解析器。两年以后,尽管这个XML解析库的代码还不到3000行,却已经增加了如命名空间支持、自由文本支持等高级的功能,并悄悄地渗透到公司的软件开发里面各个需要XML的角落。我记得有人评价COM技术的时候,说它最成功的地方其实是每个人都在使用它,但没有人感觉到它的存在。

 
然而,编程就像一场未来世界里的神奇旅行,你永远不知道下一秒会发生什么。早在我刚完成XML Mapping的第一个原型的时候,就了解过.NET Framework提供的序列化器的特性。为了证明自己没有白干,当时还叫同事给了我找了些如何使用这个序列化器的代码。当时给我的印象是使用这个序列化器的时候,还是需要自己编写太多的代码,比如在处理对嵌套对象和集合的时候就很不方便;而且似乎只能生成.NET规定格式的XML,比如XML中每个节点同时包含了某个.NET属性的值和类型信息。所以当时还是坚信自己的方向,只要我把嵌套对象和集合处理好,就一定比.NET的序列化器做得好。然而,当我着手编写这篇文章的时候,才有机会再次查阅一下.NET Framework的资料,在System.Xml.Serialization里面似乎也有熟悉的 XmlMapping之类的字眼,似乎也可以通过属性和标签来生成自定义的XML格式。我不打算详细研究它们了(因为通过自己的代码我也已经谙熟其中的原理,除非是有机会开发一个类似的新东西,毕竟微软的设计还是有很多可借鉴之处的),所以我也不知道是谁好谁坏,至少从表面上看System.Xml.Serialization里面一堆类,看得人头晕;我只提供了不到10类,也完整地实现了最常用XML映射的功能,同时也做过一些性能测试,编码解码的性能都还可以接受。 尽管如此,我还是觉得有点可笑,可笑的是在当时这样的小环境里没有人告诉我已经有现成的技术,可笑的是当时我们的开发团队也没人相信微软而反到相信了我。恐怕未来的很长一段时间,他们也不会用微软这套XML映射的 ,甚至也没有人会提到它,因为他们已经太习惯于我曾经提供给他们的编码方式了。IT业界神奇的正反馈规律,在如此小的尺度下,再次得到了验证。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值