如何写出让人抓狂的代码?
前言
今天跟大家聊一個有趣的話題:如何寫出讓人抓狂的代碼?
大家看到這個標題,第一印象覺得這篇文章可能是一篇水文。但我很負責的告訴你,它是一篇有很多干貨的技術文。
曾幾何時,你在閱讀別人代碼的時候,有沒有抓狂,想生氣,想發火的時候?
今天就跟大家一起聊聊,這20種我看了會抓狂的代碼,看看你中招了沒?
1.不注重代碼格式
代碼格式說起來很虛,下面我用幾個案例演示一下,不注重代碼格式的效果。作為這篇文章的開胃小菜吧。
1.1 空格
有時候必要的空格沒有加,比如:
@Service @Slf4j public?class?TestService1{ public?void?test1(){ addLog("test1");if?(condition1){if?(condition2){if?(condition3){log.info("info:{}",info);}}} } }你看了這段代碼有何感想,有沒有血壓飆升的感覺?
代碼好像揉到一起去了。
那么,如何把血壓降下來呢?
答:加上空格即可。
正解:
@Service @Slf4j public?class?TestService1?{public?void?test1()?{addLog("test1");if?(condition1)?{if?(condition2)?{if?(condition3)?{log.info("info:{}",?info);}}}} }只加了一些空格,稍微調整了一下,這段代碼的層次結構一下子變得非常清晰了。
好吧,我又冷靜下來了。
1.2 換行
寫代碼時,如果有些必要的換行沒有加,可能會出現這樣的代碼:
public?void?update(User?user)?{if?(null?!=?user.getId())?{User?oldUser?=?userMapper.findUserById(user.getId());if(null?==?oldUser)throw?new?RuntimeException("用戶id不存在");oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress());userMapper.updateUser(oldUser);}?else?{?userMapper.insertUser(user);} }看了這段代碼,是不是有點生無可戀的感覺?
簡單的加點空格優化一下:
public?void?update(User?user)?{if?(null?!=?user.getId())?{User?oldUser?=?userMapper.findUserById(user.getId());if(null?==?oldUser)?{throw?new?RuntimeException("用戶id不存在");}oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress());userMapper.updateUser(oldUser);}?else?{userMapper.insertUser(user);} }代碼邏輯一下子變得清晰了許多。
?
2.隨意的命名
java中沒有強制規定參數、方法、類或者包名該怎么起名。但如果我們沒有養成良好的起名習慣,隨意起名的話,可能會出現很多奇怪的代碼。
2.1 有意義的參數名
有時候,我們寫代碼時為了省事(可以少敲幾個字母),參數名起得越簡單越好。假如同事A寫的代碼如下:
int?a?=?1; int?b?=?2; String?c?=?"abc"; boolean?b?=?false;一段時間之后,同事A離職了,同事B接手了這段代碼。
他此時一臉懵逼,a是什么意思,b又是什么意思,還有c...然后心里一萬個草泥馬。
給參數起一個有意義的名字,是非常重要的事情,避免給自己或者別人埋坑。
正解:
int?supplierCount?=?1; int?purchaserCount?=?2; String?userName?=?"abc"; boolean?hasSuccess?=?false;2.2 見名知意
光起有意義的參數名還不夠,我們不能就這點追求。我們起的參數名稱最好能夠見名知意,不然就會出現這樣的情況:
String?yongHuMing?=?"蘇三"; String?用戶Name?=?"蘇三"; String?su3?=?"蘇三"; String?suThree?=?"蘇三";這幾種參數名看起來是不是有點怪怪的?
為啥不定義成國際上通用的(地球人都能看懂)英文單詞呢?
String?userName?=?"蘇三"; String?susan?=?"蘇三";上面的這兩個參數名,基本上大家都能看懂,減少了好多溝通成本。
所以建議在定義不管是參數名、方法名、類名時,優先使用國際上通用的英文單詞,更簡單直觀,減少溝通成本。少用漢子、拼音,或者數字定義名稱。
2.3 參數名風格一致
參數名其實有多種風格,列如:
//字母全小寫 int?suppliercount?=?1;//字母全大寫 int?SUPPLIERCOUNT?=?1;//小寫字母?+?下劃線 int?supplier_count?=?1;//大寫字母?+?下劃線 int?SUPPLIER_COUNT?=?1;//駝峰標識 int?supplierCount?=?1;如果某個類中定義了多種風格的參數名稱,看起來是不是有點雜亂無章?
所以建議類的成員變量、局部變量和方法參數使用supplierCount,這種駝峰風格,即:第一個字母小寫,后面的每個單詞首字母大寫。例如:
int?supplierCount?=?1;此外,為了好做區分,靜態常量建議使用SUPPLIER_COUNT,即:大寫字母 + 下劃線分隔的參數名。例如:
private?static?final?int?SUPPLIER_COUNT?=?1;?
3.出現大量重復代碼
ctrl + c 和 ctrl + v可能是程序員使用最多的快捷鍵了。
沒錯,我們是大自然的搬運工。哈哈哈。
在項目初期,我們使用這種工作模式,確實可以提高一些工作效率,可以少寫(實際上是少敲)很多代碼。
但它帶來的問題是:會出現大量的代碼重復。例如:
@Service @Slf4j public?class?TestService1?{public?void?test1()??{addLog("test1");}private?void?addLog(String?info)?{if?(log.isInfoEnabled())?{log.info("info:{}",?info);}} }@Service @Slf4j public?class?TestService2?{public?void?test2()??{addLog("test2");}private?void?addLog(String?info)?{if?(log.isInfoEnabled())?{log.info("info:{}",?info);}} }@Service @Slf4j public?class?TestService3?{public?void?test3()??{addLog("test3");}private?void?addLog(String?info)?{if?(log.isInfoEnabled())?{log.info("info:{}",?info);}} }在TestService1、TestService2、TestService3類中,都有一個addLog方法用于添加日志。
本來該功能用得好好的,直到有一天,線上出現了一個事故:服務器磁盤滿了。
原因是打印的日志太多,記了很多沒必要的日志,比如:查詢接口的所有返回值,大對象的具體打印等。
沒辦法,只能將addLog方法改成只記錄debug日志。
于是乎,你需要全文搜索,addLog方法去修改,改成如下代碼:
private?void?addLog(String?info)?{if?(log.isDebugEnabled())?{log.debug("debug:{}",?info);} }這里是有三個類中需要修改這段代碼,但如果實際工作中有三十個、三百個類需要修改,會讓你非常痛苦。改錯了,或者改漏了,都會埋下隱患,把自己坑了。
為何不把這種功能的代碼提取出來,放到某個工具類中呢?
@Slf4j public?class?LogUtil?{private?LogUtil()?{throw?new?RuntimeException("初始化失敗");}public?static?void?addLog(String?info)?{if?(log.isDebugEnabled())?{log.debug("debug:{}",?info);}} }然后,在其他的地方,只需要調用。
@Service @Slf4j public?class?TestService1?{public?void?test1()??{LogUtil.addLog("test1");} }如果哪天addLog的邏輯又要改了,只需要修改LogUtil類的addLog方法即可。你可以自信滿滿的修改,不需要再小心翼翼了。
我們寫的代碼,絕大多數是可維護性的代碼,而非一次性的。所以,建議在寫代碼的過程中,如果出現重復的代碼,盡量提取成公共方法。千萬別因為項目初期一時的爽快,而給項目埋下隱患,后面的維護成本可能會非常高。
?
4.從不寫注釋
有時候,在項目時間比較緊張時,很多人為了快速開發完功能,在寫代碼時,經常不喜歡寫注釋。
此外,還有些技術書中說過:好的代碼,不用寫注釋,因為代碼即注釋。這也給那些不喜歡寫代碼注釋的人,找了一個合理的理由。
但我個人覺得,在國內每個程序員的英文水平都不一樣,思維方式和編碼習慣也有很大區別。你要把前人某些復雜的代碼邏輯真正搞懂,可能需要花費大量的時間。
我們看到spring的核心方法refresh,也是加了很多注釋的:
public?void?refresh()?throws?BeansException,?IllegalStateException?{synchronized?(this.startupShutdownMonitor)?{//?Prepare?this?context?for?refreshing.prepareRefresh();//?Tell?the?subclass?to?refresh?the?internal?bean?factory.ConfigurableListableBeanFactory?beanFactory?=?obtainFreshBeanFactory();//?Prepare?the?bean?factory?for?use?in?this?context.prepareBeanFactory(beanFactory);try?{//?Allows?post-processing?of?the?bean?factory?in?context?subclasses.postProcessBeanFactory(beanFactory);//?Invoke?factory?processors?registered?as?beans?in?the?context.invokeBeanFactoryPostProcessors(beanFactory);//?Register?bean?processors?that?intercept?bean?creation.registerBeanPostProcessors(beanFactory);//?Initialize?message?source?for?this?context.initMessageSource();//?Initialize?event?multicaster?for?this?context.initApplicationEventMulticaster();//?Initialize?other?special?beans?in?specific?context?subclasses.onRefresh();//?Check?for?listener?beans?and?register?them.registerListeners();//?Instantiate?all?remaining?(non-lazy-init)?singletons.finishBeanFactoryInitialization(beanFactory);//?Last?step:?publish?corresponding?event.finishRefresh();}catch?(BeansException?ex)?{if?(logger.isWarnEnabled())?{logger.warn("Exception?encountered?during?context?initialization?-?"?+"cancelling?refresh?attempt:?"?+?ex);}//?Destroy?already?created?singletons?to?avoid?dangling?resources.destroyBeans();//?Reset?'active'?flag.cancelRefresh(ex);//?Propagate?exception?to?caller.throw?ex;}finally?{//?Reset?common?introspection?caches?in?Spring's?core,?since?we//?might?not?ever?need?metadata?for?singleton?beans?anymore...resetCommonCaches();}}}如果你寫的代碼完全不寫注釋,可能最近一個月、三個月、半年還記得其中的邏輯。但一年、兩年,甚至更久的時間之后,你確定還能想起當初的邏輯,而不需要花費大量的時間去重新看自己的代碼梳理邏輯?
說實話,不寫注釋,到了項目后期,不光是把自己坑了,還會坑隊友。
為什么把這一條單獨拿出來?
因為我遇到過,接過鍋,被坑慘了。
?
5.方法過長
我們平時在寫代碼時,有時候思路來了,一氣呵成,很快就把功能開發完了。但也可能會帶來一個小問題,就是方法過長。
偽代碼如下:
public?void?run()?{List<User>?userList?=?userMapper.getAll();//經過一系列的數據過濾//此處省略了50行代碼List<User>?updateList?=?//最終獲取到user集合if(CollectionUtils.isEmpty(updateList))?{return;}for(User?user:?updateList)?{//經過一些復雜的過期時間計算//此處省略30行代碼}//分頁更新用戶的過期時間//此處省略20行代碼//發mq消息通知用戶//此處省略30行代碼 }上面的run方法中包含了多種業務邏輯,雖說確實能夠實現完整的業務功能,但卻不能稱之為好。
為什么呢?
答:該方法總長度超過150行,里面的代碼邏輯很雜亂,包含了很多關聯性不大的代碼塊。該方法的職責太不單一了,非常不利于代碼復用和后期的維護。
那么,如何優化呢?
答:做方法拆分,即把一個大方法拆分成多個小方法。
例如:
public?void?run()?{List<User>?userList?=?userMapper.getAll();List<User>?updateList?=?filterUser(userList);if(CollectionUtils.isEmpty(updateList))?{return;}for(User?user:?updateList)?{clacExpireDay(user);}updateUser(updateList);sendMq(updateList);? }private?List<User>?filterUser(List<User>?userList)?{//經過一系列的數據過濾//此處省略了50行代碼List<User>?updateList?=?//最終獲取到user集合return?updateList; }private?void?clacExpireDay(User?user)?{//經過一些復雜的過期時間計算//此處省略30行代碼 }private?void?updateUser(List<User>?updateList)?{//分頁更新用戶的過期時間//此處省略20行代碼 }private?void?sendMq(List<User>?updateList)?{//發mq消息通知用戶//此處省略30行代碼 }這樣簡單的優化之后,run方法的代碼邏輯一下子變得清晰了許多,光看它調用的子方法的名字,都能猜到這些字方法是干什么的。
每個子方法只專注于自己的事情,別的事情交給其他方法處理,職責更單一了。
此外,如果此時業務上有一個新功能,也需要給用戶發消息,那么上面定義的sendMq方法就能被直接調用了。豈不是爽歪歪?
換句話說,把大方法按功能模塊拆分成N個小方法,更有利于代碼的復用。
順便說一句,Hotspot對字節碼超過8000字節的大方法有JIT編譯限制,超過了限制不會被編譯。
?
6.參數過多
我們平常在定義某個方法時,可能并沒注意參數個數的問題(其實是我猜的)。我的建議是方法的參數不要超過5個。
先一起看看下面的例子:
public?void?fun(String?a,String?b,String?c,String?d,String?e,String?f)?{... }public?void?client()?{fun("a","b","c","d",null,"f"); }上面的fun方法中定義了6個參數,這樣在調用該方面的所有地方都需要思考一下,這些參數該怎么傳值,哪些參數可以為空,哪些參數不能為空。
方法的入參太多,也會導致該方法的職責不單一,方法存在風險的概率更大。
那么,如何優化參數過多問題呢?
答:可以將一部分參數遷移到新方法中。
這個例子中,可以把參數d,e,f遷移到otherFun方法。例如:
public?Result?fun(String?a,String?b,String?c)?{...return?result; }public?void?otherFun(Result?result,String?d,String?e,String?f)?{...????? }public?void?client()?{Result?result?=?fun("a","b","c");otherFun(result,?"d",?null,?"f"); }這樣優化之后,每個方法的邏輯更單一一些,更有利于方法的復用。
如果fun中還需要返回參數a、b、c,給下個方法繼續使用,那么代碼可以改為:
public?Result?fun(String?a,String?b,String?c)?{...Result?result?=?new?Result();result.setA(a);result.setB(b);result.setC(c);return?result; }在給Result對象賦值時,這里有個小技巧,可以使用lombok的@Builder注解,做成鏈式調用。例如:
@NoArgsConstructor @AllArgsConstructor @Builder @Data public?class?Result?{private?String?a;private?String?b;private?String?c; }這樣在調用的地方,可以這樣賦值:
Result?result?=?Result.builder() .a("a").b("b").c("c") .build();非常直觀明了。
此時,有人可能會說,ThreadPoolExecutor不也提供了7個參數的方法?
public?ThreadPoolExecutor(int?corePoolSize,int?maximumPoolSize,long?keepAliveTime,TimeUnit?unit,BlockingQueue<Runnable>?workQueue,ThreadFactory?threadFactory,RejectedExecutionHandler?handler)?{...????????????????????? }沒錯,不過它是構造方法,我們這里主要討論的是普通方法。
?
7.代碼層級太深
不知道你有沒有見過類似這樣的代碼:
if?(a?==?1)?{if(b?==?2)?{if(c?==?3)?{if(d?==?4)?{if(e?==?5)?{...}...}...}...}... }這段代碼中有很多層if判斷,是不是看得人有點眼花繚亂?
有同感的同學,請舉個手。
如果你沒啥感覺,那么接著往下看:
for(int?i=0;?i<100;i++)?{for(int?j=0;?j<50;j++)?{for(int?m=0;?m<200;m++)?{for(int?n=0;?n<100;n++)?{for(int?k=0;?k<50;?k++)?{...}}}} }看了這段代碼,你心中可能會一緊。這么多循環,代碼的性能真的好嗎?
這兩個例子中的代碼都犯了同一個錯誤,即:代碼層級太深。
代碼層級太深導致的問題是代碼變得非常不好維護,不容易理清邏輯,有時候代碼的性能也可能因此變差。
那么關鍵問題來了,如何解決代碼層級較深的問題呢?
對于if判斷層級比較多的情況:
if(a!=1)?{...return; }doConditionB();private?void?doConditionB()?{if(b!=2)?{...return;}doConditionC(); }把不滿足條件(a==1)的邏輯先執行,先返回。再把滿足條件(a==1)的邏輯單獨抽取到一個方法(doConditionB)中。該doConditionB中也會把不滿足條件(b==2)的邏輯先執行,先返回。再把滿足條件(b==2)的邏輯單獨抽取到一個方法(doConditionC)中。后面邏輯以此類推。
這種做法是面向防御式編程的一種,即先把不滿足條件的代碼先執行,然后才執行滿足條件的代碼。此外別忘了,把滿足條件的代碼抽取到一個新的方法中喔。
對于for循環層級太深的優化方案,一般推薦使用map。
例如:
for(Order?order:orderList)?{for(OrderDetail?detail:?detailList)?{if(order.getId().equals(detail.getOrderId()))?{doSamething();}} }使用map優化之后:
Map<Long,?List<OrderDetail>>?detailMap?=??detailList.stream().collect(Collectors.groupingBy(OrderDetail::getOrderId));for(Order?order:orderList)?{List<OrderDetail>?detailList?=?detailMap.get(order.getId());if(CollectionUtils.isNotEmpty)?{doSamething();} }這個例子中使用map,少了一層循環,代碼效率提升一些。但不是所有的for循環都能用map替代,要根據自己實際情況選擇。
代碼層級太深,還有其他的場景,比如:方法中return的次數太多,也會降低代碼的可讀性。
這種情況,其實也可能通過面向防御式編程進行代碼優化。
?
8.判斷條件太多
我們在寫代碼的時候,判斷條件是必不可少的。不同的判斷條件,走的代碼邏輯通常會不一樣。
廢話不多說,先看看下面的代碼。
public?interface?IPay?{??void?pay();?? }??@Service public?class?AliaPay?implements?IPay?{??@Overridepublic?void?pay()?{??System.out.println("===發起支付寶支付===");??}?? }??@Service public?class?WeixinPay?implements?IPay?{??@Overridepublic?void?pay()?{??System.out.println("===發起微信支付===");??}?? }??@Service public?class?JingDongPay?implements?IPay?{??@Overridepublic?void?pay()?{??System.out.println("===發起京東支付===");??}?? }??@Service public?class?PayService?{??@Autowiredprivate?AliaPay?aliaPay;??@Autowiredprivate?WeixinPay?weixinPay;??@Autowiredprivate?JingDongPay?jingDongPay;??public?void?toPay(String?code)?{??if?("alia".equals(code))?{??aliaPay.pay();??}?elseif?("weixin".equals(code))?{??weixinPay.pay();??}?elseif?("jingdong".equals(code))?{??jingDongPay.pay();??}?else?{??System.out.println("找不到支付方式");??}??}?? }PayService類的toPay方法主要是為了發起支付,根據不同的code,決定調用用不同的支付類(比如:aliaPay)的pay方法進行支付。
這段代碼有什么問題呢?也許有些人就是這么干的。
試想一下,如果支付方式越來越多,比如:又加了百度支付、美團支付、銀聯支付等等,就需要改toPay方法的代碼,增加新的else...if判斷,判斷多了就會導致邏輯越來越多?
很明顯,這里違法了設計模式六大原則的:開閉原則 和 單一職責原則。
開閉原則:對擴展開放,對修改關閉。就是說增加新功能要盡量少改動已有代碼。
單一職責原則:顧名思義,要求邏輯盡量單一,不要太復雜,便于復用。
那么,如何優化if...else判斷呢?
答:使用 策略模式+工廠模式。
策略模式定義了一組算法,把它們一個個封裝起來, 并且使它們可相互替換。 工廠模式用于封裝和管理對象的創建,是一種創建型模式。
public?interface?IPay?{void?pay(); }@Service public?class?AliaPay?implements?IPay?{@PostConstructpublic?void?init()?{PayStrategyFactory.register("aliaPay",?this);}@Overridepublic?void?pay()?{System.out.println("===發起支付寶支付===");} }@Service public?class?WeixinPay?implements?IPay?{@PostConstructpublic?void?init()?{PayStrategyFactory.register("weixinPay",?this);}@Overridepublic?void?pay()?{System.out.println("===發起微信支付===");} }@Service public?class?JingDongPay?implements?IPay?{@PostConstructpublic?void?init()?{PayStrategyFactory.register("jingDongPay",?this);}@Overridepublic?void?pay()?{System.out.println("===發起京東支付===");} }public?class?PayStrategyFactory?{private?static?Map<String,?IPay>?PAY_REGISTERS?=?new?HashMap<>();public?static?void?register(String?code,?IPay?iPay)?{if?(null?!=?code?&&?!"".equals(code))?{PAY_REGISTERS.put(code,?iPay);}}public?static?IPay?get(String?code)?{return?PAY_REGISTERS.get(code);} }@Service public?class?PayService3?{public?void?toPay(String?code)?{PayStrategyFactory.get(code).pay();} }這段代碼的關鍵是PayStrategyFactory類,它是一個策略工廠,里面定義了一個全局的map,在所有IPay的實現類中注冊當前實例到map中,然后在調用的地方通過PayStrategyFactory類根據code從map獲取支付類實例即可。
如果加了一個新的支付方式,只需新加一個類實現IPay接口,定義init方法,并且重寫pay方法即可,其他代碼基本上可以不用動。
當然,消除又臭又長的if...else判斷,還有很多方法,比如:使用注解、動態拼接類名稱、模板方法、枚舉等等。
?
9.硬編碼
不知道你有沒有遇到過這類需求:
限制批量訂單上傳接口,一次性只能上傳200條數據。
在job中分頁查詢用戶,一頁查詢100個用戶,然后計算用戶的等級。
上面例子中的200條數據和100個用戶,很容易硬編碼,即在代碼中把參數寫死了。
我們以上傳200條數據為例:
private?static?final?int?MAX_LIMIT?=?200;public?void?upload(List<Order>?orderList)?{if(CollectionUtils.isEmpty(orderList))?{throw?new?BusinessException("訂單不能為空");}?if(orderList.size()?>?MAX_LIMIT)?{throw?new?BusinessException("超過單次請求的數量限制");} }其中MAX_LIMIT被定義成了靜態常量。
上線之后,你發現上傳歷史數據時速度太慢了,需要把限制調大一點。
我擦。。。這種小小的參數改動,還需要改源代碼,重新編譯,重新打包,重新部署。。。
但如果你當初把這些公共參數,設置成可配置的,例如:
@Value("${com.susan.maxLimit:200}") private?int?maxLimit?=?200;public?void?upload(List<Order>?orderList)?{if(CollectionUtils.isEmpty(orderList))?{throw?new?BusinessException("訂單不能為空");}?if(orderList.size()?>?maxLimit)?{throw?new?BusinessException("超過單次請求的數量限制");} }這樣只需在配置中心(比如:apollo、nocas等)中修改一下配置即可,不用修改源代碼,不用重新編譯,不用重新打包,不用重新部署。
一個字:爽。
我們在前期開發的時候,寧可多花一分鐘思考一下,這個參數后面是否會被修改,是否可以定義成可配置的參數。也比后期修改代碼,重新編譯,重新打包,重新上線花的時間少得多。
?
10.事務過大
我們平時在使用spring框架開發項目時,喜歡用@Transactional注解聲明事務。例如:
@Transactional(rollbackFor?=?Throwable.class) public?void?updateUser(User?user)?{System.out.println("update"); }只需在需要使用事務的方法上,使用@Transactional注解聲明一下,該方法通過AOP就自動擁有了事務的功能。
沒錯,這種做法給我們帶來了極大的便利,開發效率更高了。
但也給我們帶來了很多隱患,比如大事務的問題。我們一起看看下面的這段代碼:
@Transactional(rollbackFor?=?Throwable.class) public?void?updateUser(User?user)?{User?oldUser?=?userMapper.getUserById(user.getId());if(null?!=?oldUser)?{userMapper.update(user);}?else?{userMapper.insert(user);}sendMq(user); }這段代碼中getUserById方法和sendMq方法,在這個案例中無需使用事務,只有update或insert方法才需要事務。
所以上面這段代碼的事務太大了,是整個方法級別的事務。假如sendMq方法是一個非常耗時的操作,則可能會導致整個updateUser方法的事務超時,從而出現大事務問題。
那么,如何解決這個問題呢?
答:可以使用TransactionTemplate的編程式事務優化代碼。
@Autowired private?TransactionTemplate?transactionTemplate;....public?void?updateUser(User?user)?{User?oldUser?=?userMapper.getUserById(user.getId());transactionTemplate.execute((status)?=>?{if(null?!=?oldUser)?{userMapper.update(user);}?else?{userMapper.insert(user);}return?Boolean.TRUE;})sendMq(user); }只有在execute方法中的代碼塊才真正需要事務,其余的方法,可以非事務執行,這樣就能縮小事務的范圍,避免大事務。
當然使用TransactionTemplate這種編程式事務,縮小事務范圍,來解決大事務問題,只是其中一種手段。
?
11.在循環中遠程調用
有時候,我們需要在某個接口中,遠程調用第三方的某個接口。
比如:在注冊企業時,需要調用天眼查接口,查一下該企業的名稱和統一社會信用代碼是否正確。
這時候在企業注冊接口中,不得不先調用天眼查接口校驗數據。如果校驗失敗,則直接返回。如果校驗成功,才允許注冊。
如果只是一個企業還好,但如果某個請求有10個企業需要注冊,是不是要在企業注冊接口中,循環調用10次天眼查接口才能判斷所有企業是否正常呢?
public?void?register(List<Corp>?corpList)?{for(Corp?corp:?corpList)?{CorpInfo?info?=?tianyanchaService.query(corp);??if(null?==?info)?{throw?new?RuntimeException("企業名稱或統一社會信用代碼不正確");}}doRegister(corpList); }這樣做可以,但會導致整個企業注冊接口性能很差,極容易出現接口超時問題。
那么,如何解決這類在循環中調用遠程接口的問題呢?
11.1 批量操作
遠程接口支持批量操作,比如天眼查支持一次性查詢多個企業的數據,這樣就無需在循環中查詢該接口了。
但實際場景中,有些第三方不愿意提供第三方接口。
11.2 并發操作
java8以后通過CompleteFuture類,實現多個線程查天眼查接口,并且把查詢結果統一匯總到一起。
?
12.頻繁捕獲異常
通常情況下,為了在程序中拋出異常時,任然能夠繼續運行,不至于中斷整個程序,我們可以選擇手動捕獲異常。例如:
public?void?run()?{try?{doSameThing();}?catch?(Exception?e)?{//ignore}doOtherThing(); }這段代碼可以手動捕獲異常,保證即使doSameThing方法出現了異常,run方法也能繼續執行完。
但有些場景下,手動捕獲異常被濫用了。
12.1 濫用場景1
不知道你在打印異常日志時,有沒有寫過類似這樣的代碼:
public?void?run()?throws?Exception?{try?{doSameThing();}?catch?(Exception?e)?{log.error(e.getMessage(),?e);throw?e;}doOtherThing(); }通過try/catch關鍵字,手動捕獲異常的目的,僅僅是為了記錄錯誤日志,在接下來的代碼中,還是會把該異常拋出。
在每個拋出異常的地方,都捕獲一下異常,打印日志。
12.2 濫用場景2
在寫controller層接口方法時,為了保證接口有統一的返回值,你有沒有寫過類似這樣的代碼:
@PostMapping("/query") public?List<User>?query(@RequestBody?List<Long>?ids)?{try?{List<User>?userList?=?userService.query(ids);return?Result.ok(userList);}?catch?(Exception?e)?{log.error(e.getMessage(),?e);return?Result.fature(500,?"服務器內部錯誤");} }在每個controller層的接口方法中,都加上了上面這種捕獲異常的邏輯。
上述兩種場景中,頻繁的捕獲異常,會讓代碼性能降低,因為捕獲異常是會消耗性能的。
此外,這么多重復的捕獲異常代碼,看得讓人頭疼。
其實,我們還有更好的選擇。在網關層(比如:zuul或gateway),有個統一的異常處理代碼,既可以打印異常日志,也能統一封裝接口返回值,這樣可以減少很多異常被濫用的情況。
?
13.不正確的日志打印
在我們寫代碼的時候,打印日志是必不可少的工作之一。
因為日志可以幫我們快速定位問題,判斷代碼當時真正的執行邏輯。
但打印日志的時候也需要注意,不是說任何時候都要打印日志,比如:
@PostMapping("/query") public?List<User>?query(@RequestBody?List<Long>?ids)?{log.info("request?params:{}",?ids);List<User>?userList?=?userService.query(ids);log.info("response:{}",?userList);return?userList; }對于有些查詢接口,在日志中打印出了請求參數和接口返回值。
咋一看沒啥問題。
但如果ids中傳入值非常多,比如有1000個。而該接口被調用的頻次又很高,一下子就會打印大量的日志,用不了多久就可能把磁盤空間打滿。
如果真的想打印這些日志該怎么辦?
@PostMapping("/query") public?List<User>?query(@RequestBody?List<Long>?ids)?{if?(log.isDebugEnabled())?{log.debug("request?params:{}",?ids);}List<User>?userList?=?userService.query(ids);if?(log.isDebugEnabled())?{log.debug("response:{}",?userList);}return?userList; }使用isDebugEnabled判斷一下,如果當前的日志級別是debug才打印日志。生產環境默認日志級別是info,在有些緊急情況下,把某個接口或者方法的日志級別改成debug,打印完我們需要的日志后,又調整回去。
方便我們定位問題,又不會產生大量的垃圾日志,一舉兩得。
?
14.沒校驗入參
參數校驗是接口必不可少的功能之一,一般情況下,提供給第三方調用的接口,需要做嚴格的參數校驗。
以前我們是這樣校驗參數的:
@PostMapping("/add") public?void?add(@RequestBody?User?user)?{if(StringUtils.isEmpty(user.getName()))?{throw?new?RuntimeException("name不能為空");}if(null?!=?user.getAge())?{throw?new?RuntimeException("age不能為空");}if(StringUtils.isEmpty(user.getAddress()))?{throw?new?RuntimeException("address不能為空");}userService.add(user); }需要手動寫校驗的代碼,如果作為入參的實體中字段非常多,光是寫校驗的代碼,都需要花費大量的時間。而且這些校驗代碼,很多都是重復的,會讓人覺得惡心。
好消息是使用了hibernate的參數校驗框架validate之后,參數校驗一下子變得簡單多了。
我們只需要校驗的實體類User中使用validation框架的相關注解,比如:@NotEmpty、@NotNull等,定義需要校驗的字段即可。
@NoArgsConstructor @AllArgsConstructor @Data public?class?User?{private?Long?id;@NotEmptyprivate?String?name;@NotNullprivate?Integer?age;@NotEmptyprivate?String?address; }然后在controller類上加上@Validated注解,在接口方法上加上@Valid注解。
@Slf4j @Validated @RestController @RequestMapping("/user") public?class?UserController?{@Autowiredprivate?UserService?userService;@PostMapping("/add")public?void?add(@RequestBody?@Valid?User?user)?{userService.add(user);} }這樣就能自動實現參數校驗的功能。
然而,現在需求改了,需要在User類上增加了一個參數Role,它也是必填字段,并且它的roleName和tag字段都不能為空。
但如果我們在校驗參數時,不小心把代碼寫成這樣:
@NoArgsConstructor @AllArgsConstructor @Data public?class?User?{private?Long?id;@NotEmptyprivate?String?name;@NotNullprivate?Integer?age;@NotEmptyprivate?String?address;@NotNullprivate?Role?role; }@Data @NoArgsConstructor @AllArgsConstructor public?class?Role?{@NotEmptyprivate?String?roleName;@NotEmptyprivate?String?tag; }結果就悲劇了。
你心里可能還樂呵呵的認為寫的代碼不錯,但實際情況是,roleName和tag字段根本不會被校驗到。
如果傳入參數:
{"name":?"tom","age":1,"address":"123","role":{} }即使role字段傳入的是空對象,但該接口也會返回成功。
那么如何解決這個問題呢?
@NoArgsConstructor @AllArgsConstructor @Data public?class?User?{private?Long?id;@NotEmptyprivate?String?name;@NotNullprivate?Integer?age;@NotEmptyprivate?String?address;@NotNull@Validprivate?Role?role; }需要在Role字段上也加上@Valid注解。
溫馨的提醒一聲,使用validate框架校驗參數一定要自測,因為很容易踩坑。
?
15.返回值格式不統一
我之前對接某個第三方時,他們有部分接口的返回值結構是這樣的:
{"ret":0,"message":null,"data":[] }另一部分接口的返回值結構是這樣的:
{"code":0,"msg":null,"success":true,"result":[] }整得我有點懵逼。
為啥沒有一個統一的返回值?
我需要給他們的接口寫兩套返回值解析的代碼,后面其他人看到了這些代碼,可能也會心生疑問,為什么有兩種不同的返回值解析?
唯一的解釋是一些接口是新項目的,另外一些接口是老項目的。
但如果不管是新項目,還是老項目,如果都有一個統一的對外網關服務,由這個服務進行鑒權和統一封裝返回值。
{"code":0,"message":null,"data":[] }就不會有返回值結構不一致的問題。
溫馨的提醒一下,業務服務不要捕獲異常,直接把異常拋給網關服務,由它來統一全局捕獲異常,這樣就能統一異常的返回值結構。
?
16.提交到git的代碼不完整
我們寫完代碼之后,把代碼提交到gitlab上,也有一些講究。
最最忌諱的是代碼還沒有寫完,因為趕時間(著急下班),就用git把代碼提交了。例如:
public?void?test()?{String?userName="蘇三";String?password= }這段代碼中的password變量都沒有定義好,項目一運行起來必定報錯。
這種錯誤的代碼提交方式,一般是新手會犯。但還有另一種情況,就是在多個分支merge代碼的時候,有時候會出問題,merge之后的代碼不能正常運行,就被提交了。
好的習慣是:用git提交代碼之前,一定要在本地運行一下,確保項目能正常啟動才能提交。
寧可不提交代碼到遠程倉庫,切勿因為一時趕時間,提交了不完整的代碼,導致團隊的隊友們項目都啟動不了。
?
17.不處理沒用的代碼
有些時候,我們為了偷懶,對有些沒用的代碼不做任何處理。
比如:
@Slf4j @Service public?class?UserService?{@Autowiredprivate?UserMapper?userMapper;public?void?add(User?user)?{System.out.println("add");}public?void?update(User?user)?{System.out.println("update");}public?void?query(User?user)?{System.out.println("query");} }本來UserService類中的add、update、query方法都在用的。后來,某些功能砍掉了,現在只有add方法真正在用。
某一天,項目組來了一個新人,接到需求需要在user表加一個字段,這時候他是不是要把add、update、query方法都仔細看一遍,評估一下影響范圍?
后來發現只有add方法需要改,他心想前面的開發者為什么不把沒用的代碼刪掉,或者標記出來呢?
在java中可以使用@Deprecated表示這個類或者方法沒在使用了,例如:
@Slf4j @Service public?class?UserService?{@Autowiredprivate?UserMapper?userMapper;public?void?add(User?user)?{System.out.println("add");}@Deprecatedpublic?void?update(User?user)?{System.out.println("update");}@Deprecatedpublic?void?query(User?user)?{System.out.println("query");} }我們在閱讀代碼時,可以先忽略標記了@Deprecated注解的方法。這樣一個看似簡單的舉手之勞,可以給自己,或者接手該代碼的人,節省很多重復查代碼的時間。
建議我們把沒用的代碼優先刪除掉,因為gitlab中是有歷史記錄的,可以找回。但如果有些為了兼容調用方老版本的代碼,不能刪除的情況,建議使用@Deprecated注解相關類或者接口。
?
18.隨意修改接口名和參數名
不知道你有沒有遇到過這種場景:你寫了一個接口,本來以為沒人使用,后來覺得接口名或參數名不對,偷偷把它們改了。比如:
@PostMapping("/query") public?List<User>?query(@RequestBody?List<Long>?ids)?{return?userService.query(ids); }接口名改了:
@PostMapping("/queryUser") public?List<User>?queryUser(@RequestBody?List<Long>?ids)?{return?userService.query(ids); }結果導致其他人的功能報錯,原來他已經在調用該接口了。
大意了。。。
所以在修改接口名、參數名、修改參數類型、修改參數個數時,一定要先詢問一下相關同事,有沒有使用該接口,免得以后出現不必要的麻煩。
對于已經在線上使用的接口,盡量不要修改接口名、參數名、修改參數類型、修改參數個數,還有請求方式,比如:get改成post等。寧可新加一個接口,也盡量不要影響線上功能。
?
19.使用map接收參數
我之前見過有些小伙伴,在代碼中使用map接收參數的。例如:
@PostMapping("/map") public?void?map(@RequestBody?Map<String,?Object>?mapParam){System.out.println(mapParam); }在map方法中使用mapParam對象接收參數,這種做法確實很方便,可以接收多種json格式的數據。
例如:
{"id":123,"name":"蘇三","age":18,"address":"成都" }或者:
{"id":123,"name":"蘇三","age":18,"address":"成都","role":?{"roleName":"角色","tag":"t1"} }這段代碼可以毫不費勁的接收這兩種格式的參數,so cool。
但同時也帶來了一個問題,那就是:參數的數據結構你沒法控制,有可能你知道調用者傳的json數據格式是第一種,還是第二種。但如果你沒有寫好注釋,其他的同事看到這段代碼,可能會一臉懵逼,map接收的參數到底是什么東東?
項目后期,這樣的代碼變得非常不好維護。有些同學接手前人的代碼,時不時吐槽一下,是有原因的。
那么,如果優化這種代碼呢?
我們應該使用有明確含義的對象去接收參數,例如:
@PostMapping("/add") public?void?add(@RequestBody?@Valid?User?user){System.out.println(user); }其中的User對象是我們已經定義好的對象,就不會存在什么歧義了。
?
20.從不寫單元測試
因為項目時間實在太緊了,系統功能都開發不完,更何況是單元測試呢?
大部分人不寫單元測試的原因,可能也是這個吧。
但我想告訴你的是,不寫單元測試并不是個好習慣。
我見過有些編程高手是測試驅動開發,他們會先把單元測試寫好,再寫具體的業務邏輯。
那么,我們為什么要寫單元測試呢?
我們寫的代碼大多數是可維護的代碼,很有可能在未來的某一天需要被重構。試想一下,如果有些業務邏輯非常復雜,你敢輕易重構不?如果有單元測試就不一樣了,每次重構完,跑一次單元測試,就知道新寫的代碼有沒有問題。
我們新寫的對外接口,測試同學不可能完全知道邏輯,只有開發自己最清楚。不像頁面功能,可以在頁面上操作。他們在測試接口時,很有可能覆蓋不到位,很多bug測不出來。
建議由于項目時間非常緊張,在開發時確實沒有寫單元測試,但在項目后期的空閑時間也建議補上。
本文結合自己的實際工作經驗,用調侃的方式,介紹了在編寫代碼的過程中,不太好的地方和一些優化技巧,給用需要的朋友們一個參考。
技術交流群
最近有很多人問,有沒有讀者交流群,想知道怎么加入。
最近我創建了一些群,大家可以加入。交流群都是免費的,只需要大家加入之后不要隨便發廣告,多多交流技術就好了。
目前創建了多個交流群,全國交流群、北上廣杭深等各地區交流群、面試交流群、資源共享群等。
有興趣入群的同學,可長按掃描下方二維碼,一定要備注:全國 Or 城市 Or 面試 Or 資源,根據格式備注,可更快被通過且邀請進群。
▲長按掃描
往期推薦把元素周期表也禁了?
麻了!35歲奧地利總理辭職當碼農
SpringBoot 3.0最低版本要求的JDK 17,這幾個新特性不能不知道!
如果你喜歡本文,
請長按二維碼,關注?Hollis.
轉發至朋友圈,是對我最大的支持。
點個?在看?
喜歡是一種感覺
在看是一種支持
↘↘↘
總結
以上是生活随笔為你收集整理的如何写出让人抓狂的代码?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求职和跳槽最好的月份要来了吗
- 下一篇: 这样实现分布式锁,才叫优雅!