Android项目实践——短信发送接口的封装与设计

版权声明:本文为博主原创文章,未经博主允许不得转载。
系列教程:Android开发之从零开始系列

大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言:前一段时间公司服务端开发人手不足,而项目急需对接某个平台的短信发送接口,于是我便揽下了这个任务。看了看原来短信发送的工具类代码,发现好几个平台的短信接口方法都堆在一起了,相互之间仅以方法名作为区分,整个工具类好几百行代码,无论是调用相关方法还是维护原有代码,又或者是集成新的短信平台都十分不方便,于是决定一边学习面向对象设计的知识,一边动手重构短信工具类。本期就以市面上几款常见的短信接口为例子,聊一聊这种单一功能(发送短信)但有多种方案(多平台)的工具类的封装过程,希望能对大家项目开发和功能集成有所启发


封装之前的准备工作

在开始动手之前,我们需要考虑怎样去设计这个工具类。首先得明确为什么要进行封装

封装的目的
  • 调用短信工具类的用户尽可能地降低使用成本
  • 后续维护工具类的开发人员能够更加方便地集成其他短信平台和维护原有的代码

按照第一点要求,那么我希望最终调用这个短信工具类时只有一个统一的入口,然后只需要简单地通过一个参数选择短信平台,按该平台要求传入相应的参数即可完成短信发送的操作以及获取返回数据。考虑到各个平台传参的命名都不一样,且可能有自定义的功能扩展,因此决定使用Builder模式去设计工具类入口

再来看第二点要求,如果像原来那样将所有的短信平台接口都放到同一个工具类中,各种解析函数也放在这里面,那维护起来将异常麻烦,还可能会因为集成新的短信平台而引入未知的BUG,因此我们需要将各平台对接代码隔离开来

那么分析了短信工具类如何入手开发之后,根据“自上而下设计,自下而上实现”的思想,我们从各平台短信接口的集成开始一步步实现这个工具类


抽象集成短信平台接口

原有的短信工具类中集成了阿里云、网易云、云信等平台的短信发送接口,它们有的只能使用post进行数据传输,有的post、get两种方式都可以,因此我们进行抽象时需要将这两种传输方式都考虑进来,创建SMSModel抽象类

public abstract class SMSModel {
    public abstract String post(Map<String, String> map);
    public abstract String get(Map<String, String> map);
}

以阿里云的短信发送接口为例,新建ALModel继承SMSModel,实现具体的请求过程

/**
 * 阿里云短信平台
 */
public class ALModel extends SMSModel{

    @Override
    public String post(Map<String, String> map){
        //省略具体的代码实现...
        return result;
    }

    @Override
    public String get(Map<String, String> map) {//因为该平台不支持get方式传输数据,直接返回错误信息即可
        return "请求失败!该平台不支持get请求";
    }
    
}

接着按照阿里云短信平台的开发文档,按照入参列表创建SMSParameter,便于用户注入参数

public class SMSParameter {
    
    //短信接口平台
    public static final String SMS_MODEL_AL = "AL";//阿里云短信平台
    
    //阿里云短信接口参数,文档:https://help.aliyun.com/document_detail/55284.html?spm=5176.doc55289.6.557.J43llA
    public static final String AL_KEYID = "AccessKeyId";
    public static final String AL_KEYSECRET = "AccessKeySecret";
    public static final String AL_PHONENUMBERS = "PhoneNumbers";//短信接收号码
    public static final String AL_SIGNNAME = "SignName";//短信签名
    public static final String AL_TEMPLATECODE = "TemplateCode";//短信模板ID
    public static final String AL_TEMPLATEPARAM = "TemplateParam";//短信模板变量替换JSON串
    public static final String AL_SMSUPEXTENDCODE = "SmsUpExtendCode";//上行短信扩展码
    public static final String AL_OUTID = "OutId";//外部流水扩展字段
}

其他短信平台的集成也是如此,就不多赘述了


实现构建Builder类和中间件Request类

用户在使用我们的封装工具类时,无需知道短信接口具体是如何调用的,隐藏短信接口调用过程可以避免用户调用错误时引发的一系列问题,能够有效地降低用户使用成本。因此我们创建SMSBuilder类用于管理和配置用户调用工具类的传参,实现工具类的自由扩展和构建。创建SMSRequest类用于连接SMSBuilder和SMSModel,起到中间桥梁的作用

先来看SMSBuilder类,我们暂定几个用于初始化工具类的参数,使用枚举(ModelType)限制用户可选用的短信平台,并在setModelType方法中根据用户的选择实例化相应的SMSModel,最后当用户使用build()方法完成工具类初始化时实例化一个SMSRequest执行调用短信接口的操作SMSBuilder代码如下

public abstract class SMSBuilder {
    public String builderType;//builder类型,分为post和get
    public String modelType;//用户选择的短信平台
    public Map<String, String> map;//保存短信平台的传参
    public SMSModel smsModel;
    
    /**
     * 短信平台:
     * SMS_MODEL_AL(阿里短信平台)
     */
    public enum ModelType{
        SMS_MODEL_AL
    }
    
    public SMSBuilder() {}
    
    public SMSBuilder setModelType(ModelType modelType) {
        if (modelType != null) {
            switch (modelType) {
            case SMS_MODEL_AL:
                this.modelType = SMSParameter.SMS_MODEL_AL;
                smsModel = new ALModel();
                break;
            default:
                this.modelType = "";
                break;
            }
        }
        return this;
    }
    
    public SMSBuilder addMapParams(Map<String, String> map){
        if(this.map == null){
            this.map = map;
        }
        return this;
    }
    
    public SMSRequest build(){
        return new SMSRequest(this);
    }
}

SMSRequest类根据用户利用SMSBuilder初始化的参数执行具体的调用接口操作(toRequest),代码如下

public class SMSRequest {
    private SMSModel smsModel;
    private String builderType;
    private String modelType;
    private Map<String, String> paramsMap;
    private String result;
    
    private String errorMessage;
    
    public SMSRequest(SMSBuilder builder){
        builderType = builder.builderType;
        modelType = builder.modelType;
        paramsMap = builder.map;
        smsModel = builder.smsModel;
        result = "";
        errorMessage = modelType+":"+builderType;
        
        if(builder.modelType == null || builder.modelType.equals("")){
            result = builderType+"请求失败!短信接口类型不能为空";
            return;
        }
        toRequest();
    }
    
    /**
     * 同步获取返回数据
     */
    public String execute(){
        return result;
    }
    
    private void toRequest(){
        if(paramsMap==null){
            result = errorMessage+"请求失败!map不能为空!";
            return;
        }
        if(builderType.equals("post")){
            result = smsModel.post(paramsMap);
        }else if(builderType.equals("get")){
            result = smsModel.get(paramsMap);
        }
    }
}

这里仅以最基础的功能配置为例,若大家需要扩展更多的功能例如超时提醒、群发短信或异步接收回参等可以在此基础上修改


实现面向用户的SMSUtils类

最后再来看下用户直接接触到的类SMSUtils,代码比较简单,这里使用了单例模式实例化SMSUtils,并在其中定义了PostBuilderGetBuilder用于区分post和get请求,具体代码如下

public class SMSUtils {
    
    private volatile static SMSUtils mInstance;
    
    private SMSUtils(){}
    
    private static class SMSUtilsHolder{
        private static final SMSUtils mInstance = new SMSUtils();
    }
    
    public static SMSUtils getInstance(){
        return SMSUtilsHolder.mInstance;
    }
    
    public class PostBuilder extends SMSBuilder{
        public PostBuilder(){
            this.builderType = "post";
        }
    }
    
    public class GetBuilder extends SMSBuilder{
        public GetBuilder(){
            this.builderType = "get";
        }
    }
    
    public static PostBuilder post(){
        return getInstance().new PostBuilder();
    }
    
    public static GetBuilder get(){
        return getInstance().new GetBuilder();
    }
}

完成整个工具类的封装后,用户以后调用短信发送接口时只需要像下面示例那样简单写几行代码即可

String result = "";
Map<String, String> map = new HashMap<String, String>();
map.put(SMSParameter.XXX, 参数内容);
...//按照平台要求配置相应参数

result = SMSUtils
        .post()// post():请求类型为post,get():请求类型为get
        .setModelType(ModelType.SMS_MODEL_AL)// 选择短信平台
        .addMapParams(map)// 注入参数map
        .build()// 完成初始化
        .execute();
System.out.println("result:"+result);

整个工具类的目录结构如下,其中SMSUnitTest用于单元测试,model目录下存放各短信平台的具体实现类,parameter目录用于存放公用的参数常量类,utils目录用于存放某些需要用到的工具类如xml、json解析类等

整个工具类介绍完了,回到我们一开始提出的两点封装目的,我们的工具类是否有效降低了用户的使用成本?我觉得相对于在几百上千行的工具类中查找需要使用的短信接口来说,是的。Builder模式链式结构的初始化过程能让用户调用我们的工具类更加顺手且代码逻辑清晰易读性好。那么是否降低了维护人员的开发成本呢?我们以集成新的短信平台为例,维护人员只需要在model包下创建新的平台实现类,然后在SMSBuilder中配置相应的参数即可,大大降低了工具类的耦合度,也减少了多人开发造成冲突的可能性。若某个平台的对接出现BUG,仅需要在相应的实现类中DEBUG即可

本期博客到这里就结束了,由于我个人能力有限,有些地方肯定做得还不够好,若大家有什么建议欢迎留言指出,不断地写BUG再修复BUG才能学到更多的东西,共勉~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,615评论 4 59
  • 我们所有的成绩都已经出来了,大家都在焦急的等待老师把我们的成绩报出来,我特别紧张。 我同学偷偷的替我看成绩单上的数...
    爱梦的我阅读 100评论 0 0
  • 1、与人沟通前、沟通中提醒自己跳出戏剧三角,重视重要目标的达成。(今天发生的事情,自己在受害者的位置 ,因为情绪(...
    懒懒的妈阅读 285评论 0 1
  • 注释 代码注释是架起程序设计者与程序阅读者之间的通信桥梁,最大限度的提高团队开发合作效率。也是程序代码可维护性的重...
    青貊阅读 287评论 0 0