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),使用者还想要动态的调整响应消息,这种方法不妥不妥,,,那怎么搞呢,先卖个关子(晚上补上我的相关处理思路),欢迎大家一起来讨论啊
我还会回来的,,,