由XmlDocument对象引起的思考,是否太过执着于面向对象而忘记了需求?
时间:2011-01-16 来源:五点晨曦
先举个正在纠结的例子,当然不会涉及太多细节。
需求:在ASP.NET页面需要保存一些用户配置,在不同的页面间共享。例如是用户选择的字体,然后把这些变量保存在Session对象中。
实现很简单,直接访问Session对象
string Font=Session["Font"].ToString();//读取
Session["Font"]="宋体";//写入
但是Session对象在连接断开后就没了,于是需求变化了,希望能够持久地保存注册用户的配置信息,就用XML作为数据源吧,同时没有注册的用户继续使用Session
于是我们需要一个User类来进行验证,本例关注配置文件,其他细节不考虑,下面使用一点伪代码来表述
代码ASP页面PageLoad事件
{
string Font;
if(User.注册()==true)//检查用户是否注册
{
Font=ReadXmlConfig(); //从XML读取配置,具体细节忽略
}
else
{
Font=Session["Font"].ToString();//从Session读取配置
}
//……利用读到的Font来处理页面
}
string ReadXmlConfig(){/*不关心的实现细节*/}
暂时还能实现,但已经明显地有崩坏的感觉,如果在配置里增加一个新项,例如文字大小Size,单纯的string不能表示复杂的配置项了,上面的代码也要推倒重来
编写一个新结构体Config如下,用于表示所有配置项,网页文本框手打代码,如果一些访问修饰符有问题请不必在意,本文不讨论这个
public struct Config
{
public string Font;//字体
public int Size;//文字大小
}
Config纯粹起到封装字段的作用,结构体里面没有任何方法
调用如下,继续伪代码
ASP页面PageLoad事件
{
if(User.注册()==true)//检查用户是否注册
{
Config config =ReadXmlConfig(); //从XML读取配置,这次ReadXmlConfig方法返回的是Config类的一个对象
//……利用读到的config来处理页面
}
else
{
Config config =(Config)Session["Config"];//从Session读取配置,作一个强制类型转换
//……利用读到的config来处理页面
}
}
用到了一点封装的思想,但还不完善,ReadXmlConfig()这个方法应该依附在跟配置文件有关的类中,而不是无关的WebForm类里面,从Session读取数据也应该用方法封装一下
Config结构体改成类,修改如下
代码
public class Config
{
public string Font;//字体
public int Size;//文字大小
public void ReadXmlConfig(){/*在此读取xml,给字段Font和Size赋值*/}
public void ReadSessionConfig(){/*在此读取Session,给字段Font和Size赋值*/}
}
//代码调用
ASP页面PageLoad事件
{
Config config;
if(User.验证()==true)
{
config.ReadXmlConfig();
}
else
{
config.ReadSessionConfig();
}
//使用设置好的config对象
}
写到这里,基本考虑到了面向对象的封装特性,虽然仍然有很多不足的地方,(例如使用接口带来的可扩展性),但基本能满足当前程序不算复杂的需求了
且慢,真有这么简单,那就连小学生都会面向对象了
首先ReadXmlConfig和ReadSessionConfig两个方法看似很合理,但是仔细一想,如果我还有其他的数据源呢?例如SQL Server或者MySQL甚至是JSON格式的数据源?那我不是还得加上ReadSQLConfig(),ReadMySQLConfig()和ReadJSONConfig()?在Intelligence Sense给出的方法列表里看着那一大串方法是不是会觉得头大。保持良好的程序扩展性这种想法初看起来是找块便便往自己头上扣,需要在设计阶段付出更多的脑力劳动,但也不能一棍子打死说完全是无用功,或者必须要留出接口。
先看看我在这里的改动
public class Config
{ public string Font;//字体
public int Size;//文字大小
public Config(string xmlFilePath){/*解释指定路径的xml,给字段Font和Size赋值*/}
public Config(object sessionObj){/*转换object为Config对象,给字段Font和Size赋值*/}
}
//代码调用
ASP页面PageLoad事件
{
Config config;
if(User.验证()==true)
{
config=new Config("C:\\config.xml");
}
else
{
config=new Config(Session["Config"]);
}
//使用设置好的config对象
}
这里把原来的ReadXXXConfig()改成了重载的构造方法,其实本菜一直搞不清楚构造方法重载和工厂模式有什么实际区别,有高手给我上一课的话感激不尽啊,总之这里用比较容易理解的。
通过重载构造方法,给予了Config类不同的实例化方式,省掉了一大Read神马Config(),看起来清爽多了,去掉了很多丑陋的代码。好了,现在肯定会有大牛们指出我的代码依然不够OO,如果数据源的种类增加,上面的if……else子句还是会不断膨胀,会不会真的有那么多种类的数据源另说,但是设计上的确还是欠缺考虑。
于是我应该添加一个Config的抽象类,让XmlConfig,SessionConfig继承它,然后再定义一个接口IReadable,里面有一个方法ReadConfig(),用这个接口从不同的Config子类中读取数据?这样会不会有设计过度的嫌疑呢?但如果不这样写是不是会设计不足?这就是本菜一直纠结的地方。不知道谁最开始流传的那么一句话,“如果if……else或者switch的分支超过3个,就该考虑用多态重构了”,但从需求出发,似乎不太可能有那么多数据源,撑死也不过三四种吧,是否需要为这个看起来不太复杂的分支语句进行重构呢?这样做if……else好像是消除了,但程序的逻辑不变,需要分支决策的地方还是得分支,否则独立的类之间零耦合度,拿什么实现逻辑呢?只是可能一连串的if……else被打散在不同的类中,或者将实现了多态的类交给客户端调用,使得代码看起来比较容易读懂。但是过分地抽象程序,继承层次太复杂,滥用接口,反而会适得其反,让程序更难读懂,甚至还不如分支语句清晰。我们抽象的时候又应该抽象到什么粒度?这些问题都深深地困扰着本菜呀。
如果坚持看到这里的朋友会发现,说了半天,LZ你的帖子跟XmlDocument这个对象有半毛钱关系?用过Xml编程的同学肯定不会对下面的代码不陌生吧:
XmlDocument xmlDoc = new XmlDocument();
//从文件路径加载一个XML文档
xmlDoc.Load("C:\\config.xml");
//从字符串加载一个XML文档
xmlDoc.LoadXml("<?xml version=‘1.0’?>……省略一堆");
两个Load方法都是用来载入XML文档,是不是觉得很眼熟?跟我上面写的ReadXmlConfig()和ReadSessionConfig()根本就是一样的嘛!
哈,微软的coder你也写渣代码!你也不懂面向对象!
且慢!微软真那么好忽悠,那连本菜都能当MVP了。
这就是在写配置文件时引发本菜思考的地方,于是我尝试着从合理角度来给微软的程序员开脱解释,从需求出发,XML文档还需要哪几种加载方式?呃,本菜搔破头,好像就是原来那两种了,数据库什么的,自己写个DB类去读啊。
Load和LoadXml两个方法的参数都是string类型,这也表示不能用重载Load方法()来构造两个不同的实例。如果加入一种XmlString类型来保存用字符串表示的XML文档,倒是可以重载,像这样
xmlDoc.Load("C:\\config.xml");
xmlDoc.Load(new XmlString("<?xml version=‘1.0’?>……省略一堆"));
但也略显麻烦了,这里微软的coder似乎选择了一种偷懒简洁的写法,虽然不是最优雅的写法,但很好理解,只有两种加载方法也不难区分。我这里该说微软的coder很好地把握了需求了么?
于是本菜马上又想到了,我是不是应该写一个Database类来操作数据库,再写一个Xml类来读写xml,这两个类再通过一个IDataSource接口来和Config类沟通,让数据层的差异对Config类透明呢?本菜已经心力交瘁,几近崩溃了。
码了这么多字,主要是写一写最近的学习体会,以及提出一些抑压已久的疑问,相信观点会有不少错误的地方,言辞有所偏颇,前言不搭后语。请不吝指教,多加斧正,本菜感激不尽。
最后,本人绝非用记事本写项目的大牛,网页文本框打的代码估计错漏甚多,引号分号漏掉无数,请多多包涵。