Stories

Detail Return Return

代碼複雜度的代價遠比你想象得大 - Stories Detail

引言:複雜度的代價遠比你想象得大

在 Java 後端系統演進過程中,代碼複雜度是影響可維護性、穩定性和迭代效率的核心因素。然而,複雜度往往被忽視,直到一次“小改動”引發線上事故,才被重新審視。

本文以“複雜度戰爭”為主題,系統性地探討如何識別、評估和治理代碼中的複雜性。本文不會停留在抽象原則,而是結合真實案例、Java 代碼示例和可落地的工程實踐,讓你瞭解你應用的代碼複雜度,以及一個優秀的開發同學應該做到的避免代碼”腐爛“的最佳實踐。

讓我們以一些代碼案例引入今天的話題。(文中代碼案例皆為模擬案例)

案例一:圈複雜度過高導致大事故

在某一個大促開始的日子,訂單創建接口在高峯期響應時間飆升,錯誤率突破 XX%。 緊急回滾?沒有最近的發佈記錄。 最終排查日誌發現,數據庫連接池被耗盡,而根源竟是一次兩週前的“微小優化”。

開發同學為了支持一個新的促銷規則,在 OrderService.createOrder() 方法中加了這麼一段邏輯:

if (user.isVip() && order.getTotalAmount().compareTo(BigDecimal.valueOf(100)) > 0) {
    try {
        Discount discount = promotionClient.getDiscount(order);
        if (discount != null && discount.isValid()) {
            order.setFinalPrice(order.getTotalAmount().subtract(discount.getValue()));
        } else {
            order.setFinalPrice(order.getTotalAmount());
        }
    } catch (Exception e) {
        // 靜默失敗,使用原價(開發本意是防崩)
        order.setFinalPrice(order.getTotalAmount());
    }
}

問題來了:這個 catch (Exception e) 不僅吞掉了業務異常,還捕獲了 數據庫連接超時異常(SQLException),導致外層事務未及時中斷,線程持續等待,最終拖垮連接池。

而這個方法本身已有 350 行,嵌套層級達 6 層,圈複雜度高達 38 —— 沒有人意識到,這次“小修”成了壓垮系統的最後一根稻草。

這不是孤例。類似的複雜度事故,正在無數系統中悄然上演。

案例二:重複代碼引發的數據錯亂

支付網關中,簽名計算邏輯在 AlipayProcessorWechatPayProcessor 等 7 個類中重複出現:

String sign = DigestUtils.md5Hex(data + secretKey).toUpperCase();

某天,安全團隊要求升級為 SHA-256,但只改了其中 4 個實現類。剩下的 3 個渠道繼續用 MD5,導致“無效簽名”錯誤激增,影響數萬筆交易。

工具掃描顯示:重複代碼率達 12%,而這些“看起來一樣”的代碼,分散在不同模塊,無人統一維護。

案例三:“上帝類”無人敢動

CRM 系統中的 CustomerManager 類長達 2800 行,承擔着客户創建、積分計算、消息推送、審計日誌、緩存同步等 8 種職責。

更可怕的是,每次調用 updateCustomer(),都會觸發一連串隱式行為:

public void updateCustomer(Customer customer) {
    customerRepo.save(customer);
    
    // 更新積分(即使只是改了個電話)
    rewardService.calculateReward(customer);
    
    // 推送消息(同步阻塞)
    messageQueue.send(buildUpdateMessage(customer));
    
    // 寫審計日誌
    auditLogService.log("UPDATE", customer.getId(), getCurrentUser());
    
    // 刷新緩存
    cacheService.evict("customer:" + customer.getId());
}

新來的工程師想改個字段校驗邏輯,結果測出 5 個副作用 bug。從此,這個類成了團隊心中的“禁區”。

案例四:微服務拆分後更慢了

物流平台將單體拆分為訂單、路由、運力三個服務後,原本本地調用 routeService.findOptimalRoute() 的耗時從 50ms 變成 350ms(含網絡+序列化+重試)。

而最致命的是,當路由服務不穩定時,訂單服務因未配置熔斷,持續重試,反向拖垮整個鏈路。

複雜度沒有消失,只是從“代碼層面”轉移到了“分佈式層面”

這些事件背後,都有一個共同敵人:失控的代碼複雜度

它不像內存泄漏那樣立刻崩潰系統,也不像權限漏洞那樣被安全掃描抓出。它潛伏在每一次“先上線再説”的妥協裏,在每一個沒人敢動的類中,在每一段“還能看懂”的嵌套邏輯中,緩慢侵蝕系統的生命力。

而作為 Java 後端開發者,尤其是架構師,我們必須清醒地認識到:

系統的可維護性,不取決於功能多強大,而取決於它的複雜度是否可控。

在這場看不見硝煙的 複雜度戰爭 中,我們不能靠運氣取勝。我們需要工具來度量它,需要原則來約束它,更需要實戰策略來持續降低它。

接下來,我們將深入探討:

  • 哪些指標能真正衡量代碼複雜度?
  • 如何用合理的工具發現系統中的“複雜度熱點”?
  • 在日常編碼中,如何寫出高質量、低複雜度的 Java 代碼?
  • 架構層面,又該如何從源頭控制複雜度的增長?

代碼複雜度的主流定義

當我們説一段代碼“太複雜”時,往往是一種直覺判斷。但真正的工程實踐需要可量化、可檢測、可改進的指標。所謂“複雜度”,並不是指代碼行數多,而是指理解、維護、修改它的認知成本高

在軟件工程領域,已有多個被廣泛認可的複雜度維度,它們從不同角度揭示代碼的“健康狀況”。

我們將逐一介紹這些指標的含義和實際案例,並按照其作用粒度分為三個層次:方法級、類級、繼承結構級,幫助你係統化地識別和治理複雜度。

1. 圈複雜度(Cyclomatic Complexity)

定義

由 Thomas McCabe 提出,衡量程序中獨立執行路徑的數量。路徑越多,測試難度越大,出錯概率越高。

計算規則:每有一個 ifforwhilecasecatch,複雜度 +1;else 不加分。總分>5 需關注

危害

  • 路徑爆炸 → 難以覆蓋所有分支
  • 異常處理易遺漏
  • 修改風險高,容易引入副作用

實際案例

public BigDecimal calculateFinalPrice(Order order, User user, boolean hasCoupon) {
    BigDecimal total = order.getItems().stream()
        .map(Item::getPrice)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

    if (total.compareTo(BigDecimal.valueOf(100)) > 0) {           // +1
        if (user.isVip()) {                                       // +2
            total = total.multiply(BigDecimal.valueOf(0.9));      // VIP 9折
        } else if (hasCoupon) {                                   // +3
            total = total.subtract(BigDecimal.valueOf(10));       // 減10元
        }
    }

    try {
        Promotion promotion = promotionClient.getActivePromotion(); // +4
        if (promotion != null && promotion.isValid()) {             // +5
            total = total.subtract(promotion.getDiscount());
        }
    } catch (RemoteException e) {                                   // +6
        log.warn("Failed to fetch promotion, using base price");
    }

    return total;
}

該方法圈複雜度 = 6

雖然不算極端,但已接近警戒線(>5 需關注)。若未來增加節日折扣、地區限制等條件,極易突破 10。

改進方向

使用策略模式或規則引擎解耦判斷邏輯,或將促銷計算抽象為獨立服務。

2. 嵌套深度(Nesting Depth)

定義

代碼塊的嵌套層級,如 if 中套 if,再套 fortry。每增加一層,理解成本呈指數上升。。推薦閾值:≤3 層,超過即應重構。

實際案例:“左箭頭綜合徵”

public boolean processRefund(RefundRequest request) {
    if (request != null) {
        Order order = orderService.findById(request.getOrderId());
        if (order != null) {
            if (order.getStatus() == OrderStatus.PAID) {
                PaymentRecord record = paymentService.findByOrder(order);
                if (record != null) {
                    try {
                        RefundResult result = paymentGateway.refund(record);
                        if (result.isSuccess()) {
                            refundRepo.save(new Refund(record, SUCCESS));
                            return true;
                        } else {
                            log.error("Refund failed: {}", result.getMessage());
                            return false;
                        }
                    } catch (PaymentException e) {
                        log.error("Payment system error", e);
                        return false;
                    }
                } else {
                    return false;
                }
            } else {
                return false;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
}

嵌套達 6 層,閲讀需不斷“縮進-回退”,極易漏判條件。

改進方向

使用衞語句(Guard Clauses)提前返回

public boolean processRefund(RefundRequest request) {
    if (request == null) return false;

    Order order = orderService.findById(request.getOrderId());
    if (order == null || order.getStatus() != OrderStatus.PAID) return false;

    PaymentRecord record = paymentService.findByOrder(order);
    if (record == null) return false;

    try {
        RefundResult result = paymentGateway.refund(record);
        if (result.isSuccess()) {
            refundRepo.save(new Refund(record, SUCCESS));
            return true;
        } else {
            log.error("Refund failed: {}", result.getMessage());
            return false;
        }
    } catch (PaymentException e) {
        log.error("Payment system error", e);
        return false;
    }
}

邏輯扁平化,可讀性顯著提升。

3. 方法長度 & 類長度

定義

  • 方法長度:單個方法的代碼行數(不含空行和註釋)
  • 類長度:單個類的總行數

經驗閾值:

  • 方法 ≤ 50 行
  • 類 ≤ 500 行

超出即可能違反 單一職責原則(SRP)

實際案例:上帝方法

// 一個長達 320 行的 createOrder() 方法
// 包含:參數校驗、庫存扣減、價格計算、優惠應用、積分發放、消息推送、日誌記錄、異常重試……
public Order createOrder(CreateOrderRequest request) {
    // ... 320 行混合邏輯 ...
}
  • 無法單元測試所有路徑
  • 任何改動都可能引發未知副作用
  • 新人完全看不懂執行流程

改進方向

public Order createOrder(CreateOrderRequest request) {
    validateRequest(request);                    // 校驗
    InventoryResult inv = inventoryService.deduct(request); // 扣庫存
    PriceCalculation calc = priceEngine.calculate(request); // 算價
    Order order = orderRepo.save(mapToEntity(request, calc)); // 保存
    rewardService.awardPoints(order);            // 發積分
    eventPublisher.publish(new OrderCreatedEvent(order)); // 發事件
    return order;
}

每個步驟獨立,便於替換、測試、監控。

4. 類級複雜度:CK Metrics 四大經典指標

在面向對象系統中,僅看行數和方法數量還不夠。我們需要更精細的指標來評估一個類的設計質量。以下四個指標合稱 CK Metrics Suite(Chidamber & Kemerer),是業界公認的類複雜度評估標準。

(1)WMC(Weighted Methods per Class)

類的方法圈複雜度加權和

  • 含義:一個類中所有方法的圈複雜度之和
  • 示例:若某類有 5 個方法,圈複雜度分別為 6、8、5、12、4,則 WMC = 35
  • 危害:WMC 越高,表示該類整體邏輯密度大,維護和測試成本高
  • 建議閾值:≤45,否則應考慮拆分
WMC 是對“類長度”的深化 —— 它不僅看有多少方法,更關注這些方法有多複雜。

(2)CBO(Coupling Between Object Classes)

類間耦合度

  • 含義:一個類所依賴的外部類的數量
  • 關聯概念:你在“依賴複雜度”一節中提到的 Efferent Coupling(Ce) 本質上就是 CBO
  • 危害:CBO 高 → 耦合強 → 變動牽一髮而動全身,不利於複用
  • 建議閾值:≤7
小結:CBO 和 Efferent Coupling 指標一致,只是術語來源不同。現代工具如 SonarQube 使用後者,但在學術和架構評審中,“CBO”仍是通用説法。

(3)RFC(Response for a Class)

類的響應集

  • 含義:一個類能直接或間接響應的方法總數,包括自身方法 + 它調用的外部方法
  • 示例:OrderService.create() 調用了 paymentService.pay()rewardService.award(),則這兩個調用也計入 RFC
  • 危害:RFC 越大,表示該類的行為影響面越廣,測試組合爆炸,理解成本上升
  • 建議閾值:≤50

(4)LCOM(Lack of Cohesion in Methods)

方法間內聚性缺失

  • 含義:衡量類中方法是否共享相同的字段。如果方法分為幾組,各自操作不同的屬性,則 LCOM 高
class User {
    private String name, email;
    private int loginCount;
    // updateProfile() 只用 name/email
    // incrementLogin() 只用 loginCount
    // → LCOM 高,説明職責不聚焦
}
  • 危害:LCOM 高 → 類缺乏內聚性 → 實際上承擔了多個職責 → 應拆分
  • 改進方向:識別方法訪問的字段簇,按業務邊界進行類拆分

5. 繼承結構複雜度

當系統使用繼承時,還需關注類層次結構本身的複雜性。

(1)DIT(Depth of Inheritance Tree)

繼承樹深度

  • 含義:從當前類到根類的最大路徑長度
  • 示例:Animal → Mammal → Dog,Dog 的 DIT = 2
  • 危害:DIT 越深,行為越難預測(父類邏輯隱式傳遞),調試困難
  • 建議:DIT ≤ 3,過深應考慮改用組合

(2)NOC(Number of Children)

子類數量

  • 含義:一個類的直接子類個數
  • 危害:NOC 過大(如 >10)説明父類抽象不夠通用,或繼承體系設計不合理
  • 改進方向:提取共性接口,或使用策略模式替代繼承

6. 重複代碼率(Duplication)

定義

系統中相同或高度相似代碼塊的比例。違背 DRY(Don't Repeat Yourself)原則。

實際案例:到處複製的簽名邏輯

// 在 AlipayProcessor 中
String sign = DigestUtils.md5Hex(data + apiKey).toUpperCase();

// 在 WechatPayProcessor 中(一模一樣)
String sign = DigestUtils.md5Hex(data + apiKey).toUpperCase();

// 在 UnionpayProcessor 中(還是一樣)
String sign = DigestUtils.md5Hex(data + apiKey).toUpperCase();

改進:提取公共服務

@Component
public class SignatureService {
    public String sign(String data, String key) {
        return DigestUtils.sha256Hex(data + key).toUpperCase();
    }
}

總結

層級 指標 推薦閾值 主要危害
方法級 圈複雜度 ≤10 路徑爆炸,難測試
嵌套深度 ≤3 可讀性差
方法長度 ≤50 行 職責不清
類級 類長度 ≤500 行 上帝類風險
WMC ≤45 整體邏輯密度過高
CBO / Ce ≤7 耦合高,難維護
RFC ≤50 行為氾濫,測試難
LCOM 值越高越差 內聚不足,應拆分
繼承級 DIT ≤3 行為隱式傳遞
NOC 不宜過大 抽象不充分
重複代碼 DRY 不宜過多 不要重複自己

複雜度評估工具

要打贏複雜度戰爭,光靠人工 Code Review 遠遠不夠。我們需要一套自動化的評估體系,在開發、提交、構建、部署的每個環節持續監控代碼質量。

以下是目前 Java 生態中主流的複雜度評估方案與工具框架,它們可以單獨使用,也可集成形成完整的質量門禁體系。

1. SonarQube:行業標準的靜態分析平台

SonarQube 是目前最廣泛使用的代碼質量管理平台,支持對圈複雜度、重複率、代碼壞味、測試覆蓋率等指標進行可視化分析和閾值控制。

核心能力:

  • 自動計算每個方法的圈複雜度,並標記 >10 的熱點
  • 檢測重複代碼塊,支持跨文件識別
  • 提供“技術債”估算:修復所有問題需要多少人天
  • 支持 Quality Gate(質量門禁):CI 中斷機制

集成方式:

<!-- Maven 配置示例 -->
<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>3.9.1.2184</version>
</plugin>

執行掃描:

mvn sonar:sonar \ -Dsonar.projectKey=my-app \ -Dsonar.host.url=http://localhost:9000 \ -Dsonar.login=your-token

推薦規則集:

  • cognitive-complexity:認知複雜度警告
  • nested-if-else-depth:嵌套深度檢測
  • function-complexity:方法複雜度閾值
  • duplicated-blocks:重複代碼告警

2. IntelliJ IDEA 內置分析工具

IntelliJ 提供了強大的本地靜態分析功能,開發者無需離開 IDE 即可發現複雜度問題。

由於 IDEA 迭代很快,使用方式各位開發同學可以自行搜索,

優點:即時反饋,適合在編碼階段預防問題。

3. PMD 與 Checkstyle:輕量級靜態檢查工具

兩者常配合使用,用於 CI/CD 流水線中的自動化檢查。

PMD 特點:

  • 專注代碼結構問題
  • 內建規則:ExcessiveMethodLength, CyclomaticComplexity, NestedIfDepth

具體使用方式不展開描述了,大家可以自行查閲。

4. ArchUnit:架構層面的依賴約束

ArchUnit 允許你用 Java 代碼定義架構規則,防止模塊間非法依賴。

5. GitHub Actions / Jenkins 集成:將複雜度檢查納入 CI

通過 CI 腳本自動運行分析工具,實現“不達標不合並”。

GitHub Actions 示例:

name: Code Quality
on: [push, pull_request]
jobs:
  sonar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
      - name: Run SonarQube Analysis
        run: mvn verify sonar:sonar -Dsonar.qualitygate.wait=true
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

當質量門禁失敗時,PR 將被阻斷,強制開發者先修復問題。

總結

工具 適用場景 關鍵能力
SonarQube 團隊級質量管控 可視化 + 質量門禁
IntelliJ 個人開發階段 實時提示
PMD / Checkstyle CI 自動化檢查 規則驅動
ArchUnit 架構治理 依賴斷言
CI/CD 集成 流程卡點 強制合規

面向低複雜度的代碼最佳實踐

知道什麼是複雜度還不夠,關鍵是如何在日常編碼中主動降低它。本着面向代碼最佳實踐的原則,嘗試總結幾條有效降低代碼複雜的 Best Practise

原則一:單一職責

一個類或方法應該只做一件事。職責越清晰,修改影響面越小。

反例:多功能服務類

@Service
public class OrderService {
    public void createOrder() { /* 創建 */ }
    public void sendNotification() { /* 發送通知 */ }
    public void calculateReward() { /* 計算積分 */ }
    public void logAudit() { /* 寫審計日誌 */ }
}

這個類承擔了訂單生命週期的多個角色,任何變更都可能引發副作用。

改進:按職責拆分

@Service
public class OrderCreationService { ... }

@Service
public class OrderNotificationService { ... }

@Service
public class OrderRewardCalculationService { ... }

職責分離後,各模塊可獨立測試、演進。

原則二:優先組合,而非繼承

繼承容易導致深層類層次結構,增加理解和維護成本。組合更靈活、更可控。

反例:繼承濫用

class BasePaymentProcessor { }
class AlipayProcessor extends BasePaymentProcessor { }
class WechatPayProcessor extends BasePaymentProcessor { }
class HybridAlipayProcessor extends AlipayProcessor { } // 多層繼承

子類隱式繼承父類行為,難以預測執行邏輯。

改進:使用策略模式 + 組合

public interface PaymentStrategy {
    PaymentResult pay(BigDecimal amount);
}

@Service
public class AlipayStrategy implements PaymentStrategy { ... }

@Service
public class WechatPayStrategy implements PaymentStrategy { ... }

// 組合使用
public class UnifiedPaymentService {
    private final Map<String, PaymentStrategy> strategies;

    public UnifiedPaymentService(Map<String, PaymentStrategy> strategies) {
        this.strategies = strategies;
    }

    public PaymentResult pay(String type, BigDecimal amount) {
        return strategies.get(type).pay(amount);
    }
}

解耦清晰,擴展性強。

原則三:善用函數式編程減少狀態污染

Java 8 引入的 OptionalStream 不僅是語法糖,更是對抗複雜度的利器。

反例:消除 null 嵌套判斷

// 傳統寫法:多層 if 判斷
if (user != null) {
    Cart cart = user.getCart();
    if (cart != null) {
        List<Item> items = cart.getItems();
        if (items != null && !items.isEmpty()) {
            return items.stream().map(Item::getPrice).reduce(BigDecimal::add).orElse(ZERO);
        }
    }
}
return ZERO;

改進:改為 Optional 鏈式調用

return Optional.ofNullable(user)
    .map(User::getCart)
    .map(Cart::getItems)
    .filter(items -> !items.isEmpty())
    .flatMap(items -> items.stream().map(Item::getPrice).reduce(BigDecimal::add))
    .orElse(ZERO);

邏輯扁平化,無嵌套,可讀性顯著提升。

原則四:設計模式不是炫技,而是解耦武器

合理使用設計模式可以有效分解複雜邏輯,但切忌過度設計。

反例:if-else

// 反例:一堆 if-else
if ("alipay".equals(type)) {
    return alipayClient.pay(amount);
} else if ("wechat".equals(type)) {
    return wechatClient.pay(amount);
} else if ("unionpay".equals(type)) {
    return unionpayClient.pay(amount);
}

改進: 合理的設計模式

@Component
public class PaymentRouter {
    private final Map<String, PaymentClient> clients;

    public PaymentRouter(List<PaymentClient> clientList) {
        this.clients = clientList.stream()
            .collect(Collectors.toMap(PaymentClient::getType, c -> c));
    }

    public PaymentResult pay(String type, BigDecimal amount) {
        PaymentClient client = clients.get(type);
        if (client == null) throw new UnsupportedPaymentTypeException(type);
        return client.pay(amount);
    }
}

新增支付方式只需實現接口並註冊 Bean,無需修改路由邏輯。

原則五:命名即文檔,好名字勝過千行註釋

變量、方法、類的命名應準確傳達其意圖,避免縮寫和模糊詞彙。

反例:含義不明的數值枚舉

public List<Order> getList(int status) { ... } // status 是什麼?1 表示成功?

改進:明確的枚舉

public List<Order> findOrdersByStatus(OrderStatus status) { ... }

再如:

// 不清楚用途
private boolean flag;

// 明確語義
private boolean isEligibleForDiscount;

清晰的命名能讓代碼自解釋,大幅降低理解成本。

原則六:防禦性編程 + 清晰的錯誤處理

提前攔截非法輸入,明確異常路徑,避免靜默失敗。

正例:使用衞語句提前返回

public Order createOrder(CreateOrderRequest request) {
    if (request == null) {
        throw new IllegalArgumentException("Request cannot be null");
    }
    if (request.getItems() == null || request.getItems().isEmpty()) {
        throw new IllegalArgumentException("Order must have items");
    }
    // 正常邏輯開始……
}

正例:異常不要被吞掉

// 錯誤做法
catch (Exception e) {
    log.warn("Ignore error"); // 靜默吞掉
}

// 正確做法
catch (PaymentTimeoutException e) {
    log.error("Payment system timeout", e);
    throw new OrderCreationFailedException("Payment failed due to timeout", e);
}

確保異常傳播路徑清晰,便於定位問題。

小結:高質量代碼的共同特徵

原則 關鍵動作 效果
單一職責 拆分類與方法 降低變更風險
組合優於繼承 使用接口 + 注入 提升靈活性
函數式思維 使用 Optional/Stream 減少嵌套
設計模式 策略、工廠、責任鏈 解耦複雜邏輯
清晰命名 表達業務意圖 自解釋代碼
防禦性編程 提前校驗 + 明確異常 提高健壯性

這些原則不是教條,而是在長期實踐中總結出的經驗。堅持使用,你會發現自己寫的代碼越來越乾淨,系統也越來越穩健。

總結:堅持做正確的事

我們回顧一下最初的那幾個問題:

  • 一個 catch (Exception e) 真的只是“防崩”嗎?
  • 一段重複的簽名邏輯,值得花幾分鐘複製粘貼嗎?
  • 一個 2800 行的類,真的是“歷史原因”無法改動嗎?

答案從來都不是“代碼本身有多難”,而是我們是否願意為系統的長期健康付出短期成本

優秀的程序員不追求炫技式的“高複雜架構”,而是堅持寫低複雜度、高表達力的代碼。他們知道,可維護性才是系統最核心的非功能需求

工具可以幫助我們發現問題,原則可以指導我們重構代碼,但最終,守護系統整潔的,是每一位工程師對質量的敬畏之心。

user avatar king_wenzhinan Avatar u_17513518 Avatar journey_64224c9377fd5 Avatar u_13529088 Avatar seazhan Avatar AmbitionGarden Avatar u_16769727 Avatar jiangyi Avatar chuanghongdengdeqingwa_eoxet2 Avatar ahahan Avatar yizhidanshendetielian Avatar huangxunhui Avatar
Favorites 30 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.