为什么公司的文档管理系统很难维护,即使是工作多年的ASP.NET程序员也这样认为?
经过多年的积累发展,公司有一套完善的文档管理系统。一开始是用PHP设计的,.NET出来以后,逐步转用.NET重写。经过多年(N>10)的维护和发展,如今客户数量也不少,功能也完善。最近有一些任务是修改这个系统,隐藏一些控件,修改一些布局,一开始以为很容易,但随着任务的进行中,发现一个小功能的修改,也相当的麻烦。
比如,要修改一个控件,根据读取的参数的不同,把它设为隐藏或显示。
举例说明,当用户EnableSimpleUI=true时,把用户输入备注的控件隐藏起来,不需要显示,备注控件的定义如下所示
<asp:TextBox ID=”txtRemard” TextMode="MultiLine" Length=”200”>
我要做的工作,就是在Page_Init()方法中读取EnableSimpleUI的值,根据它来设置txtRemard.Visible属性。
然而工作量远非这么轻松容易,让我来列举一下为什么不容易维护。
1 界面代码与业务逻辑混淆
我不是故意批判这不对,界面和业务代码的分离也是相对的,做不到完全的100%的分离。如果是WinForms程序,通过Debug可以找到修改控件属性的每个地方,依次做出修改即可。但是,如果是ASP.NET程序,界面代码与业务逻辑混淆,界面布局大量依赖于JavaScript的函数设置,这样的布局,给维护人员带来巨大的麻烦。
先来看它的界面布局,在Visual Studio 2008的Design视图下的效果
如图所示,所有的控件都按照预定的布局设计好,而且只有html控件,不用ASP.NET服务器端控件,它的说明文字,是动态生成的。举例如下, butten的文字,是由ASP.NET后台代码传入来的
<input type="button" value="<%=RStr(1306058)%>" onclick="AddAttIndex(this.form)">
再比如标签控件,是这样写的
<%=RStr(1306011)%> :<input type="text" name="AttachmentDescription" id="AttachmentDescription" />
这还不构成复杂,复杂的在于JavaScript脚本与C#代码混淆的情况非常普遍,举例说明
<script lang="JavaScript">
var enableSimpleUI=<%=EnableSimpleUI%>; 这是JavaScript的全局变量,从后台代码获取值
像下面的这种C#代码与JavaScript代码混淆的情况
<% if (isSupportScope) { %>
form.DocClassShared.value = "0";
<% } %>
form.DocClassForceVer.value = "0";
DocClassForceVer是页面中定义的一个隐藏变量,它的初始值从C#代码中获取,之后用于ASPX页面中
<input type="hidden" name="DocClassShared" value="<%# DocTypeShared %>">
这令我想起刚毕业的时候,使用ASP做小企业网站的的时光。ASP中不存在明显的后台代码。对于只含有VB Script代码,而不包含html控件界面的asp页面可以看作是后台代码,再用#include来嵌套几个页面和后台代码,这就是ASP开发的典型模式。所以,我一直认为这是用ASP的开发模式,.NET语言来做Web项目,维护起来不容易。
2 代码重复现象严重
先来看一下,C#重复的代码是怎样的,有哪些重复的代码
public partial class DocType_AddPage : System.Web.UI.Page
{
private string defaultDate = "yyyy-MM-dd";
override protected void OnInit(EventArgs e)
{
_commonHelper = new Common(Session, Response, Request, Cache);
}
protected void Page_Error(Object sender, EventArgs e)
{
System.Exception ServerException;
ServerException = Server.GetLastError();
if (ServerException != null)
{
ErrMsg = ServerException.Message;
Server.ClearError();
}
}
protected void Page_Load(object sender, System.EventArgs e)
{
Utility.Setting setting = new Utility.Setting();
setting.Initialize(false);
}
重复代码分析
1) defaultDate这个变量可能的用途是设置日期的格式,如果不是特殊的要求,这个变量值应当放到Common Settings中去,以保持每个页面的默认日期格式都是yyyy-MM-dd。
2) Page_Load中,每次页面PostBack,都会创建setting对象,应该放到if(!Page.IsPostBack)的子句中。
3) OnInit创建通用的配置信息读取对象,Page_Error发生错误时,记录错误并清除错误,将页面重定向到错误显示页面,Page_Load加载公共的配置项,这三个功能,对于所有的页面,都需要做这样的处理。我认为,应当设计PageBase页面,把这三个方法的功能代码放到PageBase里面去,以减少重复。
再来看JavaScript的代码重复。JavaScript的一个优点是可以直接以DOM方式操作界面控件,这也是缺点,这会导致很多JavaScript代码是重复的,因为开发人员一时的偷懒,他并不想把可以解决问题的代码,提炼到公共JavaScript类型库方法中,减少写测试和参数说明的时间,换来以后维护的代价。重复的JavaScript实在多太,无法计算。
var obj=thisform.DocAttrClassType
var tmp = new Array();
for(var i=0;i<obj.options.length;i++)
{
var ops = new op();
ops._value = obj.options[i].value;
ops._text = obj.options[i].text;
tmp.push(ops);
}
tmp.sort(sortRule);
for(var j=0;j<tmp.length;j++)
{
obj.options[j].value = tmp[j]._value;
obj.options[j].text = tmp[j]._text;
}
这几句可以提炼成为一个公共的方法,把ListBox/Option中的项清空,排序后重新加入。如果再加上这两句
thisform.DocAttrClassType.selectedIndex = 0;
thisform.DocAttrClassType.disabled = false;
这一下子又变成了特定的方法,只适用于thisform.DocAttrClassType对象。
JavaScript实在是太需要公共类库了,所以JQuery非常的流行,后来被直接集成进ASP.NET中去。为了减少以后维护的代码,我认为挑选一套成熟的JavaScript类库非常有必要。
3 存在一些不必要的代码
以数据库的表Document的读写类来举例
public class Document:IDocumentAddon
{
private SqlConnection localDBConn;
private bool localDBConnInit;
private SqlTransaction localDBTran;
private bool localDBTranInit;
public Document(string ConnectionString) { }
public bool Dispose()
再以数据库的表用户表User为例子
public class User : IUser
{
protected SqlConnection localDBConn;
protected bool localDBConnInit;
protected SqlTransaction localDBTran;
protected bool localDBTranInit;
public bool Dispose()
每一个数据表读写类,都在它的内部使用完整的ADO.NET代码来读写数据库,以下代码为例子
string queryString = "SELECT OrderID, CustomerID FROM dbo.Orders;";
using (SqlConnection connection = new SqlConnection( connectionString))
{
SqlCommand command = new SqlCommand( queryString, connection);
connection.Open();
SqlDataReader reader = command.ExecuteReader();
try { while (reader.Read())
{
Console.WriteLine(String.Format("{0}, {1}", reader[0], reader[1])); }
}
finally { reader.Close(); }
}
每一个CRUD操作,都是这样的语句块封装,这样会导致大量的不必要的代码。以企业库为例子
EnterpriseLibraryShared.ConnectonString = "server=(local);database=Northwind;uid=sa;pwd=123456";
Database m_commonDb;
string SQLGet_ShiftCodeList = @"select * from dbo.Customers ";
m_commonDb = DatabaseFactory.CreateDatabase();
DbCommand cmd = m_commonDb.GetSqlStringCommand(SQLGet_ShiftCodeList);
DataSet dsShiftCode = m_commonDb.ExecuteDataSet(cmd);
把上面的标准的ADO.NET的代码换成企业库的调用方式,代码会简洁很多,而且出现了数据访问Bug也容易找问题,只需要调试企业库的ADO.NET代码即可。
4 手动拼凑SQL
先以SQL为例子,SELECT OrderID, CustomerID FROM dbo.Orders, 读取订单号和客户编号。
现在要对系统进行维护,增加一个控制点EnableCustomerVisible,用于控制是否显示客户编码。经过上面的第一条重复的讲解,先把绑定数据库的控件隐藏起来,最后,还要修改SQL语句为,也就是不读取客户编号。
SELECT OrderID FROM dbo.Orders,
如果程序的SQL语句是通过拼凑写出来的,那要修改每一条读取订单的SQL语句,以避免读取客户编号。
如果你有代码生成器,像下面这样,客户编码前的勾去掉,再重新生成一次CRUD的C#代码就可以了。
如果你使用是ORM来读写数据库,以LLBL Gen为例子,像这样写就好了
ExcludeIncludeFieldsList excludedFields = new ExcludeIncludeFieldsList();
excludedFields.Add(CustomerFields.OrderID);
if(EnableCustomerVisible)
excludedFields.Add(CustomerFields.CustomerID );
CustomersEntity c = new CustomersEntity("CHOPS");
DataAccessAdapter adapter = new DataAccessAdapter())
adapter.FetchEntity(c, null, null, excludedFields);
当EnableCustomerVisible条件为false时,不读取CustomerID字段的值。
结论是,为了以后维护方便,尽量少用拼凑SQL的方式。即使要用,也最好配合代码生成器来协助生成代码。
5 多国语言方案导致界面维护复杂度上升
ASP.NET 2.0带来了很多技术解决方案,比如主题,多语言,数据源控件。但是,在ASP.NET 2.0推出之前,大多数的公司已经做好了自己的多语言方案,在维护升级过程中不会轻易改动。以文档系统为例子
<%=RStr(1306011)%> :<input type="text" name="AttachmentDescription" id="AttachmentDescription" />需要label的地方,全部调用方法RStr,并给它传入参数值,在后台调用如下的方法,从数据库中取值
lbl_Desc5 = _commonHelper.GetLocalizedText(1306011);
这种方案已经很普通,不值得一提。随着客户定制化的要求,我多国语言的一个好处。客户通常会对label的名字有特殊的要求,如果是写死在Label中,通常需要改动页面文件才可以。但是,如果有多语言的界面方案,直接修改key值对应的Description就可以达到修改名称的目的。这是我发现的实现多国语言方案的一个额外的好处。
也很显,多国语言方案,导致界面布局在设计时,无法轻松的在设计视图查看到指定控件,并作出修改,这是不利的一面,如果有div是hidden的,需要动态的显示或隐藏,而设计视图界面没有任何的提示,这也会导致维护复杂度上升。
6 对引用的第三方类库缺少Sample和Document
如果要在项目中引用第三方的类型库,我认为很有必要建立一个Document文件夹,把Sample和Document都放在这里,以方便以后的维护。可能项目的周期会长久一点,而互联网上并不见得总是能找到合适的信息帮助你解决问题。旧系统并不是总可以得到预算,可以重写,可以大动筋骨,这种想法通常行不通。当初开发项目的人员,可以很清楚明白他所使用的第三方组件,但以后维护就没有这么轻松了。如果找不到帮助信息,在有第三方组件的源码的情况下,相当于源码级别的调试和追踪,如果没有源代码,还要反编译出来,再来跟踪调试,这样会更加麻烦,如果第三方类型库有加密,阻止反编译,这种情况会更加糟糕。所以,我的想法是,谨慎的引用第三方类型库,尽量使用微软通用的类型,这会给维护带来方便。