如何站在使用者的角度来设计SDK-微信公众号开发SDK(消息处理)设计之抛砖引玉

0.SDK之必备的基本素质

在项目中免不了要用到各种各样的第三方的sdk,在我现在的工作中就在公司内部积累了各种各样的的公共库(基于.net的,基于silverlight的等等),托管到了内部的nuget私服上,大大的方便了项目的开发。

在积累这些库的过程中走过不少弯路,今天分享给大家(借助微信公众平台开发的消息处理模块的SDK(一下简称微信消息sdk)做个设计思路剖析)笔者的一些思路的,私以为一个sdk需要具备如下的3条基本素质。

  1. 站在使用者的角度考虑设计
  2. 易维护( 对修改关闭,对扩展开放 -不要波及与扩展无关的任何代码
  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);

这只是一个固定的处理流程,那么需求来了:

  1. 用户发送一个hello的文本,我们要回复一条你好的文本消息;
  2. 用户点击一个微信菜单按钮(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的设计我是这样分解的:

  1. 解析xml字符串为实体对象;
  2. 根据实体对象分发到对应的消息处理程序;
  3. 执行消息处理程序,获取响应消息;

这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类:

  1. msgtype
  2. 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() 方法内部来判断。我的出发点如下:

  1. 如果放在个类中处理,那么久避免不了要用inputEventClickMessage的EventKey来做处理,这样不就又是switch的路子了吗,不又是在新增功能的时候去修改无关的代码吗,而只是把这种事情扔给了使用者去处理;
  2. 况且如果你如果让使用者在代码中固定判断几个eventkey的string值,也容易出错,少拼一个字母多拼一个字母啦;
  3. 再退一步讲,使用者关心的是点某一个按钮后的业务逻辑代码,凭什么你还要求我要知道这个按钮的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();

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏尚国

深入剖析最新IE0day漏洞

在2018年4月下旬,我们使用沙箱发现了IE0day漏洞;自从在野外发现上一个样本(CVE-2016-0189)已经有两年多了。从许多方面来看,这个特别的漏洞及...

16420
来自专栏Java技术分享圈

杨老师课堂_Java教程第一篇之认识计算机

*接下来就是确定、确定、确定就ok! *检验环境变量是否成功在DOS命令行里输入javac或java,如果正常显示一些内容,说明安装成功并且配...

11820
来自专栏MageekChiu

为什么要指令重排序?

我们知道java在运行的时候有两个地方可能用到重排序,一个是编译器编译的的时候,一个是处理器运行的时候。

35150
来自专栏更流畅、简洁的软件开发方式

【开源】QuickPager 分页控件的内部结构,和OO原则与设计模式

关键字:提出需求、需求分析、原则、设计模式、索引      先说一下讨论的范围:使用数据库保存信息的项目,b/s结构,asp.net编写。请不要讨论这个范围之外...

21960
来自专栏java学习

Java基础第一天学习笔记

01.01_计算机基础知识(计算机概述)(了解) * A:什么是计算机?计算机在生活中的应用举例 * 计算机(Computer)全称:电子计算机,俗称电脑。是...

37950
来自专栏木子昭的博客

从3000行缩减到1000行,用django类代替视图函数

11020
来自专栏FreeBuf

Struts2再曝S2-020补丁绕过漏洞 – 万恶的正则表达式

4月24日,网络曝出文章“安全研究人员指出Apache Struts2在漏洞公告S2-020里,在处理修复CVE-2014-0094的漏洞修补方案存在漏洞,导致...

23760
来自专栏Android-JessYan

一行代码实现Okhttp,Retrofit,Glide下载上传进度监听

原文地址: http://www.jianshu.com/p/5832c776621f qq群:301733278

22020
来自专栏静晴轩

Gulp折腾之路(II)

前段时间折腾Gulp,主要是搜寻一些插件,组合之以优化前端开发流程。这段折腾历程除了达成所愿外,给予最大的收获是:只要你想实现某功能,基本就已有对应插件供使用;...

37150
来自专栏猿人谷

PHP程序员应该掌握的10个技能

php程序员应该掌握的10个技能,看看你都掌握了哪些:   1、语法:必须熟练掌握 ,写代码的时候IDE的编辑器对某一行报错应该能够根据报错信息知道是什么样的语...

25170

扫码关注云+社区

领取腾讯云代金券