代碼的味道

要用一種精緻的態度去寫代碼,才能寫出優美而牢固的代碼。java

本文主要從平常代碼中摘錄一些不良的寫法。這些不良的寫法會擾亂清晰的主流程,淹沒重要的業務邏輯,使得代碼語義難以理解和修改。

app

代碼壞味

超長鏈式

超長鏈式的壞處: 1. getXXX() 重複出現; 2. 容易 NPE ;3. 很是醜函數

if (response.getData() != null && CollectionUtils.isNotEmpty(response.getData().getShoppingCartDTOList())) {
      // 出參轉換
      cartList = response.getData().getShoppingCartDTOList().stream().map(CartResponseBuilderV2::buildCartList).collect(Collectors.toList());
    }

寫成下面較好:工具

像 StreamUtil 這樣的工具類應該抽離出來,方便其餘同窗更好地寫代碼。ui

T data = response.getData();
if (data != null && CollectionUtils.isNotEmpty(data.getShoppingCartDTOList())) {
  cartList = StreamUtil.map(data.getShoppingCartDTOList(), CartResponseBuilderV2::buildCartList);
}


不夠的單測

一個很大的入口方法 A ,調用了 B 和 C , 只對 A 作了單測,認爲 B 和 C 天然覆蓋。this

壞處:1. 當 B 和 C 添加新的代碼路徑時,要寫單測很是困難; 2. A 的單測代碼不必定覆蓋 B,C 的全部重要狀況。code

只要 B 和 C 含有比較重要的業務點,尤爲是比較複雜的業務邏輯時,就應該加以單測。ip


未抽離的變量

很容易就順手寫出這樣的代碼: 把全部一坨東西扔到一個語句,就像把全部一坨代碼扔到一個方法裏同樣。ci

long discounts =
                    Optional.ofNullable(orderItemInfo).map(OrderItemInfo::getPrice).orElse(0L)
                    - Optional.ofNullable(orderItemInfo).map(OrderItemInfo::getPcOrderItemPrice)
                        .map(PcOrderItemPrice::getUnitPrice).orElse(0L);

應該把變量抽離處理:get

Long originPrice = Optional.ofNullable(orderItemInfo).map(OrderItemInfo::getPrice).orElse(0L);
Long unitPrice = Optional.ofNullable(orderItemInfo).map(OrderItemInfo::getPcOrderItemPrice)
                        .map(PcOrderItemPrice::getUnitPrice).orElse(0L);
long discounts = originPrice - unitPrice

這種也是暗藏危機:

BatchQuerySnapshotDTO requestParam =
        buildRequestParam(Long.parseLong(orderInfoList.get(0).getShopId()),
                          sameUserVersionOrderMap.keySet(), exportContext);

這裏取 shopId 用了 Long.parseLong(orderInfoList.get(0).getShopId()) 並且還寫在方法調用參數列表裏,這樣有兩個壞處:

  1. orderInfoList 若是爲空列表或者後續被改爲空列表,就會報越界錯誤;
  2. 取值邏輯放在方法參數裏,致使方法調用複雜難看。


未抽離的函數

看下面這段代碼:

private List<Get> buildGets(List<String> rowKeyList, String cf, List<String> columns, List<String> columnPrefixFilters) {
        return StreamUtil.map(
            rowKeyList,
            rowKey -> {
                String rowKeyNotEmpty = (rowKey == null ? "null" : rowKey);
                Get get = new Get(Bytes.toBytes(rowKeyNotEmpty));
                if (columns != null && !columns.isEmpty()) {
                    for (String col: columns) {
                        get.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col));
                    }
                }
                MultipleColumnPrefixFilter multipleColumnPrefixFilter = buildMultipleColumnPrefixFilter(columnPrefixFilters);
                if (multipleColumnPrefixFilter != null) {
                    get.setFilter(multipleColumnPrefixFilter);
                }
                return get;
            });
    }

其中 rowKey -> { // code... } 中的 塊 code 是沒有抽離出來的,這樣寫的話,很容易就忽視對裏面代碼的單測,從而埋伏潛在的BUG。應該將這樣的塊 code 抽離成一個函數,並加以單測。

private List<Get> buildGetsV2(List<String> rowKeyList, String cf, List<String> columns, List<String> columnPrefixFilters) {
        return StreamUtil.map(rowKeyList, rowKey -> buildGet(rowKey, cf, columns, columnPrefixFilters) );
    }

    private Get buildGet(String rowKey, String cf, List<String> columns, List<String> columnPrefixFilters) {
        String rowKeyNotEmpty = (rowKey == null ? "null" : rowKey);
        Get get = new Get(Bytes.toBytes(rowKeyNotEmpty));
        if (columns != null && !columns.isEmpty()) {
            for (String col: columns) {
                get.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col));
            }
        }
        MultipleColumnPrefixFilter multipleColumnPrefixFilter = buildMultipleColumnPrefixFilter(columnPrefixFilters);
        if (multipleColumnPrefixFilter != null) {
            get.setFilter(multipleColumnPrefixFilter);
        }
        return get;
    }


不夠優美

這段代碼並無問題,可是不夠優美。

public Boolean kdtIdsLimit(LimitStageEnum limitStageEnum, Set<Long> kdtIds) {
    for (Long kdtId : kdtIds) {
      Map<String, String> features = Maps.newHashMap();
      features.put("stage", limitStageEnum.getValue());
      features.put("kdt_id", kdtId.toString());
      TrafficLimitDecision decision = programmingTrafficLimitAdapter.apply(features);
      if (decision.shouldLimit()) {
        return true;
      }
    }
    return false;
  }

優美的寫法是什麼 ? 該分離的關注點分離出來,努力尋求最簡潔的方式表達。

這段代碼含有兩個語義:1. 對一個 shopId 判斷是否限流; 2. 若是有一個限流,則全部都要限流。一個是單店鋪的限流判斷的實現,一個是多個店鋪的限流匹配規則。應該分離開。

public Boolean shopIdsLimit(LimitStageEnum limitStageEnum, Set<Long> shopIds) {
    return shopIds.stream().anyMatch(this::shouldLimit);
  }
 
  private boolean shouldLimit(Long shopId) {
    Map<String, String> features = Maps.newHashMap();
    features.put("stage", limitStageEnum.getValue());
    features.put("shop_id", shopId.toString());
    TrafficLimitDecision decision = programmingTrafficLimitAdapter.apply(features);
    return decision.shouldLimit();
  }


複雜邏輯

看下面這段代碼,三個 map 的邏輯很複雜,放在裏面:

壞處:1. 複雜邏輯沒法單測,很容易出 BUG ,很容易 NPE 或 各類異常; 2. 流程邏輯不清晰。

你能看懂這裏面寫的是什麼嗎 ?

private Map<Pair<Long, Integer>, List<String>> aggregateSameUserVersionOrder(
      List<OrderInfo> orderInfoList) {
    Map<Pair<Long, Integer>, List<String>> sameVersionOrderMap = Maps.newHashMap();
 
    orderInfoList.forEach(orderInfo -> Optional.ofNullable(orderInfo.getTcExtra())
        .map(extra -> extra.get(EDU_STUDENT_INFO))
        // extraMap中存的是String
        .map(studentInfoStr -> JsonUtil.readMap((String) studentInfoStr)).map(studentInfo -> {
          Long studentId = (Long) studentInfo.get(STUDENT_ID);
          Integer version = (Integer) studentInfo.get(VERSION);
          if (studentInfo.containsKey(ID_CARD_NO) && studentId != null && version != null) {
            return Pair.of(studentId, version);
          } else {
            return null;
          }
        }).ifPresent(
            idVersionPair -> sameVersionOrderMap
                .computeIfAbsent(idVersionPair, k -> Lists.newArrayList())
                .add(orderInfo.getOrderNo())));
    return sameVersionOrderMap;
  }

這樣繞口的代碼,最好用比較平易的方式表達更佳。 不要濫用語言特性。

private Map<Pair<Long, Integer>, List<String>> aggregateSameUserVersionOrder(List<OrderInfo> orderInfoList) {

    Map<Pair<Long, Integer>, List<String>> sameUserVersionOrder = new HashMap<>();

    for (OrderInfo orderInfo: orderInfoList) {
      Pair<Long, Integer> p = generatePair(orderInfo.getTcExtra());
      if (p == null) { continue; }
      List<String> orderNoList = sameUserVersionOrder.put(p, sameUserVersionOrder.getOrDefault(p, new ArrayList<>());
      orderNoList.add(orderInfo.getOrderNo());
    }
    return sameUserVersionOrder;
  }

  private Pair<Long, Integer> generatePair(Map<String,Object> tcExtra) {
    if (tcExtra == null || !tcExtra.containsKey(EDU_STUDENT_INFO)) {
      return null;
    }

    Map studentInfoMap = JsonUtil.readMap(tcExtra.get(EDU_STUDENT_INFO).toString());
    Long studentId = (Long) studentInfoMap.get(STUDENT_ID);
    Integer version = (Integer) studentInfoMap.get(VERSION);

    if (studentInfoMap.containsKey(ID_CARD_NO) && studentId != null && version != null) {
      return Pair.of(studentId, version);
    } else {
      return null;
    }
  }


小結

代碼是邏輯與表達的藝術。

代碼編寫,就像一門手工藝。要打造出優秀的做品,良好的習慣和細節是關鍵。改掉不良的代碼習慣,堅持清爽整潔之道,才能讓代碼更優美而牢固。

什麼是好的代碼習慣 ? 兩個小原則:

  1. 拆解。凡是有比較複雜的邏輯或關注點,分離細小可複用的關注點, 分離出來做爲變量或函數。

  2. 簡潔。始終追求簡潔易懂的表達。

相關文章
相關標籤/搜索