阅读 789

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

版权声明:本文为博主原创文章,未经博主允许不得转载

Github:github.com/AnliaLee

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

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


封装之前的准备工作

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

封装的目的

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

按照第一点要求,那么我希望最终调用这个短信工具类时只有一个统一的入口,然后只需要简单地通过一个参数选择短信平台,按该平台要求传入相应的参数即可完成短信发送的操作以及获取返回数据。考虑到各个平台传参的命名都不一样,且可能有自定义的功能扩展,因此决定使用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才能学到更多的东西,共勉~

文章分类
Android