自下向上的编写容易阅读的代码(下)
我在 关于极简编程的思考 中曾提到要编写可阅读的代码。因为代码是编写一次,阅读多次。 阅读者包括代码编写者,以及后来的维护人员。能让阅读代码更轻松,有利于增强项目或者产品的可维护性。
本博客分为上下俩部分,第一部分讲解在代码层次 编写可阅读的代码
这一部分讲解方法,类,以及一些设计上的考虑,这些考虑并不是来自于某些设计原则或者是设计模式,而是基于对象的职责,将在下面会讲述
发现对象
在上半部分,我们讲到一个解析 excel 的例子,在我实际项目里,曾经是这个样子
public void parse(Sheet sheet,StringBuilder error){
User user = readUserInfo(sheet,error);
List<Order> orders = readUserOrderInfo(sheet,error);
UserCredit credit = readUserCreditInfo(sheet,error);
}
之所以提供一个 StringBuilder 参数,是因为需求是如果解析出错,需要显示出错的的位置,项目开发人员因此将错误信息拼接成字符串,最后返回给前端。
如果审查其实现,你会发现该解析方法到处都是类似如下代码
error.append("在"+line+"和”+col+“列错":+"messsage).append("\n");
这两段代码的阅读者困惑之处就是 error 定义不能说明如何处理解析错误,阅读者不得不看清楚具体实现才恍然大悟 -- 原来我的前任用 StringBuilder 是想这么干。另外一个困惑之处就是在解析 excel 的时候,就已经写死了错误输出的样子,如果想更改,就需要改每一处地方 ,我们知道业务的 excel 解析,几百行代码算是少的了。要阅读者几百行代码重构对后来者并非易事。
有什么模式或者设计原则能解决这个吗?
我想说的是,并没有模式和设计原则能解决,开发者缺少的仅仅是发现和归纳对象的能力(设计模式是锦上添花),对于 excel 解析的错误信息,实际上就应该定义一个” 错误信息 “这样的对象。比如
public class ExcelParseError{
public void addError(ParseErrorInfo info ){}
public void addSimpleError(String line,String col,String message ){}
public List<ParseErrorInfo> getMessages(){}
public String toString(){
.....
}
}
因此,excel 解析最后是这个样子
public void parse(Sheet sheet,ExcelParseError error){
User user = readUserInfo(sheet,error);
List<Order> orders = readUserOrderInfo(sheet,error);
UserCredit credit = readUserCreditInfo(sheet,error);
}
处理解析错误的代码则变成如下
error.addSimpleError(line,col,message);
再次发现对象
发现对象是让杂乱代码变得有序的最重要方式,看如下例子:
public Long startWorkflow(String user,long orgId,long taskType,long workflowType,Map<String,String> taskParas){
.....
}
这是一个工作流引擎启动流程的 API,共有 5 个参数。这是我曾经项目的最早定义的 API,后来实际上又扩展了好几个参数,比如工作流支持版本后,又需要增加一个参数是 int workflowVersion。
这 6 个参数实际上代表了启动工作流需要的三类参数,"工作流参与人的描述","工作流本身的描述",还有 "工作流启动的输入参数",因此,这个 API 最终定义成
public Long startWorkflow(Participant p,WorkflowDef workflow,Variable vars){
.....
}
Participant 对应了工作流参与人描述 WorkflowDef 对应了工作流定义 Variable 则对应了工作流参数
这些对象增强了 API 的可扩展性,更为重要的是,他的代码更加容易阅读,无论是调用者,还是 api 本身的实现,"新发现的对象" 让杂乱无章的变量变得有序起来.
对象是在我们编程生活中真实存在的,如果能感知到对象存在,则编程会美好很多,同样,阅读和维护代码也会更加方便。在没有感知对象的情况下妄谈设计模式和和设计原则,就是无源之水。
下一个例子是我的 BeetlSQL 的例子,有一个 SQLLoader 类用来加载 sql 语句,其中有一个片段是 从 markdown 文件加载 sql 语句。最初代码如下(警告,代码有毒,不要阅读,直接跳过)
bf = new BufferedReader(new InputStreamReader(ins));
String temp = null;
StringBuffer sql = null;
String key = null;
while ((temp = bf.readLine()) != null) {
if (temp.startsWith("===")) {// 读取到===号,说明上一行是key,下面是SQL语句
if (!list.isEmpty() && list.size() > 1) {// 如果链表里面有多个,说明是上一句的sql+下一句的key
String tempKey = list.pollLast();// 取出下一句sql的key先存着
sql = new StringBuffer();
key = list.pollFirst();
while (!list.isEmpty()) {// 拼装成一句sql
sql.append(list.pollFirst() + lineSeparator);
}
this.sqlSourceMap.put(modelName + key, new SQLSource(
sql.toString()));// 放入map
list.addLast(tempKey);// 把下一句的key又放进来
}
} else {
list.addLast(temp);
}
}
// 最后一句sql
sql = new StringBuffer();
key = list.pollFirst();
while (!list.isEmpty()) {
sql.append(list.pollFirst());
}
this.sqlSourceMap.put(modelName + key,
new SQLSource(sql.toString()));
这段代码解析 markdown 文件,读取以 === 分割的的 sql 片段,并放到 sqlSourceMap 里。大概格式如下
disableUser
===
* 这是一个更新用户信息的SQL语句
update user set status = 1 where id = #id#
尽管解析代码不算长,且有很多注释,但每次在这里增加一点扩展都极其困难。比如 Markdown 支持 ”“ 符号作为注释语句,那对 "" 代码解析放在个哪个地方?
后来我对这段代码进行重构了,实际上,我是发现我需要一个 MDParser 类来负责这事情 :专门解析 md 文件,MDParser 定义如下(可以阅读了)
public class MDParser {
public MDParser(String modelName,BufferedReader br) throws IOException{
this.modelName = modelName;
this.br = br;
skipHeader();
}
public void skipHeader() throws IOException{
....
}
public SQLSource next() throws IOException{
String sqlId = readSqlId();
if(status==END){
return null;
}
//去掉可能的尾部空格
sqlId = sqlId.trim();
skipComment();
if(status==END){
return null;
}
int sqlLine = this.linNumber;
String sql = readSql();
SQLSource source = new SQLSource(modelName + sqlId,sql);
source.setLine(sqlLine);
return source;
}
}
从这个类可以看到,当读入一个 markdown 文件的时候,首选调用 skipHeader,去掉 md 文件开头无关的文档整体说明
next 方法用来获取每一个 sql 片段说明,先调用 readSqlId 获取 sql 的标示符号,然后 skipComment 方法用来忽略 sql 注释,最后 readSql 用来读取 sql 语句内容。
MDParser 使得 SQLLoader 更加精简和容易阅读,也使得关于 Markkdown 解析更加容易维护。
警惕 String,数组,和 Map
当程序中出现 String 参数,数组参数,以及 Map 的时候,已经在提醒我们是遗漏了系统的对象。 这三个类型参数当然非常灵活,能容纳下任何数据结构,但有可能遗漏了系统隐含的对象。尤其是数组和 Map。我在上一章提到过的例子
Object[] rets = call();
boolean success = (Boolean)rets[0];
String msg = (String)rets[1];
就没有下面的定义好
CallResult rets = call();
boolean success = rets.isSuccess();
String msg = rets.getMessage();
如果 CallResult 包含了某个返回值,那么,将 CallResult 定义成泛型就更加容易阅读,比如返回 CallResult
public CallResult getUser(){
}
这肯定没有如下代码更容易阅读,让后来者放心去使用
public CallResult<User> getUser(){
}
这一篇我提到的每一个好的例子都相对于差的的例子,都会多写数行代码,甚至还得写一个类 ,但毫无疑问,阅读更加容易,维护更加方便了。
总结 如果只能用一个设计模式
我做过大量业务系统,电信的也好,金融也好,互联网项目,还是创业项目,也写过不少工具,能公开的比如有 Beetl,BeetlSQL,XLSUnit。这么多工程项目,如果让我说最重要的设计技巧是什么,或者只能用一个设计技巧,我会毫不犹豫的说,是” 职责模式 “
职责模式 描述了如何发现和划分对象职责,就好比一个班,应该有班长,各科学习委员,小组长。再比如,新闻里经常出现某某重大事故,就会成立了某某专项委员会。在比如,为了保证项目质量,我们有测试组,为了监控项目,我们有 PMO。我们周围生活,一直都按照人尽其职,职责划分这个原则来运作。 如果划分错了,非常影响我们的生活,比如让我去监控项目进度:(。
职责模式,可以搜索 GRASP
这是一个很少被人提起的模式,我个人推荐去学习体会。
卢正雨在《绝世高手》里,从普通人最后变成了食神,如果你看了这个电影,就知道,他成为食神是因为对食物的细腻感知。我想在《自下向上的编写容易阅读的代码方法》这一部分的总结是 ” 感知对象的存在 “,你也能写出容易阅读的代码,甚至成为高手。