流水账日记
小时候写日记很可能出现这样的:
今天早上我7点钟起床,起床后刷牙、洗脸,然后吃早餐,吃了早餐去上学。去到学校,第一节是语文课,语文课下课后跟小明一起玩,然后上数学课,数学课下课后也是跟小明一起玩,然后上体育课,上完体育课我们吃午餐、午睡。。。。(中间省略一千字)下午4点半下课后我回家,回到家我先吃了个雪糕,然后开始写语文作业,写完语文作业写数学作业。。。(再次省略一千字)。。。今天我度过了快乐、充实的一天。
这样的日记如白开水般平淡无味地描述了一天的经过,读完之后让人一脸茫然,不知重点是什么、要关注什么。
一个开发任务
现在有个开发任务:从数据库的 t_smsinfo
表取从未发送或发送失败3次以下的短信进行发送。如果发送成功了就标记为成功不再再次发送;如果发送失败了就记录失败的原因、增加失败次数,失败次数达到3次的就不再重试。
t_smsinfo
表有下列字段:
- id:唯一标识符;
- mobile:目标手机号;
- text:短信内容;
- send_by_comp:发送短信的公司,目标是支持以多家公司的名义发送;
- msg_type:消息类型,因为有不同的业务场景,希望做区分;
- status 表示发送状态,它的取值为: ‘W’ 表示未发送,’F’ 表示发送失败,’G’ 表示发送中。
- sendout_time:最近一次发送时间。
- fail_reason:最近一次发送失败的原因,希望分析失败原因;
- fail_times 表示失败的次数,默认是 0 。
流水账式开发
这个小任务怎么难得住强大的开发人员,开发工程师马上在大脑里进行开发:
- 要把所有满足条件短信从数据库里取出来;得到一批要发送的短信;
- 因为有多条短信,需要一个循环,把发送的处理过程放到循环里。
- 在循环里,把短信信息按供应商的接口要求拼装报文,调用供应商的接口发送短信,再解析响应报文。
- 根据响应的结果,如果成功了就标记为发送成功;如果失败就累加失败次数、记录失败原因。
这是一个很自然的思考过程。
开发人员在键盘上敲了 10 秒(毕竟有青轴机械键盘加持),给出了下面的代码:
public void sendSms() throws SQLException, DocumentException {
ResultSet resultSet = null;
PreparedStatement pstmt = null;
Connection connection = DbUtils.getConnection();
try {
String sql = "select id, mobile, text, send_by_comp, msg_type, status, sendout_time, fail_times"
+ " from t_smsinfo where (status = 'W' or status = 'F' and fail_times < 3)";
pstmt = connection.prepareStatement(sql);
resultSet = pstmt.executeQuery();
while (resultSet.next()) {
long id = resultSet.getLong("id");
String mobile = resultSet.getString("mobile");
String text = resultSet.getString("text");
String send_by_comp = resultSet.getString("send_by_comp");
String msg_type = resultSet.getString("msg_type");
String status = resultSet.getString("status");
int fail_times = resultSet.getInt("fail_times");
Map<String, String> smsParamMap = new HashMap<String, String>();
smsParamMap.put("mobile", mobile);
smsParamMap.put("text", text);
smsParamMap.put("send_by_comp", send_by_comp);
smsParamMap.put("msg_type", msg_type);
String xml = sendSms(smsParamMap);
Element rootElement = DocumentHelper.parseText(xml).getRootElement();
String retCode = rootElement.elementText("retCode");
if ("0".equals(retCode)) {
String updateSuccessSql = "update t_smsinfo set status = 'S' where id = ?";
PreparedStatement successPstmt = connection.prepareStatement(updateSuccessSql);
try {
successPstmt.setLong(1, id);
successPstmt.executeUpdate();
} finally {
DbUtils.close(successPstmt);
}
} else {
String retMsg = rootElement.elementText("retMsg");
String update2failSql = "update t_smsinfo set status = 'F', fail_reason = ?, fail_times = fail_times + 1 where id = ?";
PreparedStatement update2failPstmt = connection.prepareStatement(update2failSql);
try {
update2failPstmt.setString(1, retMsg);
update2failPstmt.setLong(2, id);
update2failPstmt.executeUpdate();
} finally {
DbUtils.close(update2failPstmt);
}
}
}
} finally {
DbUtils.close(resultSet, pstmt, connection);
}
}
程序上线后,短信发送再没出现延迟的现象,运行非常稳定,从没因为这段程序导致宕机。
有啥问题吗?
上面的代码有啥问题吗?在生产环境可从没出过问题哦,非常棒的。
但是,如果从逻辑清晰、简洁的角度看,好像有点问题哦。
这段代码的重点的什么?为什么看到的基本都是数据库相关的操作代码或者拼装报文的无聊代码?
“有重点”的编程方法
作为”有重点”的编程方法,肯定是要突出我们代码的重点。怎么才能突出代码的重点?
让我们从需求出发。再次给出这个需求:
从数据库的
t_smsinfo
表取从未发送或发送失败3次以下的短信进行发送。如果发送成功了就标记为成功不再再次发送;如果发送失败了就记录失败的原因、增加失败次数,失败次数达到3次的就不再重试。
level 1
从需求里提炼得到的核心需求就是:取所有需要发送的短信,进行发送。
要发送的短信在那里?怎么确定短信是要发送的?怎么取出来?怎么发送?发送的结果怎么处理?都不管先。
我们可以先写下这样的代码:
public void sendAllSms() {
List<SmsInfo> smsInfoList = getAllNeedToSendSmsInfo();
sendAll(smsInfoList);
}
private void sendAll(List<SmsInfo> smsInfoList) {
// TODO
}
private List<SmsInfo> getAllNeedToSendSmsInfo() {
// TODO
return null;
}
后面的两个方法都是空的,我们稍后实现。作为这个需求的入口,sendAllSms
方法非常清晰地说明了这个需求的目标:获取所有需要发送的短信,发送这些短信。
level 2
我们先关注怎么发送一批短信。要发送一批短信既可以顺序地发送,也可以用多线程并发地发送,但具体的某一条短信该如何发送,先不管。
private void sendAll(List<SmsInfo> smsInfoList) {
for (SmsInfo smsInfo : smsInfoList) {
sendSingleSms(smsInfo);
}
}
private void sendSingleSms(SmsInfo smsInfo) {
// TODO
}
上面的代码也非常简单清晰,一眼就知道是顺序地发送传入的一批短信。
level 3
现在实现发送一条短信的逻辑:我们没法直接把短信发到客户的手机上,需要通过短信供应商的接口进行发送,所以需要调用外部的接口;如果接口调用成功了,需要进行成功的处理;如果失败了,则需要处理失败的情况。至于接口怎么调用?成功时该怎么处理?失败时该怎么处理?都先不考虑。就有了下面的代码:
private void sendSingleSms(SmsInfo smsInfo) {
Result result = callSmsSpi(smsInfo);
if (result.isSuccess()) {
updateOnSuccess(smsInfo);
} else {
updateOnFail(smsInfo, result);
}
}
private Result callSmsSpi(SmsInfo smsInfo) {
// TODO
return null;
}
private void updateOnSuccess(SmsInfo smsInfo) {
// TODO
}
private void updateOnFail(SmsInfo smsInfo, Result result) {
// TODO
}
从 sendSingleSms(SmsInfo smsInfo)
方法,我们可以看到一条短信就那么简单地处理完了。
到目前为止,并没有任何关于数据库操作的代码,也没有拼装特定短信供应商接口请求报文、解析响应报文的代码。所有的代码都是直接体现业务需求的。
折腾那么多,有什么好处呢?
如果我们想用多线程来提升短信的发送速度,只需要修改 void sendAll(List<SmsInfo> smsInfoList)
方法,改完后只需测试这个方法,因为其他的方法都没有修改。
如果我们换了家短信供应商,接口的请求报文、响应报文格式完全不同,但我们只需要修改 Result callSmsSpi(SmsInfo smsInfo)
方法,改完后也只需测试这个方法,因为其他的方法也都没有修改。
总的来说,每个方法都是简单明了,逻辑清晰。代码易于修改、且修改的影响范围可控,这样也就方便维护。
代码的抽象层次
之前在 IBM DW 社区看到这样编程的思路:
- 尽量使同一个方法的抽象保持在同一个层次。(比如上面,在处理一批短信发送任务时,是不会关注一个任务是怎么处理。发送一条短信时也不关注短信的请求报文怎么拼装。就是因为它们的抽象层次不同。)
- 这样会产生很多小的、可复用的方法。
我们的思考过程可以是顺序的,实现过程也可以是顺序的,但顺序写出来后一定要不断提炼代码的抽象层次,以达到简洁、清晰、可维护、可复用。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。