本文是我對一個項目中一個小功能點的演進及重構過程的一點反思與心得。
背景:
本項目是一個電子商務類的網站,其中有個功能是在訂單狀態改變到某種狀態后向客戶發送通知短信的功能,短信及網關功能均已封裝為組建的方式,我們直接調用即可。
為更清晰明白地說明與本主題相關的功能,在此我以一個控制台的程序方式說明代碼的演進過程。
重構的演進過程:
最初我們是如大多數項目一樣,為在規定的時間內完成相關功能點而努力奮斗着,這個功能點的主要代碼如下:

static void SendSMS_V1(DataTable dt) { if (null == dt) return; for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; OrderStateEnum state = (OrderStateEnum)((int)row["OrderState"]); string template = string.Empty; switch (state) { case OrderStateEnum.UnConfirmed: template = "尊敬的{0},你好!你的訂單已成功下達,請盡快付款以便配送。"; break; case OrderStateEnum.Confirmed: template = "尊敬的{0},你好!你的訂單(訂單號:{1})已被確認,請耐心等待。"; break; case OrderStateEnum.Cancel: template = "尊敬的{0},你好!你的訂單(訂單號:{1})已被取消,具體原因請上網查看。"; break; case OrderStateEnum.Finish: template = "尊敬的{0},你好!你的訂單(訂單號:{1})已完成,網站感謝您的支持與配合,歡迎再次光臨。"; break; default: break; } string content = string.Format(template, row["CustomerName"], row["OrderID"]); SendSMS.Send(row["phone"].ToString(), content); } }
在項目上線初期,這段代碼工作良好。
在運營一段時間后運營部門同事陸續提出要在某些地方將產品名稱給加上去,在這一改動過程中,我發現代碼沒有和數據相分離,再者如要增加一個訂單狀態后增加相應的短信提示或者取消某一個狀態的短信提示,這個改動過程有點麻煩。於是初步想到將短信的內容放到配置文件中,在調用的時候讀取訂單狀態對應的配置文件然后格式化即可。如讀取的內容為空或者該文件不存在則跳過不發送。主要代碼如下:

#region v2 內容和數據分離 static void SendSMS_V2(DataTable dt) { if (null == dt) return; for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SMSTemplates", "V2", row["OrderState"].ToString() + ".txt"); var template = FileHelper.Read(path); if (!string.IsNullOrEmpty(template)) { string content = string.Format(template, row["CustomerName"], row["OrderID"], row["ProductName"]); SendSMS.Send(row["phone"].ToString(), content); } } } #endregion

尊敬的{0},你好!你的訂單(訂單號:{1})已被確認,請耐心等待。
隨着運營進行,運營部門不斷地提出將某些字段在某些地方顯示, string.Format 后的參數不斷地增加,每次改后都要將整個過程重新測試一通,很是讓人頭疼!我的想法是開發這邊提供一個數據標簽列表,同時在后台提供操作界面將短信管理模板讓運營同事自己去修改,不用每次都找我們?有了以上想法,使用一個自定義的 formatter :

public class IndexerNamedFormatter : IFormatProvider, ICustomFormatter { public IndexerNamedFormatter() { } public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; throw new TypeAccessException("不匹配的類型。"); } public string Format(string format, object arg, IFormatProvider formatProvider) { if (null == arg) throw new ArgumentNullException("參數 arg 不能為 null"); int indexer = 0; bool isIndexed = int.TryParse(format, out indexer); //如是 datarow if (arg is System.Data.DataRow) { return GetStringFromDataRow(format, arg, indexer, isIndexed); } //如是 datareader 之類的 if (arg is System.Data.IDataRecord) { GetStringFromIDataRecord(format, arg, indexer, isIndexed); } return string.Empty; ; } private static void GetStringFromIDataRecord(string format, object arg, int indexer, bool isIndexed) { var dr = (System.Data.IDataRecord)arg; string.Format("{0}", isIndexed ? dr[indexer] : dr[format]); } string GetStringFromDataRow(string format, object arg, int indexer, bool isIndexed) { var row = (System.Data.DataRow)arg; return string.Format("{0}", isIndexed ? row[indexer] : row[format]); }

#region v3 使用自定義標簽 static void SendSMS_V3(DataTable dt) { if (null == dt) return; IndexerNamedFormatter formatter = new IndexerNamedFormatter(); for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SMSTemplates", "V3", row["OrderState"].ToString() + ".txt"); var template = FileHelper.Read(path); if (!string.IsNullOrEmpty(template)) { string content = string.Format(formatter, template, row); SendSMS.Send(row["phone"].ToString(), content); } } } #endregion

尊敬的{0:CustomerName},你好!你的訂單(訂單號:{0:OrderID},產品:產品{0:productname})已被確認,請耐心等待。
將數據字段取出來后寫成一個標簽列表文檔提供給運營人員后,從此關於這一塊的修改要求安靜了。
以上只是一個小小設計技巧,這也讓我明白需求的准確把握與挖掘是何等地重要!往往客戶今天說要這樣,明天要那樣,大概很多人都在抱怨:你們真麻煩!但在需求不斷地出現時我們是不是在修改的時候也反思下是不是我們未准確把握他們所想要的功能而讓功能設計出了點問題?當然我也不崇尚一開始就大談設計,過度設計要付出大量的時間成本和可能導致實現的復雜度的增加。
一點疑問:
通過查閱文檔我一直感覺 arg is System.Data.DataRow 這種實現方式很是別扭,為什么索引器不定義為一個接口 ?有誰有更好的實現方法麻煩告知,謝謝!
感謝您的閱讀!文中所涉及到的代碼可從此下載。