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

劣質代碼剖析:《C語言入門經典》附錄B 21點遊戲(四)—— 從面條式代碼到模組化重構

     編輯:關於C語言

前言

《寫給大家看的C語言書》雖為入門暢銷書,但其附錄 B 的「21點撲克遊戲」卻是典型的教學反面教材:全域變數氾濫、函數耦合極高、邏輯與顯示混雜、無錯誤處理、隨機數種子重複設定等問題集大成。本系列文章將逐步剖析其缺陷,並展示現代 C 語言重構思路。本文為第四部分,聚焦於主控流程解耦與函數介面規範化。

一、原始代碼核心問題盤點

  1. 全域狀態泛濫:`cards[]`、`playerPoints[]`、`dealerPoints[]`、`total[]` 等陣列全為全域,任意函數可任意修改,導致狀態追蹤極難。
  2. 函數職責不清:`playerGetsCard` 同時負責發牌、計分、顯示、用戶交互,違反單一職責原則。
  3. 控制流程硬編碼:主函數 `main` 中混雜著「莊家發牌→玩家發牌→玩家決定→莊家決定→判斷勝負」的完整流程,無法複用、難以測試。
  4. 隨機數誤用:在迴圈內重複呼叫 `srand(time(NULL))`,若迴圈執行極快,種子相同導致產生相同隨機序列。
  5. 魔術數字與硬編碼:`DEALER 0`、`PLAYER 1`、`ACELOW 0`、`ACEHIGH 1` 等常數分散定義,缺乏語義分組。

二、重構步驟:建立領域模型

1. 定義強類型常數與結構體

// constants.h
typedef enum { ROLE_DEALER = 0, ROLE_PLAYER = 1 } Role;
typedef enum { ACE_LOW = 0, ACE_HIGH = 1 } AceMode;
typedef enum { NO = 0, YES = 1 } Bool;

// game_types.h
#define MAX_CARDS 52
#define MAX_HAND  11  // 21點最多11張牌 (11個A)

typedef struct {
    int cards[MAX_CARDS];
    int count;
} Deck;

typedef struct {
    int points[2];   // [ACE_LOW], [ACE_HIGH]
    int total;       // 最終有效點數
} HandScore;

typedef struct {
    Deck deck;
    HandScore dealer;
    HandScore player;
    Bool  game_over;
} GameState;

2. 封裝核心操作為純函數

// 發牌:從牌堆取一張,更新手牌分數,返回牌面值
int deal_card(Deck *deck, HandScore *hand) {
    if (deck->count == 0) return -1; // 牌發完
    int idx = rand() % deck->count;
    int card = deck->cards[idx];
    deck->cards[idx] = deck->cards[--deck->count]; // 交換移除 O(1)

    // 更新分數 (Ace 雙計分邏輯內聚於此)
    if (card == 1) {       // Ace
        hand->points[ACE_LOW]  += 1;
        hand->points[ACE_HIGH] += 11;
    } else if (card >= 11) { // J/Q/K
        hand->points[ACE_LOW]  += 10;
        hand->points[ACE_HIGH] += 10;
    } else {
        hand->points[ACE_LOW]  += card;
        hand->points[ACE_HIGH] += card;
    }
    // 計算有效總分
    hand->total = (hand->points[ACE_HIGH] <= 21) ? hand->points[ACE_HIGH] : hand->points[ACE_LOW];
    return card;
}

// 判斷勝負:純邏輯,無 I/O
typedef enum { RESULT_DEALER_WIN, RESULT_PLAYER_WIN, RESULT_PUSH } GameResult;

GameResult judge_winner(const HandScore *dealer, const HandScore *player) {
    if (dealer->total == 21) return RESULT_DEALER_WIN; // 莊家黑傑克
    if (dealer->total > 21) {
        return (player->total > 21) ? RESULT_PUSH : RESULT_PLAYER_WIN;
    }
    if (player->total > 21) return RESULT_DEALER_WIN;
    if (dealer->total >= player->total) return RESULT_DEALER_WIN;
    return RESULT_PLAYER_WIN;
}

三、主流程解耦:狀態機驅動

// game_flow.c
void play_round(GameState *g) {
    init_deck(&g->deck);
    reset_hand(&g->dealer);
    reset_hand(&g->player);

    // 初始發兩張牌 (交替)
    deal_card(&g->deck, &g->dealer);
    deal_card(&g->deck, &g->player);
    deal_card(&g->deck, &g->dealer);
    deal_card(&g->deck, &g->player);

    // 玩家回合
    while (!g->game_over && g->player.total < 21) {
        display_status(g);
        if (ask_hit_or_stand() == 'S') break;
        deal_card(&g->deck, &g->player);
    }

    // 莊家回合 (規則固定:<17 必補)
    while (!g->game_over && g->dealer.total < 17) {
        deal_card(&g->deck, &g->dealer);
    }

    // 結算
    g->game_over = YES;
    display_final(g);
    GameResult res = judge_winner(&g->dealer, &g->player);
    announce_result(res);
}

四、關鍵改進點總結

原始缺陷重構方案收益
全域變數封裝於 `GameState` 結構體,傳遞指標狀態可見性明確,支援多局並行、易於序列化/測試
發牌/計分/顯示混雜拆分為 `deal_card` (純邏輯) + `display_*` (純 I/O)邏輯可單元測試,介面可替換 (CLI/GUI/Web)
主流程硬編碼狀態機 `play_round` 驅動流程清晰,易擴展 (如加入分牌、雙倍下注、保險)
`srand` 重複呼叫程式啟動時 **唯一** 呼叫 `srand((unsigned)time(NULL))`保證隨機序列品質
魔術數字枚舉/常數集中定義於頭檔語義自文檔化,修改單點生效

五、延伸思考:如何進一步提升?

  • 引入錯誤碼/斷言:`deal_card` 返回 `-1` 表示牌堆空,上層需處理。
  • 抽象 I/O 介面:定義 `GameUI` 結構體含函數指標 (`show_card`, `prompt_action`...),實現 CLI/GTK/網頁前端熱插拔。
  • 單元測試覆蓋:針對 `judge_winner`、`deal_card` 邊界條件 (A+10=21、多張 A 計分、爆牌) 編寫測試用例。
  • 資料驅動規則:將「莊家補牌規則 <17」提取為配置參數,支援不同賭場規則變體。

重構不僅是「整理代碼」,更是將隱性業務知識顯性化、將耦合關係解耦合的過程。從這個 21 點遊戲的演進中,我們看到:良好的 C 程式設計同樣能實現高內聚、低耦合、可測試、可維護的架構 —— 關鍵在於嚴謹的資料建模與函數契約設計。

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