模板方法模式
模板方法模式
模板方法模式是一种行为设计模式,它在一个抽象类中定义好了一类行为的步骤流程,且允许子类在保存行为结构不修改的情况下对具体的步骤进行修改。下面将结合给企业微信发送文本消息和文本卡片消息这两种行为来说明模板方法如何使用。
一、企业微信的文本消息和文本卡片消息需要的参数
请求方式:POST(HTTPS)
请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
返回结果: |
{
"errcode" : 0,
"errmsg" : "ok",
"invaliduser" : "userid1|userid2",
"invalidparty" : "partyid1|partyid2",
"invalidtag": "tagid1|tagid2"
}
参考:企业微信发送应用消息
- 文本消息需要的请求参数
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1|PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "text",
"agentid" : 1,
"text" : {
"content" : "你的快递已到,请携带工卡前往邮件中心领取。\n出发前可查看<a href=\"http://work.weixin.qq.com\">邮件中心视频实况</a>,聪明避开排队。"
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
- 文本卡片消息需要的请求参数
{
"touser" : "UserID1|UserID2|UserID3",
"toparty" : "PartyID1 | PartyID2",
"totag" : "TagID1 | TagID2",
"msgtype" : "textcard",
"agentid" : 1,
"textcard" : {
"title" : "领奖通知",
"description" : "<div class=\"gray\">2016年9月26日</div> <div class=\"normal\">恭喜你抽中iPhone 7一台,领奖码:xxxx</div><div class=\"highlight\">请于2016年10月10日前联系行政同事领取</div>",
"url" : "URL",
"btntxt":"更多"
},
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
二、解析说明
我们要向企业发送两种不同形式的消息给企业微信,这两种形式需要的参数有部分相同的,有部分是不相同的。为了更好的体现什么是模板方法模式,现在我们假设上面两种形式的所有的参数都是必传的且不能为空。那么现在我们来整理整个发送的步骤流程:
- 获取access_token(两种形式都要)
- 校验参数是否都已经必传了,且不能为空(两种形式都要,不同的消息校验的逻辑不相同)
- 参数校验通过后,构造不同的实体VO(区别)
- 发送请求给企业微信
- 微信返回结果后,都要对结果进行处理(假设我们收到响应结果后,根据不同的消息形式给调用方响应不同的内容)
可以看到两种消息都需要进行以上的步骤,那么我们就可以定义一个抽象类来设定好这个发送消息的模板。
【模板方法结构图】
三、各消息对应的VO实体
- 共同的VO
public class CommonVo {
private String touser;
private String toparty;
private String totag;
private String msgtype;
private String agentid;
private String enable_id_trans;
private String enable_duplicate_check;
private String duplicate_check_interval;
}
- 文本消息对应的VO
public class TextVo extends CommonVo {
private String safe;
private TextMessage text;
}
class TextMessage {
private String content;
}
- 文本卡片消息对应的VO
public class TextCardVo extends CommonVo {
private TextCardMessage text;
}
class TextCardMessage {
private String description;
private String title;
private String url;
private String btntxt;
}
四、构建一个抽象类作为模板方法及其说明
public abstract class AbsQywxSendMessage {
//我们把上面的方法步骤在这个抽象类中定义好,假设调用方是用Map来传递参数的
public String sendQywxMessage(Map<String, Object> params) {
//1、获取access_token
String accessToken = getAccessToken();
//2、校验参数是否都有值,且参数是否都完整,在这一步需要分情况,在两种消息形式中,有大部分的参数的key都是一样的,
//所以我们在抽象类可以把相同的参数抽出来做一套相同的校验逻辑。不同的参数,则交给子类来具体实现
identicalParamsCheck(Map<String, Object> params);
//2.1、校验不同的参数,不同的参数有不同的校验逻辑,属性两套不同的逻辑,只能交给子类来实现了
//不同的校验参数:文本消息的参数多了一个safe,且text对应的内容也不同
diffParamsCheck(Map<String, Object> params);
//3、参数校验通过后,构造不同的VO,此处也是由子类来具体实现
CommonVo paramVo = createVo(Map<String, Object> param);
//4、发送消息到企业微信
String resultStr = sendMessage(paramVo, accessToken, url);
//5、对结果进行处理响应,由子类来具体实现
return responseResult(resultStr);
}
//1、获取access_token,此方法不能被子类修改,且两种形式都需要到access_token,所以我们定义这个方法为私有的(不需要子类参与)
private String getAccessToken() {
//TODO 获取token的逻辑
return "access_token";
}
//2、相同的参数校验同样的也在抽象类中自己实现(不需要子类参与)
private void identicalParamsCheck(Map<String, Object> params) {
//两种相同的参数有:touser toparty totag msgtype agentid textcard enable_id_trans enable_duplicate_check duplicate_check_interval
//校验存在且不能为空,以下以touser为例
Object touser = params.get("touser");
//ExceptionUtils,如果第一个参数为true,则抛出一个异常,异常的信息为第二个参数的值
ExceptionUtils.isTrue(Objects.isNull(touser), "touser不存在或其值为null!");
ExceptionUtils.isTrue(StringUtils.isBlank(touser + ""), "touser不能为空!");
//TODO 其他的参数校验......
}
//2.1、子类来实现不同的参数校验
public abstract void diffParamsCheck(Map<String, Object> params);
//3、构造参数
public abstract CommonVo createVo(Map<String, Object> params);
//4、发送消息到企业微信中(不需要子类参与)
private String sendMessage(CommonVo paramVo, String accessToken, String url) {
url = url + "?access_token=" + accessToken;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/json; charset=UTF-8"));
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<>(JSON.toJSONString(paramVo), headers);
//返回String类型的json给调用方,由调用方自行转成自定义的实体类
return restTemplate.postForObject(url, formEntity, String.class);
}
//5、对结果进行处理响应,由子类来具体实现
public abstract String responseResult(String resultStr);
}
五、模板已经好了,现在由子类实现不同的逻辑
在上面的模板中,需要子类来实现的接口主要有diffParamsCheck(子类来实现不同的参数校验)、createVo(构造参数)、responseResult(对结果进行处理响应)
- 文本消息的子类
public class TextQywxSendMessage extends AbsQywxSendMessage {
//实现抽象方法,校验不同的参数
public void diffParamsCheck(Map<String, Object> params) {
Object safe = params.get("safe");
//ExceptionUtils,如果第一个参数为true,则抛出一个异常,异常的信息为第二个参数的值
ExceptionUtils.isTrue(Objects.isNull(safe), "safe不存在或其值为null!");
ExceptionUtils.isTrue(StringUtils.isBlank(safe + ""), "safe不能为空!");
Map<String, Object> textMap = (Map<String, Object>)params.get("text");
Object content = textMap.get("content");
ExceptionUtils.isTrue(Objects.isNull(content), "content不存在或其值为null!");
ExceptionUtils.isTrue(StringUtils.isBlank(content + ""), "content不能为空!");
}
//构造不同的参数VO
public CommonVo createVo(Map<String, Object> params) {
//其实构造Vo时,这些共同的属性设置也可以抽出来放到抽象类中,此处不展开
TextVo textVo = new TextVo();
textVo.setToUser(params.get("touser") + "");
......
TextMessage m = new TextMessage();
m.setContent("XXX");
textVo.setText(m);
return textVo;
}
//处理结果
public String responseResult(String resultStr){
//TODO 。。。
return "文本消息推送成功啦!";
}
}
- 文本卡片消息的子类
public class TextCardQywxSendMessage extends AbsQywxSendMessage {
//实现抽象方法,校验不同的参数
public void diffParamsCheck(Map<String, Object> params) {
Map<String, Object> textMap = (Map<String, Object>)params.get("text");
Object title = textMap.get("title");
ExceptionUtils.isTrue(Objects.isNull(title), "title不存在或其值为null!");
ExceptionUtils.isTrue(StringUtils.isBlank(title + ""), "title不能为空!");
//TODO 同样的校验description、url、btntxt......
}
//构造不同的参数VO
public CommonVo createVo(Map<String, Object> params) {
TextCardVo textCardVo = new TextCardVo();
textCardVo.setToUser(params.get("touser") + "");
......
TextCardMessage m = new TextCardMessage();
m.setTitle("XXX");
m.setUrl("XXX");
......
textVo.setText(m);
return textCardVo;
}
//处理结果
public String responseResult(String resultStr){
//TODO 。。。
return "文本卡片消息推送成功啦!";
}
}
六、调用方调用
定义一个维护类
public class SendQywxMessageService {
private static final Map<String, AbsQywxSendMessage> messageMap = new HashMap<>(16);
static {
messageMap.put(MsgEnum.TEXT_MSG.getMsgKey(), new TextQywxSendMessage());
messageMap.put(MsgEnum.TEXT_CARD_MSG.getMsgKey(), new TextCardQywxSendMessage());
}
private AbsQywxSendMessage getSendMessageBean(@NonNull String msgType) {
return messageMap.get(msgType);
}
final public String sendMessage(Map<String, Object> map) {
//msgType定义为指定的发送消息的方式
AbsQywxSendMessage msg = getSendMessageBean(map.get("msgType") + "");
if (Objects.nonNull(msg)) {
return msg.sendQywxMessage(map);
}
return null;
}
}
//枚举类
public enum MsgEnum {
TEXT_MSG("textMessage", "文本消息"),
TEXT_CARD_MSG("textCardMessage", "文本卡片消息");
private final String msgKey;
private final String desc;
MsgEnum(String msgKey, String desc) {
this.msgKey = msgKey;
this.desc = desc;
}
public String getMsgKey() {
return msgKey;
}
public String getDesc() {
return desc;
}
}
调用
public class SendMessageServiceImpl {
@Autowired
private SendQywxMessageService msgService;
//此map中除了有发送消息的参数处,还应该有一个msgType,定义为指定的发送消息的方式(这个是为了方便维护)
public String sendMsg(Map<String, Object> map) {
//TODO 业务
return msgService.sendMessage(map);
}
}
总结
从中可以看出,即使以后增加了其他的新的消息发送方式,我们也可以不用改变业务处理类SendMessageServiceImpl和其他的已经有了的消息发送方式的逻辑代码,只需要增加一个子类来实现新的发送方式的参数校验、构造参数、结果处理。符合我们一起提倡的开闭原则(Open-Closed Principle,OCP)即对扩展开放, 对修改关闭。