我在 關於極簡編程的思考 中曾提到要編寫可閱讀的代碼。由於代碼是編寫一次,閱讀屢次。 閱讀者包括代碼編寫者,以及後來的維護人員。能讓閱讀代碼更輕鬆,有利於加強項目或者產品的可維護性。前端
本博客分爲上下倆部分,第一部分講解在代碼層次 編寫可閱讀的代碼,參考地址是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
這是一個不多被人提起的模式,我我的推薦去學習體會。
盧正雨在《絕世高手》裏,從普通人最後變成了食神,若是你看了這個電影,就知道,他成爲食神是由於對食物的細膩感知。我想在《自下向上的編寫容易閱讀的代碼方法》這一部分的總結是 」感知對象的存在「,你也能寫出容易閱讀的代碼,甚至成爲高手。