在前段時間,接觸一個很喜歡釘釘並且已在內部場景廣泛使用釘釘進行工廠內部管理的客戶,如釘釘考勤、日常審批、釘釘投影、釘釘門禁等等方面,才體會到原來釘釘已經已經在企業上可以用的很廣泛的,因此回過頭來學習研究下釘釘的一些業務範圍和其SDK的開發工作。釘釘官方的SDK提供了很多方面的封裝,不過相對於Java... ...
在前段時間,接觸一個很喜歡釘釘並且已在內部場景廣泛使用釘釘進行工廠內部管理的客戶,如釘釘考勤、日常審批、釘釘投影、釘釘門禁等等方面,才體會到原來釘釘已經已經在企業上可以用的很廣泛的,因此回過頭來學習研究下釘釘的一些業務範圍和其SDK的開發工作。釘釘官方的SDK提供了很多方面的封裝,不過相對於Java,.NET版本的一直在變化當中,之前研究釘釘C#版本SDK的時候發現一些問題反映給釘釘開發人員,基本上得不到好的解決和回應,而在使用官方的SDK的時候,有些數據竟然無法正常獲取(如角色的信息等),而且官方的SDK使用的時候覺得代碼較為臃腫,因此萌生了對釘釘官方SDK進行全面重構的想法。本系列隨筆將對整個釘釘SDK涉及的範圍進行分析重構,並分享使用過程中的效果和樂趣。
1、釘釘的介紹
釘釘(DingTalk)是阿裡巴巴集團專為中國企業打造的免費溝通和協同的多端平臺,提供PC版,Web版和手機版,支持手機和電腦間文件互傳。 釘釘是阿裡集團專為中國企業打造的通訊、協同的免費移動辦公平臺,幫助企業內部溝通和商務溝通更加高效安全。
2、使用釘釘官方SDK存在的一些問題或不足
一般我們在開發的時候,傾向於使用現有的輪子,而不是重覆發明輪子。不過如果輪子確實不適合或者有更好的想法,那就花點功夫也無妨。
在使用原有的釘釘SDK的時候,發現存在以下一些問題。
1)部分SDK由於參數或者其他問題,導致獲取到的JSON數據無法序列化為正常的屬性,如前段時間的角色列表信息部分(後來修複了這個問題)。
2)使用SDK對象的代碼過於臃腫,一些固定化的參數在使用過程中還需要傳入,不太必要而且增加了很多調用代碼。
3)對JSON序列化的部分,沒有採用JSON.NET(Newtonsoft.Json.dll)的標準化方案,而是利用了自定義的JSON解析類,導致整個釘釘SDK的解析過程繁雜很多。
4)對整個釘釘SDK的設計顯得過於複雜而不容易修改。
5)其他一些看不慣的原因
為了避免大範圍的變化導致整個使用介面也變化,我在重構過程中,儘量還是保留釘釘的使用介面,希望使用者能夠無縫對接我重構過的釘釘SDK介面,因此我在極力簡化釘釘SDK的設計過程的時候,儘量相容使用的介面。
而且由於我引入了Json.NET的對象標準序列化和反序列化的處理後,發現代碼確實簡化了不少,對於重構工作提供了非常的方便。
我們來對比一下原有釘釘SDK介面的使用代碼和重構釘釘SDK的使用代碼。
IDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken"); OapiGettokenRequest request = new OapiGettokenRequest(); request.Corpid = corpid; request.Corpsecret = corpSecret; request.SetHttpMethod("GET"); OapiGettokenResponse response = client.Execute(request); return response;
上面的代碼就是釘釘標準官方SDK的使用代碼,用來獲取token信息的一個介面。
其實這個初始化DefaultDingTalkClient,並準備使用 OapiGettokenRequest來獲取應答對象的時候,我們可以把這個URL(https://oapi.dingtalk.com/gettoken)封裝在請求裡面的,不需要使用的時候再去找這個URL,而且對應OapiGettokenRequest 請求的時候,數據提交方式POST或者GET方式也應該確定下來了,不需要用戶再去設置較好。
用戶參數比較少的情況下,可以使用構造函數傳遞,減少代碼的行數。
然後利用擴展函數的方式,我們還可以進一步減少調用的代碼行數的。
我們來看看,我重構代碼後的調用過程,簡化為兩行代碼即可:
var request = new OapiGettokenRequest(corpid, corpSecret); var response = new DefaultDingTalkClient().Execute(request);
使用擴展函數的輔助,我們還可以簡化為一行代碼,如下所示
var token = new OapiGettokenRequest(corpid, corpSecret).Execute();
對於前面N行代碼,變為目前的一行代碼,效果是一樣的,這個就是我希望的效果:簡單是美。
如果對於多個Request的調用,我們也可以重用DingTalkClient對象的,如下代碼所示。
var client = new DefaultDingTalkClient(); var tokenRequest = new OapiGettokenRequest(corpid, corpSecret); var token = client.Execute(tokenRequest); if (token != null && !token.IsError) { string id = "1"; var request = new OapiDepartmentListRequest(id); var dept = client.Execute(request, token.AccessToken); ...................
當然,由於請求對象和應答對象,我依舊保留了原來對象的名稱,只是採用了基於JSON.NET的方式來重新處理了一下對象的定義。
例如對於Token的請求和應答對象,原來的Token應答對象定義如下所示
/// <summary> /// OapiGettokenResponse. /// </summary> public class OapiGettokenResponse : DingTalkResponse { /// <summary> /// access_token /// </summary> [XmlElement("access_token")] public string AccessToken { get; set; } /// <summary> /// errcode /// </summary> [XmlElement("errcode")] public long Errcode { get; set; } /// <summary> /// errmsg /// </summary> [XmlElement("errmsg")] public string Errmsg { get; set; } /// <summary> /// expires_in /// </summary> [XmlElement("expires_in")] public long ExpiresIn { get; set; } }
我則使用了基於JSON.NET的標註來替代XmlElement的標註,並簡化了部分基類屬性。這樣Json的屬性名稱雖然是小寫,但是我們轉換為對應實體類後,它的屬性則可以轉換為.NET標準的Pascal方式的屬性名稱。
/// <summary> /// 企業內部開發獲取access_token的應答. /// </summary> public class OapiGettokenResponse : DingTalkResponse { /// <summary> /// 開放應用的token /// </summary> [JsonProperty(PropertyName ="access_token")] public string AccessToken { get; set; } /// <summary> /// 失效時間 /// </summary> [JsonProperty(PropertyName ="expires_in")] public long ExpiresIn { get; set; } }
這樣我在重構這些應答類的時候,所需要的只需要進行一定的替換工作即可。
而對於數據請求類,我則在基類裡面增加一個IsPost屬性來標識是否為POST方式,否則為GET方式的HTTP數據請求方式。
然後根據參數和IsPost的屬性,來構建提交的PostData數據。
如我修改原有的BaseDingTalkRequest基類對象代碼為下麵的代碼。
/// <summary> /// 基礎TOP請求類,存放一些通用的請求參數。 /// </summary> public abstract class BaseDingTalkRequest<T> : IDingTalkRequest<T> where T : DingTalkResponse { /// <summary> /// 構造函數 /// </summary> public BaseDingTalkRequest() { this.IsPost = true; } /// <summary> /// 參數化構造函數 /// </summary> /// <param name="serverurl">請求URL</param> /// <param name="isPost">是否為POST方式</param> public BaseDingTalkRequest(string serverurl, bool isPost) { this.ServerUrl = serverurl; this.IsPost = isPost; } /// <summary> /// 提交的數據或者增加的字元串 /// </summary> public string PostData { get { string result = ""; var dict = GetParameters(); if(dict != null) { if (IsPost) { result = dict.ToJson(); } else { //return string.Format("corpid={0}&corpsecret={1}", corpid, corpsecret); foreach (KeyValuePair<string, object> pair in dict) { if (pair.Value != null) { result += pair.Key + "=" + pair.Value + "&"; } } result = result.Trim('&'); } } return result; } } /// <summary> /// 是否POST方式(否則為GET方式) /// </summary> public virtual bool IsPost { get; set; } /// <summary> /// 連接URL,替代DefaultDingTalkClient的serverUrl /// </summary> public virtual string ServerUrl { get; set; } /// <summary> /// POST獲取GET的參數列表 /// </summary> /// <returns></returns> public virtual SortedDictionary<string, object> GetParameters() { return null; } }
而對於請求Token的Request等請求對象,我們繼承這個基類即可,如下代碼所示。
/// <summary> /// 企業內部開發獲取Token的請求 /// </summary> public class OapiGettokenRequest : BaseDingTalkRequest<OapiGettokenResponse> { public OapiGettokenRequest() { this.ServerUrl = "https://oapi.dingtalk.com/gettoken"; this.IsPost = false; } public OapiGettokenRequest(string corpid, string corpsecret) : this() { this.Corpid = corpid; this.Corpsecret = corpsecret; } /// <summary> /// 企業Id /// </summary> public string Corpid { get; set; } /// <summary> /// 企業應用的憑證密鑰 /// </summary> public string Corpsecret { get; set; } public override SortedDictionary<string, object> GetParameters() { SortedDictionary<string, object> parameters = new SortedDictionary<string, object>(); parameters.Add("corpid", this.Corpid); parameters.Add("corpsecret", this.Corpsecret); return parameters; } }
這個請求類,也就確定了請求的URL和數據請求方式(GET、POST),這樣在調用的時候,就不用再次指定這些參數了,特別在反覆調用的時候,簡化了很多。
通過這幾個類的定義,我們應該對我重構整個釘釘SDK的思路有所瞭解了,基本上就是以細節儘量封裝、簡化使用代碼的原則進行全面重構的。
而整體的思路還是基於釘釘官方的SDK基礎上進行的。
而對於釘釘SDK的核心類 DefaultDingTalkClient,我們則進行大量的修改重構處理,簡化原來的代碼(從原來的430行代碼簡化到90行),而實現功能一樣的。
主要的邏輯就是我們使用了JSON.NET的標準化序列化的方式,減少了釘釘SDK的繁雜的序列化處理,而前面使用了PostData、IsPost屬性也是簡化了請求的處理方式。
/// <summary> /// 執行TOP隱私API請求。 /// </summary> /// <typeparam name="T">領域對象</typeparam> /// <param name="request">具體的TOP API請求</param> /// <param name="accessToken">用戶會話碼</param> /// <param name="timestamp">請求時間戳</param> /// <returns>領域對象</returns> public T Execute<T>(IDingTalkRequest<T> request, string accessToken, DateTime timestamp) where T : DingTalkResponse { string url = this.serverUrl; //如果已經設置了,則以Request的為主 if(!string.IsNullOrEmpty(request.ServerUrl)) { url = request.ServerUrl; } if (!string.IsNullOrEmpty(accessToken)) { url += string.Format("?access_token={0}", accessToken); } string content = ""; HttpHelper helper = new HttpHelper(); helper.ContentType = "application/json"; content = helper.GetHtml(url, request.PostData, request.IsPost); T json = JsonConvert.DeserializeObject<T>(content); return json; }
3、使用重構的釘釘SDK
1)重構代碼封裝的調用
為了便於介紹對重構的釘釘SDK的使用情況,我編寫了幾個功能進行測試介面。
獲取Token的操作代碼如下所示。
private void btnGetToken_Click(object sender, EventArgs e) { //獲取訪問Token var request = new OapiGettokenRequest(corpid, corpSecret); var response = new DefaultDingTalkClient().Execute(request); Console.WriteLine(response.ToJson()); }
對部門信息及詳細信息的處理代碼如下所示。
private void btnDept_Click(object sender, EventArgs e) { var client = new DefaultDingTalkClient(); var tokenRequest = new OapiGettokenRequest(corpid, corpSecret); var token = client.Execute(tokenRequest); if (token != null && !token.IsError) { Console.WriteLine("獲取部門信息"); string id = "1"; var request = new OapiDepartmentListRequest(id); var dept = client.Execute(request, token.AccessToken); if (dept != null && dept.Department != null) { Console.WriteLine(dept.Department.ToJson()); Console.WriteLine("獲取部門詳細信息"); foreach (var item in dept.Department) { var getrequest = new OapiDepartmentGetRequest(item.Id.ToString()); var info = client.Execute(getrequest, token.AccessToken); if (info != null) { Console.WriteLine("部門詳細信息:{0}", info.ToJson()); Console.WriteLine("獲取部門用戶信息"); var userrequest = new OapiUserListRequest(info.Id); var list = client.Execute(userrequest, token.AccessToken); if (list != null) { Console.WriteLine(list.ToJson()); Console.WriteLine("獲取詳細用戶信息"); foreach (var userjson in list.Userlist) { var get = new OapiUserGetRequest(userjson.Userid); var userInfo = client.Execute(get, token.AccessToken); if (userInfo != null) { Console.WriteLine(userInfo.ToJson()); } } } } } } } else { Console.WriteLine("處理出現錯誤:{0}", token.ErrMsg); } }
從上面的代碼我們可以看到,對Request請求的處理簡化了很多,不用再輸入煩人的URL信息,以及是否GET還是POST方式。
獲取角色的處理操作如下所示。
private void btnRole_Click(object sender, EventArgs e) { var client = new DefaultDingTalkClient(); var tokenRequest = new OapiGettokenRequest(corpid, corpSecret); var token = client.Execute(tokenRequest); if (token != null && !token.IsError) { Console.WriteLine("獲取角色信息"); var request = new OapiRoleListRequest(); var result = client.Execute(request, token.AccessToken); if (result != null && result.Result != null && result.Result.List != null) { Console.WriteLine("角色信息:{0}", result.Result.List.ToJson()); foreach (var info in result.Result.List) { Console.WriteLine("角色組信息:{0}", info.ToJson()); Console.WriteLine("獲取角色詳細信息"); foreach (var roleInfo in info.Roles) { var roleReq = new OapiRoleGetroleRequest(roleInfo.Id); var detail = client.Execute(roleReq, token.AccessToken); if (detail != null && detail.Role != null) { Console.WriteLine("角色詳細信息:{0}", detail.Role.ToJson()); } } } } } }
獲取的信息輸出在VS的輸出窗體裡面。
2)使用擴展函數簡化代碼
從上面的代碼來看,我們看到 DefaultDingTalkClient 還是有點臃腫,我們還可以通過擴展函數來對請求進行優化處理。如下代碼
var client = new DefaultDingTalkClient(); var tokenRequest = new OapiGettokenRequest(corpid, corpSecret); var token = client.Execute(tokenRequest);
我們通過擴展函數實現的話,那麼代碼還可以進一步簡化,如下所示。
var token = new OapiGettokenRequest(corpid, corpSecret).Execute();
對於擴展函數的封裝,我們就是把對應的介面IDingTalkRequest增加擴展函數即可,如下代碼所示。
以上就是我對釘釘SDK進行整體化重構的過程,由於我需要把所有的Request和Response兩種類型的類轉換為我需要的內容,因此需要全部的類進行統一處理,每個Request類我需要參考官方提供的URL、POST/GET方式,同時需要進行JSON.NET的標誌替換,以及修改相應的內容,工作量還是不小的,不過為了後期釘釘的整體開發方面,這點付出我覺得應該是值得的。
我對不同業務範圍的定Request和Response進行歸類,把不同的業務範圍放在不同的目錄裡面,同時保留原來的Request和Response對象的類名稱,整個解決方案如下所示。