我在 关于极简编程的思考 中曾提到要编写可阅读的代码。因为代码是编写一次,阅读多次。 阅读者包括代码编写者,以及后来的维护人员。能让阅读代码更轻松,有利于增强项目或者产品的可维护性。
本博客分为上下俩部分,第一部分讲解在代码层次 编写可阅读的代码,参考地址是https://my.oschina.net/xiandafu/blog/1509679
这一部分讲解方法,类,以及一些设计上的考虑,这些考虑并不是来自于某些设计原则或者是设计模式,而是基于对象的职责,将在下面会讲述
发现对象
在上半部分,我们讲到一个解析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
这是一个很少被人提起的模式,我个人推荐去学习体会。
卢正雨在《绝世高手》里,从普通人最后变成了食神,如果你看了这个电影,就知道,他成为食神是因为对食物的细腻感知。我想在《自下向上的编写容易阅读的代码方法》这一部分的总结是 ”感知对象的存在“,你也能写出容易阅读的代码,甚至成为高手。