關於介面可維護性的一些建議

京東雲開發者發表於2023-05-17

作者:京東科技 D瓜哥

在做新需求開發或者相關係統的維護更新時,尤其是涉及到不同系統的介面呼叫時,在可維護性方面,總感覺有很多地方差強人意。一些零星思考,拋磚引玉,希望引發更多的思考和討論。總結了大概有如下幾條建議:

  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瓜哥個人覺得,有兩個處理辦法:

  1. 如果狀態值不是很多,優先在介面引數類或者返回值類中定義。

  2. 如果狀態值很多,可以考慮單獨抽取成一個常量類或者列舉類。

這樣使用的時候,觸手可及。不需要到處去找。

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 協議解釋與實戰” 等文章來看,有幾個原則值得重視的:

  1. 優先使用 boolean 型;

  2. boolean 型滿足不了,次優選擇 int 整型資料;再次可以考慮 long 型;

  3. 日期優先使用內建的日期型別(含 Java Time API 型別),而不是格式化成字串。

  4. 對於以上型別不滿足,則選擇使用字串。

  5. 集合型別,連結串列優先使用 ArrayList,也可以考慮使用 Iterator;雜湊優先使用 HashMap;

  6. 以上情況都不符合要求才選擇自定義物件。

9. 將介面的引數和返回值原始資料列印到日誌中

據觀察,一些開發人員沒有將介面,尤其是 RPC 介面的引數及返回值列印到日誌中。這對定位問題非常不利。說的更直白一點,非常不利於甩鍋。當出了問題,不能第一時間就憑藉引數及返回值順利甩鍋。可能導致自己花很多時間去排查問題,最後發現是自己依賴的其他系統的問題。

所以,一定要謹記,將介面的引數和返回值原始資料列印到日誌中。D瓜哥憑藉這個實踐,在一些客訴及反饋中,順利脫身,實現完美甩鍋。

10. 將 RPC 介面的類名及方法列印到日誌中

D瓜哥也在嘗試一個實踐:將 RPC 介面的類名和方法,再加上引數或者返回結果,同時列印到日誌中。

這裡為什麼和上面的 將介面的引數和返回值原始資料列印到日誌中 單獨列出來?因為,在這個實踐中,強調的是 “RPC 介面”。相對來說, RPC 介面存在更多容易出錯的問題,經常需要脫離系統去單獨測試 RPC 介面的可用性。把類名就方法名可以更方便在出現問題時,就可以及時根據日誌中的資訊,去單獨測試 RPC 的可用性。

11. 核心思想:以人為本,就近原則,觸手可及

洋洋灑灑總結了這麼幾條建議。這裡做一個總結。

對於可維護性建議的一個核心思想就是:以人為本,就近原則,觸手可及。通常來說,人都是有一定的惰性的。如果把飯端到眼前,相信任何正常人無法抗拒美食的誘惑。而這裡提到的一些可維護性的點,就是儘可能照顧人“懶”的特性,在第一次時,就把該做的工作做到位,減少後續人員不必要的麻煩,讓人可以“合法偷懶”。

加油!爭取讓更多人可以更好地偷懶。??????

相關文章