好久沒寫代碼了,終於好不容易接到了開發任務,一看時間還挺充足的,我就慢慢整吧,若是遇上趕進度,基本上直接是功能優先,完全不考慮設計。你可以認為我完全沒有追求,當身後有鞭子使勁趕的時候,神馬設計都是浮雲,按時上線才是王道,畢竟領導是不會關註過程和代碼質量的,領導只看結果,這也許就是我等天朝碼農的悲哀。 ...
好久沒寫代碼了,終於好不容易接到了開發任務,一看時間還挺充足的,我就慢慢整吧,若是遇上趕進度,基本上直接是功能優先,完全不考慮設計。你可以認為我完全沒有追求,當身後有鞭子使勁趕的時候,神馬設計都是浮雲,按時上線才是王道,畢竟領導是不會關註過程和代碼質量的,領導只看結果,這也許就是我等天朝碼農的悲哀。
需求:是這樣的,要開發一個簡訊發送的模板,不同客戶可能會使用不同的模板,而不同的客戶使用的變數參數也是不同的。之前為了應急,線上已經完成了一個簡訊模板發送簡訊的功能,簡訊模板表也創建了,而且在表中已經新增了一條記錄。我只需要做一個簡訊模板的增刪改查界面就可以了,看上去我的任務挺簡單的,老司機應該知道,接了個爛攤子。
下圖所示是原來已經創建好了的表
SQL創建腳本如下:
SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[MessageModule]( [Id] [uniqueidentifier] NOT NULL, [Name] [nvarchar](50) NULL, [Type] [nvarchar](50) NULL, [TypeNo] [nvarchar](50) NULL, [Channel] [nvarchar](50) NULL, [Param] [nvarchar](50) NULL, [Content] [nvarchar](max) NULL, [CreatedBy] [uniqueidentifier] NULL, [CreatedOn] [datetime] NULL, [ModifiedBy] [uniqueidentifier] NULL, [ModifiedOn] [datetime] NULL, [IsDeleted] [bit] NULL, [TypeId] [uniqueidentifier] NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO
在這之前是已經開發了一個發送簡訊的API介面供客戶調用了的,也就是說調用方(客戶),不會修改代碼,只能我這邊來修改。雖然極不情願接做了一半的任務,但是沒辦法,不可能給你的開發任務都是從頭開始的。
實體類代碼如下:
[Table("dbo.MessageModule")] public class MessageModule : DTO { public string Type { get; set; } //業務類型 public string TypeNo { get; set; } //業務編號 public string Channel { get; set; } //使用渠道 public string Name { get; set; } //名稱模版 public string Content { get; set; } //簡訊內容 }
DOT類:
public class DTO { public virtual Guid Id { get; set; } public virtual DateTime? CreatedOn { get; set; } public virtual Guid? CreatedBy { get; set; } public virtual DateTime? ModifiedOn { get; set; } public virtual Guid? ModifiedBy { get; set; } public virtual bool IsDeleted { get; set; } }
這是之前的代碼,業務實體類MessageModuleBusiness.cs代碼如下:
public class MessageModuleBusiness : GenericRepository<Model.MessageModule> { private UnitOfWork.UnitOfWork unitOfWork = new UnitOfWork.UnitOfWork(); #region old code /// <summary> /// 獲取模版內容 /// </summary> /// <param name="crowd"></param> /// <returns></returns> public string GetContent(MessageContext messageContext) { string messageContent = ""; string TypeCode = string.IsNullOrEmpty(messageContext.serviceCode) ? "001" : messageContext.serviceCode; try { var Module = unitOfWork.MessageModule.Get(c => c.Type == messageContext.channel && c.TypeNo == TypeCode).FirstOrDefault();
//Content的內容:【一應生活】您有一件單號為expressNumbers company,已到communityName收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life if (!string.IsNullOrEmpty(Module.Content)) { var content = Module.Content; content = content.Replace("company", messageContext.company); content = content.Replace("expressNumbers", messageContext.expressNumbers); content = content.Replace("communityName", messageContext.communityName); content = content.Replace("Id", messageContext.Id); content = content.Replace("receiveTime", messageContext.receiveTime); content = content.Replace("fetchCode", messageContext.fetchCode); messageContent = content; } return messageContent; } catch (Exception ex) { } return ""; } #endregion }
MessageContext類,這個是客戶端傳輸過來調用的一個實體對象。對象裡面存在許多類似於簡訊的動態標簽變數。
public class MessageContext { /// <summary> /// 手機號碼 /// </summary> public string phone { get; set; } /// <summary> /// 發送信息 /// </summary> public string message { get; set; } /// <summary> /// 簽名 /// </summary> public string sign { get; set; } /// <summary> /// 渠道 /// </summary> public string channel { get; set; } /// <summary> /// 內容 /// </summary> public string content { get; set; } /// <summary> /// 取件碼 /// </summary> public string fetchCode { get; set; } /// <summary> /// 快遞公司 /// </summary> public string company { get; set; } /// <summary> /// 快遞單號 /// </summary> public string expressNumbers { get; set; } /// <summary> /// 社區名稱 /// </summary> public string communityName { get; set; } /// <summary> /// 到件時間 /// </summary> public string receiveTime { get; set; } /// <summary> /// 序號 /// </summary> public string Id { get; set; } /// <summary> /// 業務代碼 /// </summary> public string serviceCode { get; set; } }
控制器方法externalMerchantSendMessage,這是供外部調用的
/// <summary> /// 外部商戶發送信息 /// </summary> /// <returns></returns> public ActionResult externalMerchantSendMessage(MessageContext param) { logger.Info("[externalMerchantSendMessage]param:" + param); bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign); if (!isAuth) { return Json(new Result<string>() { resultCode = ((int)ResultCode.NoPermission).ToString(), resultMsg = "簽名或無許可權訪問" }, JsonRequestBehavior.AllowGet); } var meaage = messageModuleBusiness.GetContent(param); if (string.IsNullOrEmpty(meaage)) { return Json(new Result<string>() { resultCode = ((int)ResultCode.failure).ToString(), resultMsg = "發送失敗" }, JsonRequestBehavior.AllowGet); } SMSHelper helper = new SMSHelper(); helper.SendSMS(meaage, param.phone); return Json(new Result<string>() { resultCode = ((int)ResultCode.success).ToString(), resultMsg = "發送成功" }, JsonRequestBehavior.AllowGet); }
以上是我接收開發任務之前已經實現了的功能。看上去我的任務挺簡單的,可是多年的開發經驗告訴我,這裡需要重構,如果我現在啥都不管,就只管做一個簡訊模板的增刪改查界面的話,後面維護的人一定會抓狂。
看出什麼問題沒有?
這個介面方法externalMerchantSendMessage是給所有客戶調用,而不同客戶使用不同的簡訊模板,不同的模板,又存在不同的變數參數。而現在所有的變數參數都封裝在了類MessageContext中,問題是我們無法一下子把所有的變數參數全部確定下來,並保持不變。那麼,也就是說一旦需要添加變數參數,類MessageContext中的代碼就必須修改,而且GetContent方法中的代碼是硬編的,一樣需要跟著修改。這樣就形成了一個迴圈,不斷加變數參數,不斷改代碼,不斷發佈介面版本.......
時間充裕的情況下,我自然是一個有節操的程式猿,那麼就開始重構吧。
在重構之前,在腦海浮現的並不是各種設計模式,而是面向對象設計的基本原則。各種設計模式就好比各種武學套路或者招式,習武之人應該像張無忌練習太極劍一樣,先學會各種套路,然後忘記所有套路,從而融會貫通。因為招式是死的,人是活得,有招就有破綻,根本沒有必勝招式存在,就好像沒有萬能的設計模式一樣,任何設計模式都存在缺點。
面向對象設計的核心思想就是封裝變化,那麼先找出變化點。從上面的分析中,我們已經發現了變化點,那就是簡訊模板中的變數參數,而這些變數參數都是客戶調用方傳過來的,不同客戶傳遞的參數變數又可能是不一樣的。我們先來看一下,客戶傳遞過來的是什麼?我們看下客戶調用代碼,這裡有Get和Post兩種調用方式。
function sendMsg() { //var appParam ="phone=15914070649&sign=78a7ce797cf757916c2c7675b6865b54&channel=weijiakeji&content=&fetchCode=1
&company=%E9%A1%BA%E4%B8%B0%E5%BF%AB%E9%80%92&expressNumbers=123456&communityName=%E9%95%BF%E5%9F%8E%E4%B8%80%E8%8A%B1%E5%9B%AD&receiveTime=5&Id=1231"; //Get("/Message/externalMerchantSendMessage?" + appParam, {}); var data = { "phone": "15914070649", "sign": "78a7ce797cf757916c2c7675b6865b54", "channel": "weijiakeji", "fetchCode": 1, "company": "%E9%A1%BA%E4%B8%B0%E5%BF%AB%E9%80%92", "Id": "1231" }; Post('/Message/externalMerchantSendMessage', data); }
//WebAPI Post方法 function Post(url, data) { $.ajax({ url: url, contentType: "application/json", type: "POST", dataType: "json", async: true, cache: false, data: JSON.stringify(data), success: function (response) { $('#response').text(JSON.stringify(response)); }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert(textStatus); } }); }; //// WebApi Get方法 function Get(url, data) { $.ajax({ url: url, contentType: "application/json", type: "GET", dataType: "json", async: true, cache: false, //data: JSON.stringify(data), success: function (response) { $('#response').text(JSON.stringify(response)); }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert(textStatus); } }); };
可見客戶傳遞的是一個鍵值對集合,就是一個JSON格式的對象。根據前面的代碼 bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);,可以分析出有三個參數是所有調用客戶都必須傳遞過來的,那就是:channel,phone,sign,而其它的參數就是簡訊模板的變數參數和參數值。那麼方法externalMerchantSendMessage(MessageContext param)中的參數就是一個可變對象。在C#4.0種存在一個dynamic不正是用來描述可變對象嗎?
那麼第一步修改傳入參數類型,之前是硬編碼的強類型MessageContext,現在不依賴此類,而是動態解析,修改externalMerchantSendMessage方法代碼如下:
dynamic param = null; string json = Request.QueryString.ToString(); if (Request.QueryString.Count != 0) //ajax get請求 { //相容舊的客戶調用寫法,暫時硬編了 if (json.Contains("param.")) { json = json.Replace("param.", ""); } json = "{" + json.Replace("=", ":'").Replace("&", "',") + "'}"; } else //ajax Post請求 { Request.InputStream.Position = 0; //切記這裡必須設置流的起始位置為0,否則無法讀取到數據 json = new StreamReader(Request.InputStream).ReadToEnd(); } var serializer = new JavaScriptSerializer(); serializer.RegisterConverters(new[] { new DynamicJsonConverter() }); param = serializer.Deserialize(json, typeof(object));
DynamicJsonConverter的作用是將JSON字元串轉為Object對象,代碼如下:
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Dynamic; using System.Linq; using System.Text; using System.Web.Script.Serialization; public sealed class DynamicJsonConverter : JavaScriptConverter { public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { if (dictionary == null) throw new ArgumentNullException("dictionary"); return type == typeof(object) ? new DynamicJsonObject(dictionary) : null; } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IEnumerable<Type> SupportedTypes { get { return new ReadOnlyCollection<Type>(new List<Type>(new[] { typeof(object) })); } } #region Nested type: DynamicJsonObject private sealed class DynamicJsonObject : DynamicObject { private readonly IDictionary<string, object> _dictionary; public DynamicJsonObject(IDictionary<string, object> dictionary) { if (dictionary == null) throw new ArgumentNullException("dictionary"); _dictionary = dictionary; } public override string ToString() { var sb = new StringBuilder("{"); ToString(sb); return sb.ToString(); } private void ToString(StringBuilder sb) { var firstInDictionary = true; foreach (var pair in _dictionary) { if (!firstInDictionary) sb.Append(","); firstInDictionary = false; var value = pair.Value; var name = pair.Key; if (value is string) { sb.AppendFormat("{0}:\"{1}\"", name, value); } else if (value is IDictionary<string, object>) { new DynamicJsonObject((IDictionary<string, object>)value).ToString(sb); } else if (value is ArrayList) { sb.Append(name + ":["); var firstInArray = true; foreach (var arrayValue in (ArrayList)value) { if (!firstInArray) sb.Append(","); firstInArray = false; if (arrayValue is IDictionary<string, object>) new DynamicJsonObject((IDictionary<string, object>)arrayValue).ToString(sb); else if (arrayValue is string) sb.AppendFormat("\"{0}\"", arrayValue); else sb.AppendFormat("{0}", arrayValue); } sb.Append("]"); } else { sb.AppendFormat("{0}:{1}", name, value); } } sb.Append("}"); } public override bool TryGetMember(GetMemberBinder binder, out object result) { if (!_dictionary.TryGetValue(binder.Name, out result)) { // return null to avoid exception. caller can check for null this way... result = null; return true; } var dictionary = result as IDictionary<string, object>; if (dictionary != null) { result = new DynamicJsonObject(dictionary); return true; } var arrayList = result as ArrayList; if (arrayList != null && arrayList.Count > 0) { if (arrayList[0] is IDictionary<string, object>) result = new List<object>(arrayList.Cast<IDictionary<string, object>>().Select(x => new DynamicJsonObject(x))); else result = new List<object>(arrayList.Cast<object>()); } return true; } } #endregion }View Code
接下來是GetContent方法,此方法的目的很簡單,就是要根據客戶傳遞的模板變數參數鍵值對和簡訊模板內容,拼裝成最後的簡訊發送內容,之前此方法裡面是硬編碼的,現在我們需要變成動態獲取。
簡訊模板的內容示例:
【一應生活】您有一件單號為expressNumbers company,已到communityName收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life
我發現這樣的模板內容有問題,模板中的變數參數是直接用的英文單詞表示的,而我們的簡訊內容中可能有時候也會存在英文單詞,那麼我就給所有的變數參數加上{}。修改後如下:
【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life
我們需要根據客戶傳遞過來的對象,將簡訊模板中的變數參數,替換成變數參數對應的值。那麼我們首先就要解析這個對象中的鍵值對信息。
/// <summary> /// 把object對象的屬性反射獲取到字典列表中 /// </summary> /// <param name="data">object對象</param> /// <returns>返回Dictionary(屬性名,屬性值)列表</returns> static Dictionary<string, string> GetProperties(object data) { Dictionary<string, string> dict = new Dictionary<string, string>(); Type type = data.GetType(); string[] propertyNames = type.GetProperties().Select(p => p.Name).ToArray(); foreach (var prop in propertyNames) { object propValue = type.GetProperty(prop).GetValue(data, null); string value = (propValue != null) ? propValue.ToString() : ""; if (!dict.ContainsKey(prop)) { dict.Add(prop, value); } } return dict; }
接下來是通過正則表達式來匹配簡訊模板內容。
/// <summary> /// 多個匹配內容 /// </summary> /// <param name="sInput">輸入內容</param> /// <param name="sRegex">表達式字元串</param> /// <param name="sGroupName">分組名, ""代表不分組</param> static List<string> GetList(string sInput, string sRegex, string sGroupName) { List<string> list = new List<string>(); Regex re = new Regex(sRegex, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline); MatchCollection mcs = re.Matches(sInput); foreach (Match mc in mcs) { if (sGroupName != "") { list.Add(mc.Groups[sGroupName].Value); } else { list.Add(mc.Value); } } return list; } public static string ReplaceTemplate(string template, object data) { var regex = @"\{(?<name>.*?)\}"; List<string> itemList = GetList(template, regex, "name"); //獲取模板變數對象 Dictionary<string, string> dict = GetProperties(data); foreach (string item in itemList) { //如果屬性存在,則替換模板,並修改模板值 if (dict.ContainsKey(item)) { template = template.Replace("{"+item+"}", dict.First(x => x.Key == item).Value); } } return template; }
這樣就講客戶傳遞的對象和我們的解析代碼進行瞭解耦,客戶傳遞的對象不再依賴於我們的代碼實現,而是依賴於我們數據表中模板內容的配置。
這幾個方法我是寫好了,順便弄個單元測試來驗證一下是不是我要的效果,可憐的是,這個項目中根本就沒用到單元測試,沒辦法,我自己創建一個單元測試
[TestClass] public class MatchHelperTest { [TestMethod] public void ReplaceTemplate() { //模板文本 var template = "【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life"; //數據對象 var data = new { expressNumbers = "2016", company = "長城", communityName = "長怡花園"}; string str = "【一應生活】您有一件單號為2016 長城,已到長怡花園收發室,請打開一應生活APP“收發室”獲取取件碼進行取件。點擊下載http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life"; string str1=MatchHelper.ReplaceTemplate(template, data); Assert.AreEqual(str1,str); //重覆標簽的測試 template = "【一應生活】您有一件單號為{expressNumbers} {company},已到{communityName}收發室,單號:{expressNumbers}"; str = "【一應生活】您有一件單號為2016 長城,已到長怡花園收發室,單號:2016"; str1=MatchHelper.ReplaceTemplate(template, data); Assert.AreEqual(str1, str); } }
說到單元測試,我相信在許多公司都沒有用起來,理由太多。我也覺得如果業務簡單的話,根本沒必要寫單元測試,國內太多創業型公司項目進度都非常趕,如果說寫單元測試不費時間,那絕對是騙人的,至於說寫單元測試能提高開發效率,減少返工率,個人感覺這個還真難說,因為即便不寫單元測試也還是