如何站在使用者的角度来设计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 ...
随机推荐
- KnocKout 绑定数据
Controller 里面的方法: public ActionResult Index() { return View(); } [HttpPost] public JsonResult Reader ...
- MVC 上传 下载
[上传]带进度条 view 注:添加easyui的js文件 <script type="text/javascript"> function fileSelected ...
- 指定的架构无效。错误: CLR 类型到 EDM 类型的映射不明确
在使用WebService开发时,同时使用了EF和linq,查询数据时,使用linq(查询订单)可以正常拉出数据, 但是使用EF(查询用户)却会报以下错误: {"指定的架构无效.错误: \r ...
- c# unchecked关键字。byte 合并short
参考MSDN 代码: public class BytesOperate { /// <summary> /// 计算校验和,SUM /// </summary> public ...
- duilib各种布局的作用,相对布局与绝对布局的的意义与用法
大多数刚使用duilib的朋友时候非常依赖duilib自带的设计器,用他可以拖拉控件,可视化的做出自己想要的界面.可是用一段时间就会发现原带的设计器有很多bug,时不时会崩溃,支持的控件数量有限,属性 ...
- STMFD 和LDMFD指令
http://blog.163.com/oy_mcu/blog/static/16864297220120193458892/ LDM/STM指令主要用于现场保护,数据复制,参数传送等. STMFD指 ...
- jdbc 数据的增删改查的Statement Resultset PreparedStatement
完成数据库的连接,就马上要对数据库进行增删改查操作了:先来了解一下Statement 通过JDBC插入数据 (这里提供一个查找和插入方法) Statement:用于执行sql语句的对象: *1.通过C ...
- win7&win8.1 x64位系统下在VS2010下配置MPICH2&测试&解决scanf不能输入
1. Mpich下载地址http://www.mpich.org/downloads/,下载mpich2-1.4.1p1-win-x86-64(32位系统请下载mpich2-1.4.1p1 ...
- TortoiseGit 连接oschina不用每次输入用户名和密码的方法
每次git clone 和push 都要输入用户名和密码.虽然安全,但在本机上每次都输有些麻烦,如何记住用户名和密码呢? 在网上看了各种方法,太杂,很多可能环境不一样,一直行不通.最后找到一种有效的方 ...
- memcache的最佳实践方案
1.memcached的基本设置 1)启动Memcache的服务器端 # /usr/local/bin/memcached -d -m 10 -u root -l 192.168.0.200 -p 1 ...