首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

劣質代碼評析:《C語言入門經典》附錄B 21點遊戲(二)—— 函數分解與狀態封裝

     編輯:關於C語言

接續上文:從混亂走向模組化

上一篇指出原代碼的核心問題:全域變數耦合、函數職責不清、遞迴濫用。本文展示重構關鍵步驟:資料封裝與函數解耦。

步驟 1:定義領域結構體

// 牌堆:52 張牌 + 當前牌數
typedef struct {
    int cards[52];
    int num_cards;
} POKER;

// 玩家/莊家狀態:兩種點數計算方式 (A=1 或 11) + 最終有效點數
typedef struct {
    int low;   // A 算 1 點
    int high;  // A 算 11 點
    int score; // 實際採用點數: high<=21 ? high : low
} GAMER;

設計意圖:將分散的陣列 (cards[], playerPoints[], dealerPoints[], total[]) 封裝為語意清晰的類型,消除「魔術索引」(如 ACELOW=0, ACEHIGH=1)。

步驟 2:拆分職責 —— 顯示與計分分離

原始混亂版

void dispCard(int cardDrawn, int points[]) {
    switch(cardDrawn) {
        case 11: puts("Jack"); points[ACELOW]+=10; points[ACEHIGH]+=10; break;
        case 1:  puts("Ace");  points[ACELOW]+=1;  points[ACEHIGH]+=11; break;
        // ...
    }
}

重構後:純顯示 disp() + 純計分 update()

// 只負責輸出牌面名稱
void disp(int card) {
    switch(card) {
        case 1:  puts("Ace");   return;
        case 11: puts("Jack");  return;
        case 12: puts("Queen"); return;
        case 13: puts("King");  return;
        default: printf("%d\n", card); return;
    }
}

// 只負責更新點數結構體
void update(int card, int points[2]) {  // points[0]=low, points[1]=high
    if (card >= 11 && card <= 13) {      // J, Q, K
        points[0] += 10; points[1] += 10;
    } else if (card == 1) {              // Ace
        points[0] += 1;  points[1] += 11;
    } else {                             // 2-10
        points[0] += card; points[1] += card;
    }
}

收益:disp() 可單獨測試/複用;update() 邏輯集中,易修改規則 (如新增 Joker 牌)。

步驟 3:統一抽牌介面 getcard()

// 從牌堆發一張牌,更新玩家狀態,顯示牌面與總分
void getcard(GAMER *p, int card) {
    disp(card);           // 顯示
    update(card, (int[]){p->low, p->high}); // 更新 (需適配結構體)
    // 計算最終有效分數
    p->score = (p->high <= 21) ? p->high : p->low;
    printf("總分:%d\n", p->score);
}

主流程變得極其清晰:

puts("莊家拿牌:"); getcard(&dealer, dealcard(&poker));
puts("你拿牌:");   getcard(&player, dealcard(&poker));

步驟 4:發牌邏輯獨立 dealcard()

// 從牌堆隨機抽一張,用最後一張牌填補空位 (O(1) 移除)
int dealcard(POKER *p) {
    int idx = rand() % p->num_cards;
    int card = p->cards[idx];
    p->cards[idx] = p->cards[--p->num_cards];  // 縮減牌堆
    return card;
}

原代碼 (*numCards)-- 分散在多處,現集中於此,且使用 Fisher-Yates 洗牌思想 (隨機抽取+末尾填補) 保證均勻分布。

步驟 5:玩家回合用迴圈取代遞迴

// 玩家決定要牌/停牌
do {
    getcard(&player, dealcard(&poker));
} while (again("繼續要牌(Y/N)?") == YES);

again() 處理輸入緩衝區,回傳枚舉 YES/NO,型別安全。

步驟 6:莊家策略與勝負判斷

// 莊家策略:點數 < 17 必須要牌
puts("莊家繼續拿牌:");
do {
    getcard(&dealer, dealcard(&poker));
} while (dealer.score < 17);

// 勝負判斷邏輯集中化
void declare_winner(GAMER dealer, GAMER player) {
    if (dealer.score == 21) { puts("你輸了。"); return; }
    if (dealer.score > 21) {
        if (player.score > 21) { puts("平局。"); return; }
        puts("你贏了!\a"); return;
    }
    // dealer < 21
    if (player.score > 21 || dealer.score >= player.score) {
        puts("你輸了。"); return;
    }
    puts("你贏了!\a");
}

所有規則集中在 declare_winner(),易於閱讀、修改、單元測試。

完整重構版架構概覽

main()
  ├─ init_poker(&poker)          // 初始化牌堆
  ├─ srand(time(NULL))           // 隨機種子
  ├─ 遊戲主迴圈
  │    ├─ getcard(&dealer, dealcard(&poker))  // 莊家首張
  │    ├─ getcard(&player, dealcard(&poker))  // 玩家首張
  │    ├─ do { getcard(&player, dealcard(&poker)); } while (again(...))  // 玩家回合
  │    ├─ do { getcard(&dealer, dealcard(&poker)); } while (dealer.score < 17) // 莊家回合
  │    └─ declare_winner(dealer, player)      // 判斷勝負
  └─ again("繼續遊戲?") 控制外層迴圈

重構成果對比

指標原始代碼重構版
全域變數10+ 個0 個 (main 區域變數)
函數數量12 個 (耦合)8 個 (高內聚低耦合)
遞迴深度不受控 (風險)0 (全迴圈)
測試難度極難 (需完整遊戲)易 (每函數可獨立測試)
擴充性幾乎不可能易 (如加入分牌、雙倍下注)

核心原則回顧

  1. 資料與操作綁定:POKER 管牌,GAMER 管分,函數操作結構體指標
  2. 單一職責:disp 只顯示,update 只算分,dealcard 只發牌
  3. 迴圈代替遞迴:遊戲流程用 do-while,堆疊安全、邏輯平坦
  4. 枚舉代替魔術數:YES/NO、ACE_LOW/HIGH (若保留陣列) 增加可讀性

這個重構過程不僅修復了劣質代碼,更展示了「領域建模 → 介面設計 → 流程編排」的軟體工程基本功。下一篇將展示完整可編譯執行的最終代碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved