我在 关于极简编程的思考 中曾提到要编写可阅读的代码。由于代码是编写一次,阅读屡次。 阅读者包括代码编写者,以及后来的维护人员。能让阅读代码更轻松,有利于加强项目或者产品的可维护性。前端
本博客分为上下俩部分,第一部分讲解在代码层次 编写可阅读的代码,参考地址是https://my.oschina.net/xiandafu/blog/1509679java
这一部分讲解方法,类,以及一些设计上的考虑,这些考虑并非来自于某些设计原则或者是设计模式,而是基于对象的职责,将在下面会讲述sql
在上半部分,咱们讲到一个解析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 参数,是由于需求是若是解析出错,须要显示出错的的位置,项目开发人员所以将错误信息拼接成字符串,最后返回给前端。设计模式
若是审查其实现,你会发现该解析方法处处都是相似以下代码api
error.append("在"+line+"和”+col+“列错":+"messsage).append("\n");
这两段代码的阅读者困惑之处就是error定义不能说明如何处理解析错误,阅读者不得不看清楚具体实现才恍然大悟--原来个人前任用StringBuilder是想这么干。另一个困惑之处就是在解析excel的时候,就已经写死了错误输出的样子,若是想更改,就须要改每一处地方 ,咱们知道业务的excel解析,几百行代码算是少的了。要阅读者几百行代码重构对后来者并不是易事。数组
有什么模式或者设计原则能解决这个吗?markdown
我想说的是,并无模式和设计原则能解决,开发者缺乏的仅仅是发现和概括对象的能力(设计模式是锦上添花),对于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解析最后是这个样子app
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的时候,已经在提醒咱们是遗漏了系统的对象。 这三个类型参数固然很是灵活,能容纳下任何数据结构,但有可能遗漏了系统隐含的对象。尤为是数组和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
这是一个不多被人提起的模式,我我的推荐去学习体会。
卢正雨在《绝世高手》里,从普通人最后变成了食神,若是你看了这个电影,就知道,他成为食神是由于对食物的细腻感知。我想在《自下向上的编写容易阅读的代码方法》这一部分的总结是 ”感知对象的存在“,你也能写出容易阅读的代码,甚至成为高手。