在前段时间,接触一个很喜欢钉钉并且已在内部场景广泛使用钉钉进行工厂内部管理的客户,如钉钉考勤、日常审批、钉钉投影、钉钉门禁等等方面,才体会到原来钉钉已经已经在企业上可以用的很广泛的,因此回过头来学习研究下钉钉的一些业务范围和其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对象的类名称,整个解决方案如下所示。