京東資深架構(gòu)師代碼評(píng)審?fù)嵩?shī)
賈言
架構(gòu)師說, 用20個(gè)字描述代碼評(píng)審的內(nèi)容, 自省也省人。由于是一字一含義, 不連貫, 為了增強(qiáng)趣味性, 每句都增加對(duì)應(yīng)的歪解。只是對(duì)常見評(píng)審的描述, 不盡之處,歡迎補(bǔ)充!
驗(yàn)幻空越重 -- 言歡空月蟲
驗(yàn): 公共方法都要做參數(shù)的校驗(yàn),參數(shù)校驗(yàn)不通過明確拋出異?;?qū)?yīng)響應(yīng)碼。
- java bean驗(yàn)證已經(jīng)是一個(gè)很古老的技術(shù)了,會(huì)避免我們很多問題,可參考: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/
- 在接口中也明確使用驗(yàn)證注解修飾參數(shù)和返回值, 作為一種協(xié)議要求調(diào)用方按驗(yàn)證注解約束傳參, 返回值驗(yàn)證注解約束提供方按注解要求返回參數(shù)
幻: 在代碼中要杜絕幻數(shù),幻數(shù)可定義為枚舉或常量以增強(qiáng)其可讀性
空: 要時(shí)刻警惕空指針異常
- 常見的 a.equals(b) 要把常量放到左側(cè)
- aInteger == 10 如果 aInteger 為空時(shí)會(huì)拋出空指針異常
- 不確認(rèn)返回集合是否可為空時(shí)要做非空判斷, 再做for循環(huán)
- 使用空對(duì)象模式, 約定返回空集合, 而非null
- 使用StringUtils判斷字符串非空
越: 如果方法傳入數(shù)組下標(biāo)作為參數(shù),要在一開始就做下標(biāo)越界的校驗(yàn),避免下標(biāo)越界異常
重: 不要寫重復(fù)代碼,重復(fù)代碼要使用重構(gòu)工具提取重構(gòu)
命循頻異長(zhǎng) - 明勛品宜昌
命: 包 / 類 / 方法 / 字段 / 變量 / 常量的命名要遵循規(guī)范,要名副其實(shí), 這不但可以增加可讀性,還可以在起名的過程中引導(dǎo)我們思考方法 / 變量 / 類的職責(zé)是否合適
有意義很重要, 典型無(wú)意義命名:
- public static final Integer CODE_39120 = 39120;
- public static final String MESSAGE_39120 = "[包裹]與[庫(kù)房號(hào)]不一致,確定裝箱?";
- public static final Integer CODE_39121 = 39121;
- public static final String MESSAGE_39121 = "[包裹]與[箱號(hào)]的承運(yùn)類型不一致,確定裝箱?";
- Rule rule1 = request.getRuleMap().get("1050");
CODE_39120這個(gè)名字和幻數(shù)沒多大區(qū)別。
循: 不要在循環(huán)中調(diào)用服務(wù),不要在循環(huán)中做數(shù)據(jù)庫(kù)等跨網(wǎng)絡(luò)操作
頻: 寫每一個(gè)方法時(shí)都要知道這個(gè)方法的調(diào)用頻率,一天多少,一分多少,一秒多少,峰值可能達(dá)到多少,調(diào)用頻率高的一定要考慮性能指標(biāo), 考慮是否會(huì)打垮數(shù)據(jù)庫(kù),是否會(huì)擊穿緩存
異: 異常處理是程序員最基本的素質(zhì),不要處處捕獲異常,對(duì)于捕獲了只寫日志,沒有任何處理的 catch 要問一問自己,這樣吃掉異常,是否合理
下面是一個(gè)反例, 在導(dǎo)出文件的controller方法中做了兩層的try...catch, 在catch塊中記錄日志后什么都沒做, 這樣用戶看不到真正想要的內(nèi)容, 研發(fā)也只有看日志才能發(fā)現(xiàn)錯(cuò)誤, 而“看日志”, 通常只有業(yè)務(wù)方反饋問題時(shí)才會(huì)看, 就會(huì)導(dǎo)致研發(fā)人員發(fā)現(xiàn)錯(cuò)誤會(huì)比現(xiàn)場(chǎng)人員還會(huì)晚。
- @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<CityRelationDomain> 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("訂單類型編碼,始發(fā)城市-省,始發(fā)城市-市,目的城市-省,目的城市-市");
- 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("導(dǎo)出CSV文件異常");
- } finally {
- try {
- if (zos != null) {
- zos.close();
- }
- if (bufferedWriter != null) {
- bufferedWriter.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
長(zhǎng): 如果一行代碼過長(zhǎng),要分解開來;如果一個(gè)方法過長(zhǎng),要重構(gòu)方法;如果一個(gè)類過長(zhǎng)要考慮拆分類
依輪線日簡(jiǎn) - 依倫先日賤
依: 如果調(diào)用了外部依賴, 一定要搞清楚這個(gè)外部依賴可以提供的性能指標(biāo),***約定 SLA
輪: 不要重復(fù)造輪子,如果已經(jīng)有成熟類庫(kù)實(shí)現(xiàn)了類似功能,要優(yōu)先使用成熟類庫(kù)的方法,這是因?yàn)槌墒祛悗?kù)中的方法都經(jīng)過很多人的測(cè)試驗(yàn)證,通常情況下我們自己實(shí)現(xiàn)的質(zhì)量***等同于成熟類庫(kù)的質(zhì)量。
線: 要注意我們的 jsf 服務(wù),web 應(yīng)用,消費(fèi)消息的 worker 都是多線程環(huán)境,要注意線程安全問題,最典型的 HashMap,SimpleDateFormat ,ArrayList 是非線程安全的,另外如果使用 Spring 自動(dòng)掃描服務(wù),那么這個(gè)服務(wù)默認(rèn)是單例,其內(nèi)部成員是多個(gè)線程共享的,如果直接用成員變量是有線程不安全的。
兩個(gè)典型的錯(cuò)誤代碼片段:
- 無(wú)視 SimpleDateFormat 非線程安全
- @Service
- public class AService {
- private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
- public void doSomething() {
- //use FORMAT
- }
- }
- @Service
- public class BService {
- private Pojo b;
- public void doB() {
- b = getB();
- process(b);
- }
- }
日: 打印日志和設(shè)定合理的日志級(jí)別,如有必要要添加 if 條件限定是否打印日志,在日志中使用 JSON 序列化,生成長(zhǎng)字符串的 toString() 都要做 if 限定打印,否則配置的日志級(jí)別沒達(dá)到,也會(huì)做大量字符串拼接,占用很多 gc 年輕代內(nèi)存. 另外一定要通過log4j打印日志而不是直接把日志打印到控制臺(tái)。
典型錯(cuò)誤示例:
- @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();}
- }
- }
簡(jiǎn): 盡可能保持整體設(shè)計(jì)的簡(jiǎn)潔,方法實(shí)現(xiàn)的簡(jiǎn)潔,要根據(jù)情況使用內(nèi)存緩存,redis 緩存,jmq 異步處理。這里的簡(jiǎn)需要把握好分寸。
接偶正分壯 - 潔偶正粉妝
接: 接口是用來隔離變化的,如果一個(gè)業(yè)務(wù)有幾種不同的形態(tài),但都有相同的處理,那么可以定義接口來隔離業(yè)務(wù)形態(tài)的不同,在服務(wù)調(diào)用處,通過業(yè)務(wù)類型字段來獲得不同的服務(wù)類。而不要實(shí)現(xiàn)一個(gè)類,然后在類的各個(gè)方法中都根據(jù)業(yè)務(wù)類型做 if else 或更復(fù)雜的各種判斷。
典型示例:
- 做法 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
- }
- }
- 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); //如果需要就調(diào)用, 不需要就不調(diào)用父類
- }
- }
做法 2 的好處是將不同類型的邏輯解耦,各自發(fā)展,不會(huì)相互影響,如果添加類型也不必影響現(xiàn)有類型邏輯。
偶: 認(rèn)識(shí)系統(tǒng)之間的耦合關(guān)系,通過同步數(shù)據(jù)來做兩個(gè)系統(tǒng)之間的交互是一種很強(qiáng)的耦合關(guān)系,會(huì)使數(shù)據(jù)接收方依賴于數(shù)據(jù)發(fā)送方的數(shù)據(jù)庫(kù)定義,如果發(fā)送方想改數(shù)據(jù)結(jié)構(gòu),必須要求下游接收方一起修改;通過接口調(diào)用是一種常見的系統(tǒng)耦合關(guān)系,接口的提供方要保證接口的可用性,接口的調(diào)用方要考慮接口不可用時(shí)的應(yīng)對(duì)方案; mq 消息是一種解耦的方法,兩個(gè)系統(tǒng)不存在實(shí)時(shí)的耦合關(guān)系。但是 mq 解耦的方式不能濫用,在同一系統(tǒng)內(nèi)不宜過多使用 mq 消息來做異步,要盡可能保證接口的性
能, 而不是通過 mq 防止出問題后重新消費(fèi)。
正: 模塊之間依賴關(guān)系要正向依賴,不能讓底層模塊依賴于上層模塊;不能讓數(shù)據(jù)層依賴于服務(wù)層也不能讓服務(wù)層依賴于 UI 層; 也不能在模塊之間形成循環(huán)依賴關(guān)系。
分: 分而治之,復(fù)雜的問題要分解成幾個(gè)相對(duì)簡(jiǎn)單的問題來解決,首先要分析出核心問題, 然后分析出核心的入?yún)⑹鞘裁?,結(jié)果是什么,入?yún)⑼ㄟ^幾步變化可以得出結(jié)果。
壯: 時(shí)刻注意程序的健壯性,從兩個(gè)方面實(shí)踐提升健壯性:
- 契約,在設(shè)計(jì)接口時(shí)定義好協(xié)議參數(shù),并在實(shí)現(xiàn)時(shí)***時(shí)間校驗(yàn)參數(shù),如果參數(shù)有問題,直接返回給調(diào)用方; 如果出現(xiàn)異常情況, 也按異常情況約定應(yīng)對(duì)策略
- 考慮各種邊界條件的輸出,比如運(yùn)單號(hào)查詢服務(wù), 要考慮用戶輸入錯(cuò)誤運(yùn)單時(shí)怎么返回,有邊界的查詢條件,如果用戶查詢條件超過邊界了, 應(yīng)該返回什么
- 為失敗做設(shè)計(jì),如果出問題了有降級(jí)應(yīng)對(duì)方案。
作者:趙玉開,十年以上互聯(lián)網(wǎng)研發(fā)經(jīng)驗(yàn),2013年加入京東,在運(yùn)營(yíng)研發(fā)部任架構(gòu)師,期間先后主持了物流系統(tǒng)自動(dòng)化運(yùn)維平臺(tái)、青龍數(shù)據(jù)監(jiān)控系統(tǒng)和物流開放平臺(tái)的研發(fā)工作,具有豐富的物流系統(tǒng)業(yè)務(wù)和架構(gòu)經(jīng)驗(yàn)。在此之前在和訊網(wǎng)負(fù)責(zé)股票基金行情系統(tǒng)的研發(fā)工作,具備高并發(fā)、高可用互聯(lián)網(wǎng)應(yīng)用研發(fā)經(jīng)驗(yàn)。
【本文來自51CTO專欄作者張開濤的微信公眾號(hào)(開濤的博客),公眾號(hào)id: kaitao-1234567】