0.SDK之必備的基本素質
在項目中免不了要用到各種各樣的第三方的sdk,在我現在的工作中就在公司內部積累了各種各樣的的公共庫(基於.net的,基於silverlight的等等),托管到了內部的nuget私服上,大大的方便了項目的開發。
在積累這些庫的過程中走過不少彎路,今天分享給大家(借助微信公眾平台開發的消息處理模塊的SDK(一下簡稱微信消息sdk)做個設計思路剖析)筆者的一些思路的,私以為一個sdk需要具備如下的3條基本素質。
- 站在使用者的角度考慮設計!
- 易維護( 對修改關閉,對擴展開放 -不要波及與擴展無關的任何代碼)!
- 勿做過多的假設!
各位看官如有不同意見和建議歡迎指正,下面就拿微信消息sdk(相關的接口文檔請戳這里)針對這3條基本素質一一解釋。
1.站在使用者的角度考慮設計
一直很喜歡一句話“不要因為走的太遠而忘記為何而出發”。我們寫SDK是為了什么呢?答曰:“為使用者提供服務”,這才是我們的目的嘛,要讓使用者方便,而不是為使用者添堵,見過好多的sdk好像在這條路上市走偏了的,,,
拿微信消息sdk來說,站在使用者的角度來看,微信消息和本質是接受微信服務器轉發來的消息體(xml字符串),然后響應一個消息體(也是xml字符串),那么站在使用者的角度來寫客戶端代碼就是:
//偽代碼 //從httprequest中讀xml消息 String xmContent=ReadXmlContent(request); //處理xml消息並獲得響應的輸出消息 OutputMessage outputMessage=MessageClient.ProgressMessage(xmlContent); //把響應消息寫入httpresponse response.Write(outputMessage);
這只是一個固定的處理流程,那么需求來了:
- 用戶發送一個hello的文本,我們要回復一條你好的文本消息;
- 用戶點擊一個微信菜單按鈕(click類型),回復用戶他(她)你點了哪個按鈕。
我們去翻翻開發者文檔,發現微信為上述兩點需求發送了2中類型的消息,具體的消息內容我就不貼出來了,使用者最直接的用法是什么呢?
文本消息的使用場景(偽代碼):
1 public class HandlerTextMessage 2 { 3 public OutputTextMessage HandlerTextMessage(InputTextMessage inputTextMessage) 4 { 5 if (inputTextMessage.Content == "hello") 6 { 7 return new OutputTextMessage() 8 { 9 Content = "你好!" 10 }; 11 } 12 return new OutputTextMessage() 13 { 14 Content = "說人話,聽不懂..." 15 }; 16 } 17 }
按鈕點擊事件消息的使用場景(偽代碼):
1 public class HandlerEventClickMessage 2 { 3 public OutputTextMessage HandlerEventClickMessage(InputEventClickMessage inputEventClickMessage) 4 { 5 return new OutputTextMessage() 6 { 7 Content = String.Format("你點了按鈕:[{1}]", inputEventClickMessage.EventKey) 8 }; 9 } 10 }
使用者:寫了這么多好累啊,剩下的工作就交給sdk處理吧。
sdk: 什么,剩下的工作都是我的,憑什么啊,,,
使用者:你妹啊,是你伺候我,不是我伺候你,剩下的你去辦吧,我再不寫一行代碼了。
2.易維護(對修改關閉,對擴展開放-不要波及與擴展無關的任何代碼)
這條基本素質的意思不用過多解釋了吧,更直白點就是說代碼應該盡量做到只增加,不修改(當然如果是涉及到修改也要把修改扼殺到最小的范圍內),苦逼的sdk要開始干活了,心里默念對修改關閉對擴展開放,,,
對微信消息sdk的設計我是這樣分解的:
- 解析xml字符串為實體對象;
- 根據實體對象分發到對應的消息處理程序;
- 執行消息處理程序,獲取響應消息;
這3部分邏輯其實就是上面的偽代碼 OutputMessage outputMessage=MessageClient.ProgressMessage(xmlContent) 的內部處理邏輯。
2.1消息解析器-解析xml字符串為實體對象
根據上面的需求,我們需要解析2類消息,文本類型的消息和click按鈕點擊類型的消息,如下:
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[this is a test]]></Content> <MsgId>1234567890123456</MsgId> </xml>
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[FromUser]]></FromUserName> <CreateTime>123456789</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[CLICK]]></Event> <EventKey><![CDATA[EVENTKEY]]></EventKey> </xml>
好了,xml結構有了,怎么解析呢,我這里有2中方案,反序列化xml和用xmlapi解析,其實都一樣,沒本質差異,我這里就用xml的api來解析了。但是,有個很重要的前提,那就是自己的事情自己做的(為文本消息建一個類,為click按鈕消息建一個類負責解析,如果有新增的消息類型,新建一個類就好了)。
public class InputTextMessage { public string Content { get; private set; } internal InputTextMessage(XElement xmlContent) { //一些共有字段的解析 //。。。 //解析我就不寫了 Content = "xxx"; } } public class InputEventClickMessage { public string EventKey { get; private set; } internal InputEventClickMessage(XElement xmlContent) { //一些共有字段的解析 //。。。 //解析我就不寫了 EventKey = "xxx"; } }
等等,咦,有一些公有字段,那就抽象成一個基類唄。於是代碼就變成了一下的樣子:
public class InputMessage { public String FormUserName { get; private set; } protected InputMessage(XElement xmlContent) { FormUserName = "xxx"; //其他共有字段的解析 } } public class InputTextMessage : InputMessage { public string Content { get; private set; } internal InputTextMessage(XElement xmlContent) : base(xmlContent) { //解析我就不寫了 Content = "xxx"; } } public class InputEventClickMessage : InputMessage { public string EventKey { get; private set; } internal InputEventClickMessage(XElement xmlContent) : base(xmlContent) { //解析我就不寫了 EventKey = "xxx"; } }
我想再強調一點訪問修飾符的重要性:一些代碼邏輯是在類內部,sdk內部完成的,不允許外部做寫操作的字段以及方法,那么它的訪問級別就應該嚴格控制起來,不該外部使用者看到的或者操作到的接口絕不公開。
解析式寫好了,但是我怎么判斷接收到的一個消息應該new哪一個實體類啊,微信官方還有好多其他類型的消息,難道我要寫switch一個一個判斷嗎,這樣就違背了對修改關閉,對擴展開放的原則了,新增一個類別的消息就改該switch的代碼,不好不好,不要波及無辜嘛,再說了,你是新增,為嘛要修改以前的代碼呢。
怎 么解決呢,翻翻文檔先,既然是很多類消息,那么它必定有方式來區分何種類型消息,嘿找到了,msgtype字段可以區分;但是還不夠完善,關注事件、點擊 按鈕都是的msgtype都是event,那就再加一個event字段.
好了我們的消息類型區分確定下來了,分為2類:
- msgtype
- msgtype_event
既然不用switch,那么怎么辦呢,怎么動態的在運行時創建一個對象出來呢,這時候C#的反射功能就排上用場了,我可以用Activator.CreateInstance傳入一個類型類型信息創建一個類,還可以傳構造參數(xmlContent作為構造參數傳遞進去)。
那么思路就有了,根據微信消息類型區分字段和對應的實體對象的類型信息作為一個映射表,獲取消息的類型區分字段,找到對應的實體對象的類型,反射創建出來對象。映射表就需要C#的Attribute上場了。
1 public class InputMessageDescriptorAttribute : Attribute 2 { 3 public String UniqueId { get; private set; } 4 5 public Type InputMessageType { get; internal set; } 6 7 8 public InputMessageDescriptorAttribute(String uniqueId) 9 { 10 this.UniqueId = uniqueId; 11 } 12 }
然后InputTextMessage和InputEventClickMessage就變成了如下樣子:
1 [InputMessageDescriptor("text")] 2 public class InputTextMessage : InputMessage 3 { 4 public string Content { get; private set; } 5 6 internal InputTextMessage(XmlElement xmlContent) 7 : base(xmlContent) 8 { 9 //解析我就不寫了 10 Content = "xxx"; 11 } 12 } 13 14 [InputMessageDescriptor("event_click")] 15 public class InputEventClickMessage : InputMessage 16 { 17 public string EventKey { get; private set; } 18 19 internal InputEventClickMessage(XmlElement xmlContent) 20 : base(xmlContent) 21 { 22 //解析我就不寫了 23 EventKey = "xxx"; 24 } 25 }
還有個小問題,微信消息還有加密模式,怎么解析呢?怎么應對這種擴展點呢,so,我們需要一個消息解析的接口來負責屏蔽這種差異,然后一個實現類負責明文消息的反射,一個實現類負責解密消息的反射(解密的實現類代碼就不貼了)。其實在一個實現類中負責明文和解密的邏輯也是一樣的。消息解析接口、其實現類、以及消息特性處理代碼如下:
1 public interface IMessageResolver 2 { 3 InputMessage GetInputMessage(XElement xmlContent); 4 } 5 6 public class MessageResolver : IMessageResolver 7 { 8 public InputMessage GetInputMessage(XElement xmlContent) 9 { 10 String uniqueId = String.Empty; 11 uniqueId = xmlContent.Element("MsgType").Value; 12 if (xmlContent.Element("event") != null) 13 { 14 uniqueId += "_" + xmlContent.Element("event").Value; 15 } 16 Type inputMessageType = null; 17 InputMessageDescriptorAttribute inputMessageDescriptor =
MessageConfig.GetInputMessageDescriptor(uniqueId); 18 if (inputMessageDescriptor != null) 19 { 20 inputMessageType = inputMessageDescriptor.InputMessageType; 21 } 22 else 23 { 24 inputMessageType = typeof(InputMessage); 25 } 26 return Activator.CreateInstance(inputMessageType, new Object[] { xmlContent }) as InputMessage; 27 } 28 } 29 30 public class MessageConfig 31 { 32 private static List<InputMessageDescriptorAttribute> _inputMessageDescriptors;//微信消息描述信息 33 static MessageConfig() 34 { 35 _inputMessageDescriptors = new List<InputMessageDescriptorAttribute>(); 36 Assembly currentAssembly = Assembly.GetExecutingAssembly(); 37 Type[] types = currentAssembly.GetTypes(); 38 foreach (var type in types) 39 { 40 InputMessageDescriptorAttribute inputMessageDescriptor =
type.GetCustomAttribute(typeof(InputMessageDescriptorAttribute)) as InputMessageDescriptorAttribute; 41 if (inputMessageDescriptor != null) 42 { 43 inputMessageDescriptor.InputMessageType = type; 44 _inputMessageDescriptors.Add(inputMessageDescriptor); 45 } 46 } 47 } 48 49 public static InputMessageDescriptorAttribute GetInputMessageDescriptor(String uniqueId) 50 { 51 foreach (var item in _inputMessageDescriptors) 52 { 53 if (String.Equals(uniqueId,item.UniqueId,StringComparison.OrdinalIgnoreCase)==true) 54 { 55 return item; 56 } 57 } 58 return null; 59 } 60 }
至此消息解析模塊完工啦,滿足了我們的要求,對擴展開放,對修改關閉,對於新增消息類型,我們只需寫新的InputXXXMessage類,然后用InputMessageDescriptorAttribute描述一下就好啦。
3.勿做過多假設
上面已經把消息解析模塊完成了,接下來要處理由消息實體對象到消息處理程序的分發了,我們呢先跳過這部分,先來處理下消息處理程序模塊,順帶也會來進行一次重構。
從使用者的代碼邏輯分析做起:
1 public class HandlerTextMessage 2 { 3 public OutputTextMessage HandlerTextMessage(InputTextMessage inputTextMessage) 4 { 5 //業務邏輯 6 } 7 8 } 9 10 public class HandlerEventClickMessage 11 { 12 public OutputTextMessage HandlerEventClickMessage(InputEventClickMessage inputEventClickMessage) 13 { 14 //業務邏輯 15 } 16 }
按照我的邏輯來說,每一類消息的處理程序都應該單獨是一個類,更進一步來講,每一種情況就是一個單獨的類,比如說現在的需求是要增加一個按鈕2,點擊返回我是按鈕2。那么我的處理辦法就是再增加一個類 HandlerEventClick2Message 來處理這件事情,而不是寫到 HandlerEventClickMessage.HandlerEventClickMessage() 方法內部來判斷。我的出發點如下:
- 如果放在個類中處理,那么久避免不了要用inputEventClickMessage的EventKey來做處理,這樣不就又是switch的路子了嗎,不又是在新增功能的時候去修改無關的代碼嗎,而只是把這種事情扔給了使用者去處理;
- 況且如果你如果讓使用者在代碼中固定判斷幾個eventkey的string值,也容易出錯,少拼一個字母多拼一個字母啦;
- 再退一步講,使用者關心的是點某一個按鈕后的業務邏輯代碼,憑什么你還要求我要知道這個按鈕的eventkey才能用呢,這些負擔不應該轉嫁到使用者頭上。
各位看官如果不知是否贊同我上面3個出發點,如有建議或意見請多多指教;其實我想說的就是不要對使用者做一些不必要的假設,假設他怎么我們的sdk,也不要把一些不必要的細節暴露給使用者(因為你一旦暴露出來之后使用者就可能會用到,那么這個細節就會帶來不必要的依賴關系,就很難做到低耦合);而是應該假設使用者都是小白、假設使用者會亂用我們的sdk(就像我們有時候會亂用.net 的api一樣(●'◡'●)),就像我們永遠不要相信用戶的輸入這條鐵的定律一樣。
3.1消息處理程序-執行客戶端業務邏輯&響應消息
根據上面我對消息處理程序的推論結果,我是要為每一個業務處理都建一個HandlerXXXMessage類,那么對應到sdk這邊,我們考慮的自然不是每一個業務邏輯怎么寫,而是怎么讓使用者可以對一個業務處理新建一個類來處理。so,必須要有一個抽象基類出現了,就像MVC的Controller基類那樣提供一些基礎的服務,讓使用者專注處理自己的業務邏輯:
1 public abstract class MessageHandler 2 { 3 public abstract OutputMessage Execute(InputMessage inputMessage); 4 }
這樣的話使用者的代碼就需要做一些調整了,結果如下:
1 public class HandlerTextMessage: MessageHandler 2 { 3 public override OutputMessage Execute(InputMessage inputTextMessage) 4 { 5 if (inputTextMessage.Content == "hello") 6 { 7 return new OutputTextMessage() 8 { 9 Content = "你好!" 10 }; 11 } 12 return new OutputTextMessage() 13 { 14 Content = "說人話,聽不懂..." 15 }; 16 } 17 } 18 public class HandlerEventClickMessage : MessageHandler 19 { 20 public override OutputMessage Execute(InputMessage inputEventClickMessage) 21 { 22 return new OutputTextMessage() 23 { 24 Content = String.Format("你點了按鈕:[{1}]", inputEventClickMessage.EventKey) 25 }; 26 } 27 }
細心的朋友可能已經發現問題了,所有參數都是InputMessage類型的,使用者處理文本消息需要的是InputTextMessage、處理按鈕消息需要的是InputEventClickMessage,難道你要使用者用的時候做強制類型轉換啊,,,要不得要不得滴。那怎么解決呢,在C#中如何處理呢,,,嘿,有了,泛型啊!於是就演化成了如下的代碼:
1 public abstract class MessageHandler<TInputMessage> where TInputMessage : InputMessage 2 { 3 public TInputMessage InputMessage { get; private set; } 4 5 protected MessageHandler(TInputMessage inputMessage) 6 { 7 this.InputMessage = inputMessage; 8 } 9 10 public abstract OutputMessage Execute(); 11 } 12 13 //客戶端代碼 14 public class HandlerTextMessage : MessageHandler<InputTextMessage> 15 { 16 public HandlerTextMessage(InputTextMessage inputMessage) : base(inputMessage) { } 17 18 public override OutputMessage Execute() 19 { 20 if (base.InputMessage.Content == "hello") 21 { 22 return new OutputTextMessage() 23 { 24 Content = "你好!" 25 }; 26 } 27 return new OutputTextMessage() 28 { 29 Content = "說人話,聽不懂..." 30 }; 31 } 32 } 33 34 //客戶端代碼 35 public class HandlerEventClickMessage : MessageHandler<InputEventClickMessage> 36 { 37 public HandlerEventClickMessage(InputEventClickMessage inputMessage) : base(inputMessage) { } 38 39 public override OutputMessage Execute() 40 { 41 return new OutputTextMessage() 42 { 43 Content = String.Format("你點了按鈕:[{1}]", base.InputMessage.EventKey) 44 }; 45 } 46 }
咦,好像還少點什么東西,OutputMessage消息的FormUserName和ToUserName要取自輸入消息的ToUserName和FormUserName,本着為使用者考慮,不讓使用者多寫無用代碼的思路下,那就重構下OutputMessage吧:
1 public abstract class OutputMessage 2 { 3 public String FormUserName { get; private set; } 4 5 public String ToUserName { get; private set; } 6 7 protected OutputMessage(InputMessage inputMessage) 8 { 9 this.FormUserName = inputMessage.ToUserName; 10 this.ToUserName = inputMessage.FormUserName; 11 //其他字段略。。。 12 } 13 14 public abstract String GetResult(); 15 } 16 17 public class OutputTextMessage : OutputMessage 18 { 19 public OutputTextMessage(InputMessage inputMessage) : base(inputMessage) { } 20 21 public string Content { get; set; } 22 23 public override string GetResult() 24 { 25 throw new System.NotImplementedException(); 26 } 27 }
好啦,到此消息處理程序這塊大體已經完工。應對新增業務代碼的處理方案就是繼承MessageHandler<TInputMessage>,用當前業務需要何種的輸入消息類型作為泛型參數,重寫Execute足以,同時也用泛型約束對客戶端代碼的書寫施加了基類約束,避免使用不當造成的錯誤,也避免掉了客戶端代碼要判斷eventkey的問題(並未徹底解決,往下看)。
3.2消息分發器-根據實體對象分發到對應的消息處理程序
上面已經完成了消息解析,響應消息的實體類和消息處理程序的規划和編寫,但是缺少了最重要的一個環節,如何從解析得到消息實體去執行相應的MessageHandler呢?
讓客戶端去獲取InputMessage的消息類型碼,比如你要客戶端這么干:
1 //客戶端代碼 2 IMessageResolver messageResolver = new MessageResolver(); 3 InputMessage inputMessage = messageResolver.GetInputMessage(xmlContent); 4 MessageHandler<InputMessage> messageHandler = null; 5 switch (inputMessage.MessageType) 6 { 7 case "text": 8 messageHandler = new HandlerTextMessage(inputMessage); 9 default: 10 break; 11 } 12 OutputMessage outputMessage = messageHandler.Execute();
這豈不是又要客戶端代碼依賴具體的實現細節了,新增一個業務邏輯又要調整不相干的代碼,還要假設客戶端知道消息類型(text,image),使用者還想要動態的調整響應消息,這種方法不妥不妥,,,那怎么搞呢,先賣個關子(晚上補上我的相關處理思路),歡迎大家一起來討論啊
我還會回來的,,,