經過多年的積累發展,公司有一套完善的文件管理系統。一開始是用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 User (string ConnectionString) { }
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都放在這裡,以方便以後的維護。可能項目的周期會長久一點,而互連網上並不見得總是能找到合適的資訊協助你解決問題。舊系統並不是總可以得到預算,可以重寫,可以大動筋骨,這種想法通常行不通。當初開發項目的人員,可以很清楚明白他所使用的第三方組件,但以後維護就沒有這麼輕鬆了。如果找不到協助資訊,在有第三方組件的源碼的情況下,相當於源碼層級的調試和追蹤,如果沒有原始碼,還要反編譯出來,再來跟蹤調試,這樣會更加麻煩,如果第三方類型庫有加密,阻止反編譯,這種情況會更加糟糕。所以,我的想法是,謹慎的引用第三方類型庫,盡量使用微軟通用的類型,這會給維護帶來方便。