# 軟體設計 final_project # [Refactor project](https://github.com/simonracz/linux-terminal-game/blob/master) ### 1. 重複的代碼(Duplicated Code) 在 handle_player 函數中,對於上下左右的移動邏輯,幾乎完全相同,只是處理的座標不同。這種重複代碼增加了維護成本。 重構建議: 可以將移動邏輯提取到一個單獨的函數中,這樣就可以避免重複的邏輯。 然後,在 handle_player 函數中,只需要根據鍵盤輸入呼叫這個 move_player 函數。 原來的 handle_player() 變成: game_logic.c ```clike= 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 ```clike= #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 ```clike= #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 ```clike= #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 ```clike= #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 ```clike= #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 語句中。具體表現為: 相同的寶石收集、移動和狀態更新邏輯在多處重複。 例如: ```clike= 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; ``` 影響: 重複代碼增加了維護成本。如果需要修改某個邏輯,例如處理寶石,必須在每個方向邏輯中進行重複修改,容易出現疏漏。 2. 巨大的函數(Large Function) handle_player 包含多層嵌套的 switch 語句,導致: 函數的結構混亂,難以理解每個邏輯分支的具體功能。 嵌套層次過多,降低了代碼可讀性。 影響: 新增功能或調試時需要逐層跟蹤嵌套,容易導致錯誤。 3. 職責不單一(Violation of Single Responsibility Principle, SRP) handle_player 同時處理: 玩家輸入(解析方向)。 移動邏輯(更新屏幕和玩家位置)。 碰撞邏輯(寶石、出口、死亡、推動岩石)。 影響: 功能耦合,測試和擴展都很困難。例如,如果需要更改移動邏輯,會影響整個函數。 4. 缺乏可測試性(Lack of Testability) 嵌套邏輯難以單獨測試特定的場景(例如處理寶石或推動岩石)。 無法單獨測試移動邏輯和輸入處理邏輯。 1. 減少重複代碼 重構後的通用函數: process_move 將所有方向的移動邏輯統一,簡化了代碼結構: ```clike= 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; } } ``` 通用邏輯提取後,減少了方向處理中的冗餘代碼。 2. 簡化主函數邏輯 重構後的 handle_player: ```clike= 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; } } ``` 函數結構更加清晰,僅處理方向輸入和調用通用邏輯。 每個分支對應一個方向,便於維護。 3. 提高職責單一性 handle_player 的職責限定為處理玩家輸入。 具體的移動和碰撞邏輯完全由 process_move 處理,降低了耦合性。 重構後的優勢總結 ![image](https://hackmd.io/_uploads/Hyku52ZUkl.png) test_handle_player.c ```clike= #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 ```clike= #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 ```clike= #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 ```