自下向上的编写容易阅读的代码方法(下 )

原创
2017/08/18 17:01
阅读数 2.9K

我在 关于极简编程的思考 中曾提到要编写可阅读的代码。因为代码是编写一次,阅读多次。 阅读者包括代码编写者,以及后来的维护人员。能让阅读代码更轻松,有利于增强项目或者产品的可维护性。

本博客分为上下俩部分,第一部分讲解在代码层次 编写可阅读的代码,参考地址是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

这是一个很少被人提起的模式,我个人推荐去学习体会。

卢正雨在《绝世高手》里,从普通人最后变成了食神,如果你看了这个电影,就知道,他成为食神是因为对食物的细腻感知。我想在《自下向上的编写容易阅读的代码方法》这一部分的总结是 ”感知对象的存在“,你也能写出容易阅读的代码,甚至成为高手。

展开阅读全文
加载中
点击加入讨论🔥(5) 发布并加入讨论🔥
5 评论
16 收藏
7
分享
返回顶部
顶部