Try   HackMD

軟體設計 final_project

Refactor project

1. 重複的代碼(Duplicated Code)

在 handle_player 函數中,對於上下左右的移動邏輯,幾乎完全相同,只是處理的座標不同。這種重複代碼增加了維護成本。
重構建議:
可以將移動邏輯提取到一個單獨的函數中,這樣就可以避免重複的邏輯。

然後,在 handle_player 函數中,只需要根據鍵盤輸入呼叫這個 move_player 函數。

原來的 handle_player() 變成:
game_logic.c

void move_player(GameState* state, int delta_x, int delta_y) { char next_cell = state->screen[state->pos_y + delta_y][state->pos_x + delta_x]; switch (next_cell) { case '$': // 寶石 state->gems_collected++; /* fall through */ case ' ': // 空地 case '.': // 泥土 state->screen[state->pos_y][state->pos_x] = ' '; state->screen[state->pos_y + delta_y][state->pos_x + delta_x] = '@'; state->pos_x += delta_x; state->pos_y += delta_y; break; case 'E': // 出口 state->won = 1; state->screen[state->pos_y][state->pos_x] = ' '; state->screen[state->pos_y + delta_y][state->pos_x + delta_x] = '@'; state->pos_x += delta_x; state->pos_y += delta_y; break; case 'o': // 落石 case 'S': // 掉落寶石 state->dead = 1; break; default: break; } } void handle_player(GameState* state) { switch (state->key) { case 1: move_player(state, 0, -1); break; // 上 case 2: move_player(state, 0, 1); break; // 下 case 3: move_player(state, 1, 0); break; // 右 case 4: move_player(state, -1, 0); break; // 左 default: break; } }

game_logic.h

#ifndef GAME_LOGIC_H #define GAME_LOGIC_H // 遊戲狀態結構體 typedef struct { int key; int pos_x; int pos_y; unsigned int count; int gems_collected; int dead; int won; char old_screen[26][60]; // 預設最大大小 char screen[26][60]; } GameState; // 遊戲邏輯相關函數 void move_player(GameState* state, int delta_x, int delta_y); void handle_player(GameState* state); void update_all_elements(GameState* state); void update(GameState* state); #endif // GAME_LOGIC_H

test_move_player.c

#include <assert.h> #include <stdio.h> #include <string.h> #include "../game_logic.h" void test_move_player() { // 初始化遊戲狀態 GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', '.', '.', '.', 'X'}, {'X', '.', '@', '$', 'X'}, {'X', '.', '.', 'E', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, .pos_x = 2, // 玩家初始位置 // 第幾col .pos_y = 2, // 第幾row }; // 測試向右移動到寶石 move_player(&state, 1, 0); assert(state.pos_x == 3); // 玩家應移動到 (2, 3) assert(state.pos_y == 2); assert(state.gems_collected == 1); // 應收集到 1 顆寶石 assert(state.screen[2][2] == ' '); // 原位置應變為空 assert(state.screen[2][3] == '@'); // 新位置為玩家 printf("move right correct!\n"); // 測試向下移動到出口 move_player(&state, 0, 1); assert(state.pos_x == 3); // 玩家應移動到 (3, 3) assert(state.pos_y == 3); assert(state.won == 1); // 玩家應達到勝利條件 assert(state.screen[3][3] == '@'); // 新位置為玩家 printf("test_move_player passed!\n"); } int main() { test_move_player(); // 測試移動功能 return 0; }

巨大的函數(Large Function)

update_all_elements 函數處理了多個不同的遊戲元素,包括處理岩石、寶石以及玩家角色。這樣的函數過於龐大,且包含了過多的邏輯。

為何需要重構:
函數過大,難以維護和理解。
使代碼缺乏清晰的模組化,當添加新功能時很容易出現錯誤。
原update_all_elements()問題
職責不單一:update_all_elements 涵蓋了多個邏輯:
處理靜止岩石和寶石(handle_rocks_gems)。
處理下落岩石和寶石(handle_falling_rocks_gems)。
測試困難:無法單獨測試岩石/寶石邏輯。
可讀性差:每個條件分支(case)隱藏了重要邏輯,難以擴展或調試。
重構建議:
將每一類別的處理邏輯提取到單獨的函數中。例如,處理岩石和寶石的邏輯可以單獨放到 handle_rocks 和 handle_falling_rocks 函數中。

game_logic.c

#include <stdio.h> #include "game_logic.h" #define MAX_X 60 #define MAX_Y 26 // 元素處理器表 typedef void (*ElementHandler)(GameState* state, int x, int y); ElementHandler element_handlers[128]; // 初始化元素處理器 void initialize_element_handlers() { for (int i = 0; i < 128; i++) { element_handlers[i] = NULL; } element_handlers['O'] = handle_rocks; element_handlers['$'] = handle_rocks; element_handlers['o'] = handle_falling_rocks; element_handlers['S'] = handle_falling_rocks; } // x: col, y: row // 處理靜止的岩石和寶石 void handle_rocks(GameState* state, int x, int y) { int is_gem = (state->screen[y][x] == '$'); if (state->screen[y + 1][x] == ' ') { state->screen[y][x] = is_gem ? 'S' : 'o'; } } // 處理下落的岩石和寶石 void handle_falling_rocks(GameState* state, int x, int y) { int is_gem = (state->screen[y][x] == 'S'); if (state->screen[y + 1][x] == ' ') { state->screen[y][x] = ' '; state->screen[y + 1][x] = is_gem ? 'S' : 'o'; } else if (state->screen[y + 1][x] == '@') { state->dead = 1; } } // 遍歷屏幕元素 void for_each_screen_element(GameState* state, void (*callback)(GameState*, int, int)) { for (int j = MAX_Y - 1; j >= 0; --j) { for (int i = MAX_X - 1; i >= 0; --i) { callback(state, i, j); } } } // 處理單個屏幕元素 void process_element(GameState* state, int x, int y) { char element = state->screen[y][x]; if (element_handlers[element]) { element_handlers[element](state, x, y); } } // 更新所有元素 void update_all_elements(GameState* state) { for_each_screen_element(state, process_element); }

game_logic.h

#ifndef GAME_LOGIC_H #define GAME_LOGIC_H // 遊戲狀態結構 typedef struct { int key; // 玩家輸入 int pos_x, pos_y; // 玩家位置 unsigned int count; // 更新次數 int gems_collected; // 收集的寶石數 int dead; // 玩家是否死亡 int won; // 玩家是否贏得遊戲 char old_screen[26][60]; // 上一幀畫面 char screen[26][60]; // 當前畫面 } GameState; // 遊戲邏輯函數 void initialize_element_handlers(); void handle_rocks(GameState* state, int x, int y); void handle_falling_rocks(GameState* state, int x, int y); void for_each_screen_element(GameState* state, void (*callback)(GameState*, int, int)); void process_element(GameState* state, int x, int y); void update_all_elements(GameState* state); #endif // GAME_LOGIC_H

test_elements.c

#include <assert.h> #include <stdio.h> #include "../game_logic.h" // 測試靜止岩石處理 void test_handle_rocks() { GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', ' ', 'O', ' ', 'X'}, {'X', ' ', ' ', ' ', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, }; handle_rocks(&state, 2, 1); assert(state.screen[1][2] == 'o'); printf("test_handle_rocks passed!\n"); } // 測試下落岩石處理 void test_handle_falling_rocks() { GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', ' ', 'o', 'o', 'X'}, {'X', ' ', ' ', '@', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, .dead = 0, }; handle_falling_rocks(&state, 2, 1); assert(state.screen[1][2] == ' '); assert(state.screen[2][2] == 'o'); assert(state.dead == 0); handle_falling_rocks(&state, 3, 1); assert(state.dead == 1); printf("test_handle_falling_rocks passed!\n"); } // 測試更新所有元素 void test_update_all_elements() { GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', ' ', '$', 'o', 'X'}, {'X', ' ', ' ', '@', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, .dead = 0, }; initialize_element_handlers(); update_all_elements(&state); assert(state.screen[1][2] == 'S'); assert(state.screen[1][3] == 'o'); assert(state.dead == 1); printf("test_update_all_elements passed!\n"); } int main() { test_handle_rocks(); test_handle_falling_rocks(); test_update_all_elements(); return 0; }

清晰的結構:
每個元素的邏輯處理被封裝到特定函數中,便於維護。
高效的對應:
使用 ASCII 字元作為索引,查找對應處理函數的時間複雜度為 O(1)。
擴展性:
如果新增遊戲元素,只需將對應字元和函數添加到 initialize_element_handlers()。

重複代碼(Duplicated Code)
每個方向(上、下、左、右)的邏輯幾乎相同,但分別嵌套在多個 case 語句中。具體表現為:

相同的寶石收集、移動和狀態更新邏輯在多處重複。
例如:

case 1: // UP switch (state->screen[state->pos_y - 1][state->pos_x]) { case '$': state->gems_collected++; // fallthrough case ' ': case '.': state->screen[state->pos_y][state->pos_x] = ' '; state->screen[state->pos_y - 1][state->pos_x] = '@'; --state->pos_y; break; // ... } break;

影響:

重複代碼增加了維護成本。如果需要修改某個邏輯,例如處理寶石,必須在每個方向邏輯中進行重複修改,容易出現疏漏。

  1. 巨大的函數(Large Function)
    handle_player 包含多層嵌套的 switch 語句,導致:

函數的結構混亂,難以理解每個邏輯分支的具體功能。
嵌套層次過多,降低了代碼可讀性。
影響:

新增功能或調試時需要逐層跟蹤嵌套,容易導致錯誤。

  1. 職責不單一(Violation of Single Responsibility Principle, SRP)
    handle_player 同時處理:

玩家輸入(解析方向)。
移動邏輯(更新屏幕和玩家位置)。
碰撞邏輯(寶石、出口、死亡、推動岩石)。
影響:

功能耦合,測試和擴展都很困難。例如,如果需要更改移動邏輯,會影響整個函數。

  1. 缺乏可測試性(Lack of Testability)
    嵌套邏輯難以單獨測試特定的場景(例如處理寶石或推動岩石)。
    無法單獨測試移動邏輯和輸入處理邏輯。

  2. 減少重複代碼
    重構後的通用函數: process_move 將所有方向的移動邏輯統一,簡化了代碼結構:

void process_move(GameState* state, int dx, int dy) { int target_x = state->pos_x + dx; int target_y = state->pos_y + dy; char target_cell = state->screen[target_y][target_x]; switch (target_cell) { case '$': // 收集寶石 state->gems_collected++; case ' ': // 空格 case '.': // 泥土 state->screen[state->pos_y][state->pos_x] = ' '; state->screen[target_y][target_x] = '@'; state->pos_x = target_x; state->pos_y = target_y; break; case 'E': // 出口 state->won = 1; break; case 'o': // 下落岩石 case 'S': // 下落寶石 state->dead = 1; break; case 'O': // 推動岩石 if (state->screen[target_y][target_x + dx] == ' ') { state->screen[target_y][target_x + dx] = 'O'; state->screen[state->pos_y][state->pos_x] = ' '; state->screen[target_y][target_x] = '@'; state->pos_x = target_x; state->pos_y = target_y; } break; default: break; } }

通用邏輯提取後,減少了方向處理中的冗餘代碼。

  1. 簡化主函數邏輯
    重構後的 handle_player:
void handle_player(GameState* state) { switch (state->key) { case 1: process_move(state, 0, -1); break; // 上 case 2: process_move(state, 0, 1); break; // 下 case 3: process_move(state, 1, 0); break; // 右 case 4: process_move(state, -1, 0); break; // 左 default: break; } }

函數結構更加清晰,僅處理方向輸入和調用通用邏輯。
每個分支對應一個方向,便於維護。

  1. 提高職責單一性
    handle_player 的職責限定為處理玩家輸入。
    具體的移動和碰撞邏輯完全由 process_move 處理,降低了耦合性。

重構後的優勢總結
image

test_handle_player.c

#include <assert.h> #include <stdio.h> #include "../game_logic.h" // 測試處理移動 void test_process_move() { GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', '.', '.', '.', 'X'}, {'X', '.', '@', '$', 'X'}, {'X', '.', '.', 'E', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, .pos_x = 2, .pos_y = 2, }; process_move(&state, 1, 0); // 向右移動 assert(state.pos_x == 3); assert(state.pos_y == 2); assert(state.gems_collected == 1); assert(state.screen[2][2] == ' '); assert(state.screen[2][3] == '@'); printf("move right correct!\n"); process_move(&state, 0, 1); // 向下移動到出口 assert(state.pos_x == 3); assert(state.pos_y == 3); assert(state.won == 1); assert(state.screen[3][3] == '@'); printf("test_process_move passed!\n"); } // 測試處理玩家輸入 void test_handle_player() { GameState state = { .screen = { {'X', 'X', 'X', 'X', 'X'}, {'X', '.', '.', '.', 'X'}, {'X', '.', '@', '$', 'X'}, {'X', '.', '.', 'E', 'X'}, {'X', 'X', 'X', 'X', 'X'}, }, .pos_x = 2, .pos_y = 2, .key = 3, // 向右 }; handle_player(&state); assert(state.pos_x == 3); assert(state.pos_y == 2); assert(state.gems_collected == 1); assert(state.screen[2][2] == ' '); assert(state.screen[2][3] == '@'); state.key = 2; // 向下 handle_player(&state); assert(state.pos_x == 3); assert(state.pos_y == 3); assert(state.won == 1); assert(state.screen[3][3] == '@'); printf("test_handle_player passed!\n"); } int main() { test_process_move(); test_handle_player(); return 0; }

game_logic.c

#include "game_logic.h" // 處理玩家移動的共用邏輯 void process_move(GameState* state, int dx, int dy) { int target_x = state->pos_x + dx; int target_y = state->pos_y + dy; char target_cell = state->screen[target_y][target_x]; switch (target_cell) { case '$': // 收集寶石 state->gems_collected++; /* fall through */ case ' ': // 空格 case '.': // 泥土 state->screen[state->pos_y][state->pos_x] = ' '; state->screen[target_y][target_x] = '@'; state->pos_x = target_x; state->pos_y = target_y; break; case 'E': // 出口 state->screen[state->pos_y][state->pos_x] = ' '; state->screen[target_y][target_x] = '@'; state->pos_x = target_x; state->pos_y = target_y; state->won = 1; break; case 'o': // 下落岩石 case 'S': // 下落寶石 state->dead = 1; break; case 'O': // 推動岩石 if (state->screen[target_y][target_x + dx] == ' ') { state->screen[target_y][target_x + dx] = 'O'; state->screen[state->pos_y][state->pos_x] = ' '; state->screen[target_y][target_x] = '@'; state->pos_x = target_x; state->pos_y = target_y; } break; default: break; } } // 處理玩家輸入 void handle_player(GameState* state) { switch (state->key) { case 1: process_move(state, 0, -1); break; // 上 case 2: process_move(state, 0, 1); break; // 下 case 3: process_move(state, 1, 0); break; // 右 case 4: process_move(state, -1, 0); break; // 左 default: break; } }

game_logic.h

#ifndef GAME_LOGIC_H #define GAME_LOGIC_H // 遊戲狀態結構 typedef struct { int key; // 玩家輸入 int pos_x, pos_y; // 玩家位置 unsigned int count; // 更新次數 int gems_collected; // 收集的寶石數 int dead; // 玩家是否死亡 int won; // 玩家是否贏得遊戲 char old_screen[26][60]; // 上一幀畫面 char screen[26][60]; // 當前畫面 } GameState; // 遊戲邏輯函數 void initialize_element_handlers(); void handle_rocks(GameState* state, int x, int y); void handle_falling_rocks(GameState* state, int x, int y); void for_each_screen_element(GameState* state, void (*callback)(GameState*, int, int)); void process_element(GameState* state, int x, int y); void update_all_elements(GameState* state); void process_move(GameState* state, int dx, int dy); void handle_player(GameState* state); #endif // GAME_LOGIC_H