如何站在使用者的角度来设计SDK-微信公众号开发SDK(消息处理)设计之抛砖引玉
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中类型的消息,具体的消息内容我就不贴出来了,使用者最直接的用法是什么呢?
文本消息的使用场景(伪代码):
public class HandlerTextMessage { public OutputTextMessage HandlerTextMessage(InputTextMessage inputTextMessage) { if (inputTextMessage.Content == "hello") { return new OutputTextMessage() { Content = "你好!" }; } return new OutputTextMessage() { Content = "说人话,听不懂..." }; } }
按钮点击事件消息的使用场景(伪代码):
public class HandlerEventClickMessage { public OutputTextMessage HandlerEventClickMessage(InputEventClickMessage inputEventClickMessage) { return new OutputTextMessage() { Content = String.Format("你点了按钮:[{1}]", inputEventClickMessage.EventKey) }; } }
使用者:写了这么多好累啊,剩下的工作就交给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上场了。
public class InputMessageDescriptorAttribute : Attribute { public String UniqueId { get; private set; } public Type InputMessageType { get; internal set; } public InputMessageDescriptorAttribute(String uniqueId) { this.UniqueId = uniqueId; } }
然后InputTextMessage和InputEventClickMessage就变成了如下样子:
[InputMessageDescriptor("text")] public class InputTextMessage : InputMessage { public string Content { get; private set; } internal InputTextMessage(XmlElement xmlContent) : base(xmlContent) { //解析我就不写了 Content = "xxx"; } } [InputMessageDescriptor("event_click")] public class InputEventClickMessage : InputMessage { public string EventKey { get; private set; } internal InputEventClickMessage(XmlElement xmlContent) : base(xmlContent) { //解析我就不写了 EventKey = "xxx"; } }
还有个小问题,微信消息还有加密模式,怎么解析呢?怎么应对这种扩展点呢,so,我们需要一个消息解析的接口来负责屏蔽这种差异,然后一个实现类负责明文消息的反射,一个实现类负责解密消息的反射(解密的实现类代码就不贴了)。其实在一个实现类中负责明文和解密的逻辑也是一样的。消息解析接口、其实现类、以及消息特性处理代码如下:
public interface IMessageResolver { InputMessage GetInputMessage(XElement xmlContent); } public class MessageResolver : IMessageResolver { public InputMessage GetInputMessage(XElement xmlContent) { String uniqueId = String.Empty; uniqueId = xmlContent.Element("MsgType").Value; if (xmlContent.Element("event") != null) { uniqueId += "_" + xmlContent.Element("event").Value; } Type inputMessageType = null; InputMessageDescriptorAttribute inputMessageDescriptor = MessageConfig.GetInputMessageDescriptor(uniqueId); if (inputMessageDescriptor != null) { inputMessageType = inputMessageDescriptor.InputMessageType; } else { inputMessageType = typeof(InputMessage); } return Activator.CreateInstance(inputMessageType, new Object[] { xmlContent }) as InputMessage; } } public class MessageConfig { private static List<InputMessageDescriptorAttribute> _inputMessageDescriptors;//微信消息描述信息 static MessageConfig() { _inputMessageDescriptors = new List<InputMessageDescriptorAttribute>(); Assembly currentAssembly = Assembly.GetExecutingAssembly(); Type[] types = currentAssembly.GetTypes(); foreach (var type in types) { InputMessageDescriptorAttribute inputMessageDescriptor = type.GetCustomAttribute(typeof(InputMessageDescriptorAttribute)) as InputMessageDescriptorAttribute; if (inputMessageDescriptor != null) { inputMessageDescriptor.InputMessageType = type; _inputMessageDescriptors.Add(inputMessageDescriptor); } } } public static InputMessageDescriptorAttribute GetInputMessageDescriptor(String uniqueId) { foreach (var item in _inputMessageDescriptors) { if (String.Equals(uniqueId,item.UniqueId,StringComparison.OrdinalIgnoreCase)==true) { return item; } } return null; } }
至此消息解析模块完工啦,满足了我们的要求,对扩展开放,对修改关闭,对于新增消息类型,我们只需写新的InputXXXMessage类,然后用InputMessageDescriptorAttribute描述一下就好啦。
3.勿做过多假设
上面已经把消息解析模块完成了,接下来要处理由消息实体对象到消息处理程序的分发了,我们呢先跳过这部分,先来处理下消息处理程序模块,顺带也会来进行一次重构。
从使用者的代码逻辑分析做起:
public class HandlerTextMessage { public OutputTextMessage HandlerTextMessage(InputTextMessage inputTextMessage) { //业务逻辑 } } public class HandlerEventClickMessage { public OutputTextMessage HandlerEventClickMessage(InputEventClickMessage inputEventClickMessage) { //业务逻辑 } }
按照我的逻辑来说,每一类消息的处理程序都应该单独是一个类,更进一步来讲,每一种情况就是一个单独的类,比如说现在的需求是要增加一个按钮2,点击返回我是按钮2。那么我的处理办法就是再增加一个类 HandlerEventClick2Message 来处理这件事情,而不是写到 HandlerEventClickMessage.HandlerEventClickMessage() 方法内部来判断。我的出发点如下:
- 如果放在个类中处理,那么久避免不了要用inputEventClickMessage的EventKey来做处理,这样不就又是switch的路子了吗,不又是在新增功能的时候去修改无关的代码吗,而只是把这种事情扔给了使用者去处理;
- 况且如果你如果让使用者在代码中固定判断几个eventkey的string值,也容易出错,少拼一个字母多拼一个字母啦;
- 再退一步讲,使用者关心的是点某一个按钮后的业务逻辑代码,凭什么你还要求我要知道这个按钮的eventkey才能用呢,这些负担不应该转嫁到使用者头上。
各位看官如果不知是否赞同我上面3个出发点,如有建议或意见请多多指教;其实我想说的就是不要对使用者做一些不必要的假设,假设他怎么我们的sdk,也不要把一些不必要的细节暴露给使用者(因为你一旦暴露出来之后使用者就可能会用到,那么这个细节就会带来不必要的依赖关系,就很难做到低耦合);而是应该假设使用者都是小白、假设使用者会乱用我们的sdk(就像我们有时候会乱用.net 的api一样(●'◡'●)),就像我们永远不要相信用户的输入这条铁的定律一样。
3.1消息处理程序-执行客户端业务逻辑&响应消息
根据上面我对消息处理程序的推论结果,我是要为每一个业务处理都建一个HandlerXXXMessage类,那么对应到sdk这边,我们考虑的自然不是每一个业务逻辑怎么写,而是怎么让使用者可以对一个业务处理新建一个类来处理。so,必须要有一个抽象基类出现了,就像MVC的Controller基类那样提供一些基础的服务,让使用者专注处理自己的业务逻辑:
public abstract class MessageHandler { public abstract OutputMessage Execute(InputMessage inputMessage); }
这样的话使用者的代码就需要做一些调整了,结果如下:
public class HandlerTextMessage: MessageHandler { public override OutputMessage Execute(InputMessage inputTextMessage) { if (inputTextMessage.Content == "hello") { return new OutputTextMessage() { Content = "你好!" }; } return new OutputTextMessage() { Content = "说人话,听不懂..." }; } } public class HandlerEventClickMessage : MessageHandler { public override OutputMessage Execute(InputMessage inputEventClickMessage) { return new OutputTextMessage() { Content = String.Format("你点了按钮:[{1}]", inputEventClickMessage.EventKey) }; } }
细心的朋友可能已经发现问题了,所有参数都是InputMessage类型的,使用者处理文本消息需要的是InputTextMessage、处理按钮消息需要的是InputEventClickMessage,难道你要使用者用的时候做强制类型转换啊,,,要不得要不得滴。那怎么解决呢,在C#中如何处理呢,,,嘿,有了,泛型啊!于是就演化成了如下的代码:
public abstract class MessageHandler<TInputMessage> where TInputMessage : InputMessage { public TInputMessage InputMessage { get; private set; } protected MessageHandler(TInputMessage inputMessage) { this.InputMessage = inputMessage; } public abstract OutputMessage Execute(); } //客户端代码 public class HandlerTextMessage : MessageHandler<InputTextMessage> { public HandlerTextMessage(InputTextMessage inputMessage) : base(inputMessage) { } public override OutputMessage Execute() { if (base.InputMessage.Content == "hello") { return new OutputTextMessage() { Content = "你好!" }; } return new OutputTextMessage() { Content = "说人话,听不懂..." }; } } //客户端代码 public class HandlerEventClickMessage : MessageHandler<InputEventClickMessage> { public HandlerEventClickMessage(InputEventClickMessage inputMessage) : base(inputMessage) { } public override OutputMessage Execute() { return new OutputTextMessage() { Content = String.Format("你点了按钮:[{1}]", base.InputMessage.EventKey) }; } }
咦,好像还少点什么东西,OutputMessage消息的FormUserName和ToUserName要取自输入消息的ToUserName和FormUserName,本着为使用者考虑,不让使用者多写无用代码的思路下,那就重构下OutputMessage吧:
public abstract class OutputMessage { public String FormUserName { get; private set; } public String ToUserName { get; private set; } protected OutputMessage(InputMessage inputMessage) { this.FormUserName = inputMessage.ToUserName; this.ToUserName = inputMessage.FormUserName; //其他字段略。。。 } public abstract String GetResult(); } public class OutputTextMessage : OutputMessage { public OutputTextMessage(InputMessage inputMessage) : base(inputMessage) { } public string Content { get; set; } public override string GetResult() { throw new System.NotImplementedException(); } }
好啦,到此消息处理程序这块大体已经完工。应对新增业务代码的处理方案就是继承MessageHandler<TInputMessage>,用当前业务需要何种的输入消息类型作为泛型参数,重写Execute足以,同时也用泛型约束对客户端代码的书写施加了基类约束,避免使用不当造成的错误,也避免掉了客户端代码要判断eventkey的问题(并未彻底解决,往下看)。
3.2消息分发器-根据实体对象分发到对应的消息处理程序
上面已经完成了消息解析,响应消息的实体类和消息处理程序的规划和编写,但是缺少了最重要的一个环节,如何从解析得到消息实体去执行相应的MessageHandler呢?
让客户端去获取InputMessage的消息类型码,比如你要客户端这么干:
//客户端代码 IMessageResolver messageResolver = new MessageResolver(); InputMessage inputMessage = messageResolver.GetInputMessage(xmlContent); MessageHandler<InputMessage> messageHandler = null; switch (inputMessage.MessageType) { case "text": messageHandler = new HandlerTextMessage(inputMessage); default: break; } OutputMessage outputMessage = messageHandler.Execute();
这岂不是又要客户端代码依赖具体的实现细节了,新增一个业务逻辑又要调整不相干的代码,还要假设客户端知道消息类型(text,image),使用者还想要动态的调整响应消息,这种方法不妥不妥,,,那怎么搞呢,先卖个关子(晚上补上我的相关处理思路),欢迎大家一起来讨论啊
我还会回来的,,,
如何站在使用者的角度来设计SDK-微信公众号开发SDK(消息处理)设计之抛砖引玉的更多相关文章
- 微信公众号菜单openid 点击菜单即可打开并登录微站
现在大部分微站都通过用户的微信openid来实现自动登录.在我之前的开发中,用户通过点击一个菜单,公众号返回一个图文,用户点击这个图文才可以自动登录微站.但是如果你拥有高级接口,就可以实现点击菜单,打 ...
- 微信公众平台应用开发框架sophia设计不足(1)
设计一个小框架考虑的东西真不少,每一样都不easy: 1.既要解决当前技术的不足: 2.又要方便他人使用(基本的目的). 3.同一时候又要设计得优雅.easy扩展. sophia一開始设计用来支持智能 ...
- 建站集成软件包 XAMPP搭建后台系统与微信小程序开发
下载安装XAMPP软件,运行Apache和MySQL 查看项目文件放在哪个位置可以正常运行 然后访问localhost即可 下载weiphp官网的weiapp(专为微信小程序开发使用)放在htdocs ...
- 【云速建站】微信公众平台中维护IP白名单
[摘要] 介绍获取接入IP白名单的操作步骤 网站后台对接微信公众号.支付等都依赖于白名单,接下来就介绍一下白名单的配置. 1.1 为什么要设置白名单 为了提高公众平台开发者接口调用的安全性, ...
- 简易音乐播放器主界面设计 - .NET CORE(C#) WPF开发
微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. 简易音乐播放器主界面设计 - .NET CORE(C#) WPF开发 阅读导航 本文背景 代码 ...
- 投资人的能量往往大多远远不仅于此,他能站在不同的角度和高度看问题(要早点拿投资,要舍得让出股份)——最好不要让 Leader 一边做技术、一边做管理,人的能力是有限的,精力也是有限的
摘要:在创业三年时间里作为联合创始人,虽然拿着大家均等的股份,我始终是没有什么话语权的,但是,这也给了我从旁观者的角度看清整个局面的机会.创业公司的成败绝大程度取决于技术大牛和公司 Leader, ...
- 站在Java的角度看LinkedList
站在Java的角度看,玩队列不就是玩对象引用对象嘛! public class LinkedList<E> implements List<E>, Deque<E> ...
- 升讯威微信营销系统开发实践:(1)功能概要与架构设计( 完整开源于 Github)
GitHub:https://github.com/iccb1013/Sheng.WeixinConstruction因为个人精力时间有限,不会再对现有代码进行更新维护,不过微信接口比较稳定,经测试至 ...
- [连载]《C#通讯(串口和网络)框架的设计与实现》-3.设备驱动的设计
目 录 第三章 设备驱动的设计... 2 3.1 初始化设备... 4 3.2 运行设备接口设计... 4 3.3 ...
随机推荐
- node静态资源管理变迁之路
使用express自带的,express.static,如:app.use(express.static('hehe')),就可以用localhost/hua.png,访问项目根目录下,hehe文件夹 ...
- C# 中GUID生成格式的四种格式
var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12 var uuidN = Guid.NewGu ...
- laravel 在windows中使用一键安装包步骤
安装 PHP 注意一:Laravel 5.0 开始对 PHP 版本的要求是 >=5.4,Laravel 5.1 要求 PHP 版本 >=5.5.9,所以,建议大家尽量安装 5.5.x 的最 ...
- Docker version 1.12.5建立registry私库
sudo docker run -d -p 5000:5000 -v /opt/data/registry:/var/lib/registry registry :前面的是宿主机的地址(/opt/da ...
- The connection to adb is down, and a severe error has occured.(转)
启动android模拟器时.有时会报The connection to adb is down, and a severe error has occured.的错误.在网友说在任务管理器上把所有ad ...
- with CTE AS
CTE/表变量/Temp http://www.cnblogs.com/ziyeyimeng/articles/2366855.html
- [原创]java WEB学习笔记103:Spring学习---Spring Bean配置:基于注解的方式(基于注解配置bean,基于注解来装配bean的属性)
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
- 浅谈HTTPS和SSL/TLS协议的背景和基础
相关背景知识要说清楚HTTPS协议的实现原理,至少要需要如下几个背景知识.大致了解几个基础术语(HTTPS.SSL.TLS)的含义大致了解HTTP和TCP的关系(尤其是"短连接"和 ...
- HDU - 1875 畅通工程再续
Problem Description 相信大家都听说一个“百岛湖”的地方吧,百岛湖的居民生活在不同的小岛中,当他们想去其他的小岛时都要通过划小船来实现.现在政府决定大力发展百岛湖,发展首先要解决的问 ...
- 夺命雷公狗----Git---3---vi编辑器
如果直接使用了 git commit 即进入vi编辑器,所以强烈推荐使用 git commit -m 中文注释 但是如果进入vi编辑器其实也没什么好怕的,如果动linux 的朋友应该都会使用 进入v ...