驗幻空越重,命循頻異長。依輪線日簡,接偶正分壯。言歡空月蟲,明勛品宜昌。依倫先日賤,潔偶正粉妝。 ...
賈言 | |
驗幻空越重, 命循頻異長。 依輪線日簡, 接偶正分壯。 |
言歡空月蟲, 明勛品宜昌。 依倫先日賤, 潔偶正粉妝。 |
賈言
架構師說, 用20個字描述代碼評審的內容, 自省也省人。由於是一字一含義, 不連貫, 為了增強趣味性, 每句都增加對應的歪解。只是對常見評審的描述, 不盡之處,歡迎補充!
驗幻空越重 -- 言歡空月蟲
驗:公共方法都要做參數的校驗, 參數校驗不通過明確拋出異常或對應響應碼。
- java bean驗證已經是一個很古老的技術了, 會避免我們很多問題, 可參考:
http://beanvalidation.org/ http://www.infoq.com/cn/news/2010/03/javaee6-validation https://www.sitepoint.com/using-java-bean-validation-method-parameters-return-values/ - 在介面中也明確使用驗證註解修飾參數和返回值, 作為一種協議要求調用方按驗證註解約束傳參, 返回值驗證註解約束提供方按註解要求返回參數
幻:在代碼中要杜絕幻數,幻數可定義為枚舉或常量以增強其可讀性
空:要時刻警惕空指針異常
- 常見的 a.equals(b) 要把常量放到左側
- aInteger == 10 如果 aInteger 為空時會拋出空指針異常
- 不確認返回集合是否可為空時要做非空判斷, 再做for迴圈
- 使用空對象模式, 約定返回空集合, 而非null
- 使用StringUtils判斷字元串非空
越:如果方法傳入數組下標作為參數,要在一開始就做下標越界的校驗,避免下標越界異常
重:不要寫重覆代碼,重覆代碼要使用重構工具提取重構
命循頻異長 -明勛品宜昌
命:包/類/方法/欄位/變數/常量的命名要遵循規範,要名副其實,這不但可以增加可讀性,還可以在起名的過程中引導我們思考方法/變數/類的職責是否合適
有意義很重要, 典型無意義命名:
public static final Integer CODE_39120 = 39120;
public static final String MESSAGE_39120 = "[包裹]與[庫房號]不一致,確定裝箱?";
public static final Integer CODE_39121 = 39121;
public static final String MESSAGE_39121 = "[包裹]與[箱號]的承運類型不一致,確定裝箱?";
Rule rule1 = request.getRuleMap().get("1050");
CODE_39120這個名字和幻數沒多大區別。
循:不要在迴圈中調用服務,不要在迴圈中做資料庫等跨網路操作
頻:寫每一個方法時都要知道這個方法的調用頻率,一天多少,一分多少,一秒多少,峰值可能達到多少,調用頻率高的一定要考慮性能指標,考慮是否會打垮資料庫,是否會擊穿緩存
異:異常處理是程式員最基本的素質,不要處處捕獲異常,對於捕獲了只寫日誌,沒有任何處理的catch要問一問自己,這樣吃掉異常,是否合理
下麵是一個反例, 在導出文件的controller方法中做了兩層的try...catch, 在catch塊中記錄日誌後什麼都沒做, 這樣用戶看不到真正想要的內容, 研發也只有看日誌才能發現錯誤, 而“看日誌”, 通常只有業務方反饋問題時才會看, 就會導致研發人員發現錯誤會比現場人員還會晚。
@RequestMapping(value = "/export")
public void export(CityRelationDomain condition, HttpServletResponse response) {
ZipOutputStream zos = null;
BufferedWriter bufferedWriter = null;
try {
condition.setStart(0);
condition.setSize(MAX_EXPORT_LINES);
List list = cityRelationService.getOrdersByCondition(condition);
response.setCharacterEncoding("GBK");
response.setContentType("multipart/form-data");
response.setHeader("Content-Disposition", "attachment;fileName=export.zip");
zos = new ZipOutputStream(response.getOutputStream());
bufferedWriter = new BufferedWriter(new OutputStreamWriter(zos, "GBK"));
bufferedWriter.write("訂單類型編碼,始發城市-省,始發城市-市,目的城市-省,目的城市-市");
ZipEntry zipEntry = new ZipEntry("export.csv");
zos.putNextEntry(zipEntry);
for (CityRelationDomain domain : list) {
try {
bufferedWriter.newLine();
bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getOrderCode()));
bufferedWriter.write(',');
bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getProvinceNameFrom()));
bufferedWriter.write(',');
bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getCityNameFrom()));
bufferedWriter.write(',');
bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getProvinceNameTo()));
bufferedWriter.write(',');
bufferedWriter.write(CSVExportUtil.trans2CSV(domain.getCityNameTo()));
} catch (Exception e) {
e.printStackTrace();
}
}
bufferedWriter.newLine();
bufferedWriter.flush();
zos.closeEntry();
bufferedWriter.close();
} catch (Exception e) {
e.printStackTrace();
logger.error("導出CSV文件異常");
} finally {
try {
if (zos != null) {
zos.close();
}
if (bufferedWriter != null) {
bufferedWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
長:如果一行代碼過長,要分解開來;如果一個方法過長,要重構方法;如果一個類過長要考慮拆分類
依輪線日簡 -依倫先日賤
依:如果調用了外部依賴,一定要搞清楚這個外部依賴可以提供的性能指標,最好約定SLA
輪:不要重覆造輪子,如果已經有成熟類庫實現了類似功能,要優先使用成熟類庫的方法,這是因為成熟類庫中的方法都經過很多人的測試驗證,通常情況下我們自己實現的質量最大等同於成熟類庫的質量。
線:要註意我們的jsf服務, web應用,消費消息的worker都是多線程環境,要註意線程安全問題,最典型的HashMap, SimpleDateFormat, ArrayList是非線程安全的,另外如果使用Spring自動掃描服務,那麼這個服務預設是單例,其內部成員是多個線程共用的,如果直接用成員變數是有線程不安全的。
兩個典型的錯誤代碼片段:
1)無視SimpleDateFormat非線程安全
@Service
public class AService {
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public void doSomething() {
//use FORMAT
}
}
2)使用Service成員變數
@Service
public class BService {
private Pojo b;
public void doB() {
b = getB();
process(b);
}
}
日:列印日誌和設定合理的日誌級別,如有必要要添加if條件限定是否列印日誌,在日誌中使用JSON序列化,生成長字元串的toString()都要做if限定列印,否則配置的日誌級別沒達到,也會做大量字元串拼接,占用很多gc年輕代記憶體. 另外一定要通過log4j列印日誌而不是直接把日誌列印到控制台。
典型錯誤示例:
@Service
public class FooService {
private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);
public void doFooThing(Foo foo) {
LOGGER.debug("get parameter foo {}", JSONObject.toString(foo));
try {/*do something*/} catch (Exception ex) {ex.printStackTrace();}
}
}
簡: 儘可能保持整體設計的簡潔, 方法實現的簡潔, 要根據情況使用記憶體緩存, redis 緩存, jmq 非同步處理。 這裡的簡需要把握好分寸。
接偶正分壯 - 潔偶正粉妝
接:介面是用來隔離變化的,如果一個業務有幾種不同的形態,但都有相同的處理,那麼可以定義介面來隔離業務形態的不同,在服務調用處,通過業務類型欄位來獲得不同的服務類。 而不要實現一個類,然後在類的各個方法中都根據業務類型做if else或更複雜的各種判斷。
典型示例:
做法1:
public interface BarService { void doBarThing(Bar b);
void doBarFatherThing(Bar b);
}
public class BarServiceImpl implement BarService{
public void doBarThing(Bar b) {
if (b.getType() == BarType.A) {
//do some logic
} else (b.getType() == BarType.B) {
//do some B type logic
}
//do other doBarThing logic
}
public void doBarFatherThing(Bar b) {
if (b.getType() == BarType.A) {
//do some logic
} else (b.getType() == BarType.B) {
//do some B type logic
}
//do other doBarFatherThing logic
}
}
做法 2 :
public interface BarService {
void doBarThing(Bar b);
void doBarFatherThing(Bar b);
}
public class BarServiceFactory {
public BarService getBarService(BarType type) {
// get bar service logic
}
}
//如果有公共邏輯就定義, 沒有就不定義
public class BaseBarService implement BarService {
public void doBarThing(Bar b) {
//do other doBarThing logic
}
public void doBarFatherThing(Bar b) {
//do other doBarFatherThing logic
}
}
public class TypeABarService extends BaseBarService implement BarService {
public void doBarThing(Bar b) {
// doATypeThing
super.doBarThing(b);
}
public void doBarFatherThing(Bar b) {
// do bar type A service
super.doBarFatherThing(b); //如果需要就調用, 不需要就不調用父類
}
}
做法2的好處是將不同類型的邏輯解耦,各自發展,不會相互影響,如果添加類型也不必影響現有類型邏輯。
偶:認識系統之間的耦合關係,通過同步數據來做兩個系統之間的交互是一種很強的耦合關係,會使數據接收方依賴於數據發送方的資料庫定義,如果發送方想改數據結構,必須要求下游接收方一起修改;通過介面調用是一種常見的系統耦合關係,介面的提供方要保證介面的可用性,介面的調用方要考慮介面不可用時的應對方案; mq消息是一種解耦的方法,兩個系統不存在實時的耦合關係。但是mq解耦的方式不能濫用,在同一系統內不宜過多使用mq消息來做非同步,要儘可能保證介面的性
能,而不是通過mq防止出問題後重新消費。
正:模塊之間依賴關係要正向依賴,不能讓底層模塊依賴於上層模塊;不能讓數據層依賴於服務層也不能讓服務層依賴於UI層;也不能在模塊之間形成迴圈依賴關係。
分:分而治之,複雜的問題要分解成幾個相對簡單的問題來解決,首先要分析出核心問題,然後分析出核心的入參是什麼,結果是什麼,入參通過幾步變化可以得出結果。
壯:時刻註意程式的健壯性,從下麵三個方面實踐提升健壯性:
1)契約,在設計介面時定義好協議參數,併在實現時第一時間校驗參數,如果參數有問題,直接返回給調用方; 如果出現異常情況, 也按異常情況約定應對策略
2) 考慮各種邊界條件的輸出, 比如運單號查詢服務, 要考慮用戶輸入錯誤運單時怎麼返回, 有邊界的查詢條件, 如果用戶查詢條件超過邊界了, 應該返回什麼
3 )為失敗做設計,如果出問題了有降級應對方案。