在做新需求開發或者相關系統的維護更新時,尤其是涉及到不同系統的接口調用時,在可維護性方面,總感覺有很多地方差強人意。一些零星思考,抛磚引玉,希望引發更多的思考和讨論。總結了大概有如下幾條建議:
1.在接口注釋中加入接口文檔連結
2.将調用接口處寫上被調用接口文檔連結
3.将接口源代碼釋出到私服倉庫
4.對于狀态值常量,優先在接口參數類或者傳回值類中定義
5.如果使用 Map 對象作為傳輸載體,要提供 Key 值定義常量
6.針對 Map 傳回值,可以考慮使用将 Map 轉化成對象
7.盡可能簡化接口依賴
8.隻傳遞必要字段,盡量避免大而全的接口
9.将接口的參數和傳回值原始資料列印到日志中
10.将 RPC 接口的類名及方法列印到日志中
11.核心思想:以人為本,就近原則,觸手可及
下面,D瓜哥對每一條建議做一個詳細說明。
1. 在接口注釋中加入接口文檔連結
在做接口開發時,無論是對自有接口的更新改造,還是針對外部接口的從頭接入,都涉及到接口文檔。不同之處是,前者的工作重點是書寫或者更新接口文檔;而後者是根據接口文檔開發合适的接入代碼。
但是,經常遇到的一個麻煩是,找不到接口文檔。在組内需要找老同僚詢問;如果是跨部門,還需要兩層甚至三層的進行轉接,非常麻煩。
D瓜哥認為,在這種情況下,為了友善大家維護,最好的辦法就是将接口文檔連結直接放在代碼注釋中,這樣後續維護的人員,直接就可以點選連結直達接口文檔,簡單友善高效。如果是建立的接口,就可以先建立一個空文檔,把連結放在注釋中,後續再書寫文檔内容。
如果是維護已有接口,可以在維護時,将缺失的連結加入到注釋中,自己友善,也友善其他人進行後續的維護更新。這樣,在循序漸進的過程中,逐漸就可以把文檔連結補充到代碼中,友善維護代碼,也同步更新文檔。
2. 将調用接口處寫上被調用接口文檔連結
在調用其他系統的接口時,沒有接口文檔,幾乎寸步難行。在第一次接入接口時,絕大多數情況下,都是參考着接口文檔做接入工作。但是,目前的情況時,接入時參考文檔,參考完就随手把文檔給“扔了”。後續如果還需要做進一步更新維護,還需要到處找接口文檔;另外,互動的系統難免有一些 Bug,在和其他系統維護人員對接處理 Bug 時,隻有接口沒有文檔,對方可能也需要去找文檔連結。無形中,很多時間都浪費在了找文檔的過程中。
D瓜哥最近嘗試了一個實踐,就是在接口調用的地方,把接口文檔連結當做注釋加入到代碼中。這樣,無論是後續維護更新,還是溝通協調處理問題,都非常友善。别人問接口是什麼,連接配接口+文檔都可以一把複制就搞定。
經過最近一段時間的實踐情況來看,這個處理非常友善,是一個非常值得推廣的實踐。再插一句,也可以像一條建議一樣,可以在維護代碼時,不斷把已接入的接口文檔加入到調用接口的地方,循序漸進,友善後續人維護更新。
3. 将接口源代碼釋出到私服倉庫
接口文檔連結在注釋中,在建構結果中就不複存在了。是以,為了友善接口使用方可以在接口中查詢到對應的接口文檔,就需要把源碼也釋出到私服倉庫中。
這裡隻說明一下 Java 的相關處理辦法。如果使用 Maven 作為建構工具的話,預設是不會将源代碼釋出到私服倉庫中的。關于如何将源代碼釋出到,在 更新 Maven 插件:将源碼釋出到私服倉庫 中已經做過相關介紹,這裡就不再贅述。
除了将源碼釋出到私服倉庫,另外,還建議編譯建構時,保持方法的原始參數命名。這個也可以通過配置 Maven 插件來完成,具體配置見: 更新 Maven 插件:位元組碼檔案包含原始參數名稱。
4. 對于狀态值常量,優先在接口參數類或者傳回值類中定義
在做接口開發時,很多資料都有一個狀态值,比如訂單狀态,再比如接口狀态等等。目前的一個情況時,這些狀态值大部分書寫在文檔中,在接入接口時,需要接入方自定義這些狀态值。這就有些繁瑣了,而且狀态定義也不明确,甚至有可能遺漏一些重要的狀态值。有些懶省事,直接在代碼中寫死一個魔法值,後續維護的跟還需要根據上下文反推這個值的含義,非常不利于維護。
D瓜哥個人覺得,有兩個處理辦法:
- 如果狀态值不是很多,優先在接口參數類或者傳回值類中定義。
- 如果狀态值很多,可以考慮單獨抽取成一個常量類或者枚舉類。
這樣使用的時候,觸手可及。不需要到處去找。
5. 如果使用 Map 對象作為傳輸載體,要提供 Key 值定義常量
有些系統可能考慮友善增加字段,選擇使用 Map 作為資料載體。自己開發的時候很爽,但是給接口接入卻非常不友好。接入方從 Map 中擷取資料時,要麼自己定義 Key 值;要麼直接使用魔法值寫死在代碼中。使用前者方案,就需要在各個接入方都需要自定義一套;使用後者,初期是省事了,後來維護的人員就懵逼了。這都無形中增加了很多元護成本。
D瓜哥覺得一個方案更優,那就是直接由接口提供方來定義這些可以取值的 Key 值常量。這樣,任何接入方都可以直接使用這些常量。
6. 針對 Map 傳回值,可以考慮使用将 Map 轉化成對象
針對 Map 的處理,即使按照 如果使用 Map 對象作為傳輸載體,要提供 Key 值定義常量 推薦的做法,定義了相關的 Key,在取值時,也略有麻煩,需要不斷的 map.get(KEY)。一個更簡單的方法是自定義一個類型,使用工具将 Map 對象轉化成自定義類型的對象。這樣就可以直接使用方法調用來取值。
在 Java 中,可以直接使用 Jackson 來完成這個轉換工作。工具類代碼如下:
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import java.util.*;java
/**
* Map 工具類
*
* @author D瓜哥 · https://www.diguage.com
*/
@Slf4j
public class MapUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 将 Map 轉換成指定類型的對象
*
* @author D瓜哥 · https://www.diguage.com
*/
public static <T> T convertToObject(Map<String, Object> data, Class<T> clazz) {
try {
T result = MAPPER.convertValue(data, MAPPER.getTypeFactory().constructType(clazz));
if (log.isInfoEnabled()) {
log.info("converted {} to a {} object: {}",
JsonUtils.toJson(data), clazz.getSimpleName(), JsonUtils.toJson(result));
}
return result;
} catch (Exception e) {
log.error("converting failed! data: {}, class: {}",
JsonUtils.toJson(data), clazz.getSimpleName(), e);
}
return null;
}
/**
* 将 Map 轉換成指定類型的對象
*
* @author D瓜哥 · https://www.diguage.com
*/
public static <T> List<T> convertToObjects(List<Map<String, Object>> datas, Class<T> clazz) {
if (CollectionUtils.isEmpty(datas) || Objects.isNull(clazz)) {
return Collections.emptyList();
}
List<T> result = new ArrayList<>(datas.size());
if (CollectionUtils.isNotEmpty(datas)) {
for (Map<String, Object> data : datas) {
T t = convertToObject(data, clazz);
result.add(t);
}
}
return result;
}
}
7. 盡可能簡化接口依賴
現在,很多對外暴露接口的定義是,接口定義放在一個子產品中;模型定義在一個子產品中;有些工具類又定義在一個子產品中。接口依賴模型子產品;模型子產品又依賴工具類子產品;而工具類依賴了一大堆外部依賴。個人覺得這是一個非常不好的實踐。會導緻很多不必要的依賴被間接引入到了接口使用方的系統中,無形中增加很多元護成本。
D瓜哥推薦的一個實踐是:将接口和模型定義放在一個子產品中,對外暴露也隻需要這一個子產品即可。接口使用方隻需要引入這一個依賴。避免引入很多無用的其他外部依賴。如果模型需要依賴一些公共的父類,可以考慮将這些單獨定義在一個子產品中,這個子產品隻儲存多個系統依賴的公共類,并且剔除掉一些工具類的定義,這樣就可以保證接口依賴的純淨性。如果其他系統需要工具類,讓其明确去引入,而不是被動依賴。
對于前面 對于狀态值常量,優先在接口參數類或者傳回值類中定義 中提到了“如果狀态值很多,可以考慮單獨抽取成一個常量類或者枚舉類。” 這裡存在一種情況需要特别說明,狀态值的定義需要在本系統的業務子產品的代碼中使用,可以将接口的依賴加入到改業務子產品的依賴中,而不是反過來。為什麼會這樣的操作?一個核心思想是保持對外暴露接口的純淨性。這樣既可以減少狀态定義的重複性,又可以減少接口的外部依賴。
8. 隻傳遞必要字段,盡量避免大而全的接口
觀察很多系統,尤其是一些以業務為核心的系統的對外暴露接口,很多接口是大而全的接口,一個接口就可以把指定資料的所有資訊全部傳回出去。這樣,很多字段需要去識别,也要在衆多字段中區篩選出來符合自己要求的資料,無形中浪費了很多心智,不利于維護。
D瓜哥認為,在做接口開發時,一定要做一個“吝啬的守财奴”。把資料當做财富一樣守護,對外隻提供必要的資料,做到“夠用就行”。
這一點不僅僅是維護上的考慮,還有資料傳輸效率的點。在其他條件相同的情況下,更小的資料,無論是機器處理效率,還是傳輸效率,都會更快更高。
關于傳輸效率上的一些思考,結合 Hessian、Msgpack 和 JSON 執行個體對比 以及 “Hessian 協定解釋與實戰” 等文章來看,有幾個原則值得重視的:
- 優先使用 boolean 型;
- boolean 型滿足不了,次優選擇 int 整型資料;再次可以考慮 long 型;
- 日期優先使用内置的日期類型(含 Java Time API 類型),而不是格式化成字元串。
- 對于以上類型不滿足,則選擇使用字元串。
- 集合類型,連結清單優先使用 ArrayList,也可以考慮使用 Iterator;哈希優先使用 HashMap;
- 以上情況都不符合要求才選擇自定義對象。
9. 将接口的參數和傳回值原始資料列印到日志中
據觀察,一些開發人員沒有将接口,尤其是 RPC 接口的參數及傳回值列印到日志中。這對定位問題非常不利。說的更直白一點,非常不利于甩鍋。當出了問題,不能第一時間就憑借參數及傳回值順利甩鍋。可能導緻自己花很多時間去排查問題,最後發現是自己依賴的其他系統的問題。
是以,一定要謹記,将接口的參數和傳回值原始資料列印到日志中。D瓜哥憑借這個實踐,在一些客訴及回報中,順利脫身,實作完美甩鍋。
10. 将 RPC 接口的類名及方法列印到日志中
D瓜哥也在嘗試一個實踐:将 RPC 接口的類名和方法,再加上參數或者傳回結果,同時列印到日志中。
這裡為什麼和上面的 将接口的參數和傳回值原始資料列印到日志中 單獨列出來?因為,在這個實踐中,強調的是 “RPC 接口”。相對來說, RPC 接口存在更多容易出錯的問題,經常需要脫離系統去單獨測試 RPC 接口的可用性。把類名就方法名可以更友善在出現問題時,就可以及時根據日志中的資訊,去單獨測試 RPC 的可用性。
11. 核心思想:以人為本,就近原則,觸手可及
洋洋灑灑總結了這麼幾條建議。這裡做一個總結。
對于可維護性建議的一個核心思想就是:以人為本,就近原則,觸手可及。通常來說,人都是有一定的惰性的。如果把飯端到眼前,相信任何正常人無法抗拒美食的誘惑。而這裡提到的一些可維護性的點,就是盡可能照顧人“懶”的特性,在第一次時,就把該做的工作做到位,減少後續人員不必要的麻煩,讓人可以“合法偷懶”。
加油!争取讓更多人可以更好地偷懶。
作者:D瓜哥
内容來源:京東雲開發者社群。未經授權請勿轉載。