
1. 项目概述一个生产级的玩具机器人模拟器最近我完成了一个挺有意思的“玩具机器人模拟器”项目这其实是应聘一家知名药企高级全栈工程师职位时收到的技术测试。虽然听起来像是个简单的编程题但题目要求非常明确“请将其视为生产代码”。这意味着我们不能仅仅满足于功能实现更要考虑代码结构、可维护性、测试覆盖、工程化实践等一整套软件工程流程。这个项目用 React JavaScript 构建核心是模拟一个在 5x5 方格桌面上移动的机器人接收并执行PLACE、MOVE、LEFT、RIGHT、REPORT等指令。我不仅完成了功能还按照真实生产仓库的标准搭建了完整的开发环境、自动化测试、构建部署流水线并将最终成果部署到了 GitHub Pages 上。接下来我会详细拆解这个“简单”项目背后一个资深工程师是如何思考并落地的。2. 项目架构设计与工程化考量接到需求后我的第一反应不是立刻开始写Robot.js而是思考如何搭建一个可持续维护、易于协作的代码库。题目要求“结构应如同设置一个真实的仓库”这直接决定了项目的起点。2.1 技术栈选型与工具链配置项目明确要求使用 React/JavaScript。在这个基础上我选择了最主流、最稳定的工具组合来保证开发效率和代码质量。构建工具Create React App (CRA)。虽然现在 Vite 很火但 CRA 依然是零配置、开箱即用 React 项目的黄金标准它内置了 Webpack、Babel、ESLint 等能让我快速聚焦业务逻辑而非构建配置。这对于限时测试和保证项目基线稳定至关重要。测试框架Jest React Testing Library。这是 React 生态测试的事实标准。Jest 提供测试运行器、断言和模拟功能React Testing Library 则鼓励从用户交互和组件行为的角度进行测试这与“测试用户看到什么而非实现细节”的理念吻合。代码质量ESLint Prettier。CRA 默认集成了 ESLint我在此基础上扩展了规则确保代码风格一致并避免常见错误。Prettier 负责自动格式化让团队协作时没有代码风格的争论。版本控制与部署Git GitHub Actions GitHub Pages。使用 Git 进行完整的提交历史管理题目明确要求看到工作过程。通过 GitHub Actions 设置 CI/CD 流水线实现提交代码后自动运行测试、构建并部署到 GitHub Pages。这模拟了真实生产环境的自动化部署流程。实操心得在技术测试中选择“最稳妥”而非“最新潮”的技术栈往往是上策。评审者更关心你如何运用成熟工具解决问题而非你是否用了最前沿的库。完整的 Git 历史能清晰展示你的思考过程和迭代步骤这比一个完美的、但只有一次提交的仓库更有说服力。2.2 应用核心架构设计模拟器的核心是状态管理和命令解析。我采用了典型的前端分层思想将逻辑与 UI 分离。领域模型层 (Domain Model)这是业务核心。我创建了一个ToyRobot类它完全独立于 React。这个类封装了机器人的状态x,y,facing和行为方法place,move,left,right,report。所有桌面边界校验防止掉落都封装在此。它的设计是纯逻辑的不依赖任何 UI 框架因此其单元测试可以写得非常纯粹和快速。命令解析与服务层 (Service Layer)我创建了一个CommandParser服务。它的职责是读取用户输入的多行文本按行解析识别有效的命令PLACE 0,0,NORTH和参数并调用ToyRobot实例的对应方法。它处理了“在PLACE命令前忽略所有其他命令”等规则。状态管理层 (State Management)对于这个规模的应用React 的useState和useReducer足以胜任。我使用useReducer来管理应用状态因为机器人的一系列动作命令序列本质上是一个状态转换过程useReducer比多个useState更清晰。表示层 (Presentation Layer)即 React 组件。它们职责单一RobotSimulator主容器组件持有ToyRobot实例和CommandParser并通过useReducer管理状态。TableTop用于可视化渲染 5x5 的桌面和机器人位置。CommandInput提供文本框供用户输入命令。OutputDisplay显示REPORT的输出和历史命令结果。ControlButtons提供按钮来触发常见命令序列如示例。这种架构的优势在于可测试性ToyRobot和CommandParser可以完全独立进行单元测试。可维护性业务逻辑变更只需修改领域层UI 改动不影响核心逻辑。清晰度代码职责分明任何新成员都能快速理解数据流向。3. 核心模块实现与难点解析3.1 ToyRobot 领域模型实现这是项目的心脏。我将其实现为一个 ES6 Class。// ToyRobot.js class ToyRobot { constructor() { this.x null; this.y null; this.facing null; // NORTH, EAST, SOUTH, WEST this.tableSize 5; // 5x5 表格 this.isPlaced false; } place(x, y, facing) { // 验证位置是否在桌面内且朝向有效 if (this._isValidPosition(x, y) this._isValidFacing(facing)) { this.x x; this.y y; this.facing facing; this.isPlaced true; return true; } return false; } move() { if (!this.isPlaced) return false; let newX this.x; let newY this.y; switch (this.facing) { case NORTH: newY 1; break; case EAST: newX 1; break; case SOUTH: newY - 1; break; case WEST: newX - 1; break; } // 关键移动前校验新位置是否有效 if (this._isValidPosition(newX, newY)) { this.x newX; this.y newY; return true; } // 无效移动会掉落忽略该命令 return false; } left() { if (!this.isPlaced) return false; const directions [NORTH, WEST, SOUTH, EAST]; // 左转顺序 const currentIndex directions.indexOf(this.facing); this.facing directions[(currentIndex 1) % 4]; return true; } right() { if (!this.isPlaced) return false; const directions [NORTH, EAST, SOUTH, WEST]; // 右转顺序 const currentIndex directions.indexOf(this.facing); this.facing directions[(currentIndex 1) % 4]; return true; } report() { if (!this.isPlaced) return null; return { x: this.x, y: this.y, facing: this.facing }; } // 私有方法通过命名约定如前置下划线 _isValidPosition(x, y) { return x 0 x this.tableSize y 0 y this.tableSize; } _isValidFacing(facing) { return [NORTH, SOUTH, EAST, WEST].includes(facing); } }难点与设计决策状态有效性校验isPlaced标志位至关重要。它确保了在PLACE之前所有其他命令都被安全地忽略返回false或null。校验逻辑集中在place和move方法中符合“失败静默”的要求无效命令被忽略不影响后续。方向旋转的优雅实现使用数组和取模运算来实现左转和右转代码简洁且易于理解。将方向顺序定义为数组旋转就变成了索引的加减操作。坐标系统题目定义原点(0,0)为西南角SOUTH WEST。这意味着y轴正向是北NORTHx轴正向是东EAST。在move()方法中NORTH对应y1SOUTH对应y-1这与常见的数学坐标系一致避免了混淆。3.2 CommandParser 命令解析器解析器的输入是一段文本需要处理换行、空格、大小写等问题。// CommandParser.js class CommandParser { constructor(robot) { this.robot robot; } execute(rawInput) { const lines rawInput.trim().toUpperCase().split(\n); const results []; let hasBeenPlaced false; for (const line of lines) { const trimmedLine line.trim(); if (!trimmedLine) continue; // 跳过空行 const result this._parseAndExecuteSingleCommand(trimmedLine, hasBeenPlaced); results.push({ command: trimmedLine, output: result.output }); // 如果这条命令是一个成功的 PLACE则更新状态 if (result.type PLACE result.success) { hasBeenPlaced true; } } return results; // 返回每条命令的执行结果历史 } _parseAndExecuteSingleCommand(commandStr, robotIsAlreadyPlaced) { if (commandStr.startsWith(PLACE )) { const args commandStr.substring(6).split(,); if (args.length ! 3) return { type: PLACE, success: false, output: Invalid PLACE command format }; const x parseInt(args[0], 10); const y parseInt(args[1], 10); const f args[2].trim(); if (isNaN(x) || isNaN(y)) { return { type: PLACE, success: false, output: Invalid coordinates }; } const success this.robot.place(x, y, f); return { type: PLACE, success, output: success ? Placed at ${x},${y},${f} : Placement failed (out of bounds or invalid direction) }; } // 在首次 PLACE 前忽略所有其他命令 if (!robotIsAlreadyPlaced) { return { type: IGNORED, success: false, output: Ignored (Robot not placed yet) }; } // 解析 MOVE, LEFT, RIGHT, REPORT switch (commandStr) { case MOVE: const moved this.robot.move(); return { type: MOVE, success: moved, output: moved ? Moved one step forward : Move ignored (would fall off) }; case LEFT: this.robot.left(); return { type: LEFT, success: true, output: Turned left }; case RIGHT: this.robot.right(); return { type: RIGHT, success: true, output: Turned right }; case REPORT: const report this.robot.report(); const output report ? Output: ${report.x},${report.y},${report.facing} : Robot is not on the table; return { type: REPORT, success: !!report, output }; default: return { type: UNKNOWN, success: false, output: Invalid command: ${commandStr} }; } } }设计要点状态跟踪解析器内部维护一个hasBeenPlaced标志用于在遍历多行命令时正确执行“首条有效命令必须是 PLACE”的规则。丰富的反馈每条命令的执行结果都返回一个包含类型、成功状态和描述性输出的对象。这比简单的成功/失败布尔值更有用尤其对于 UI 显示和历史记录。健壮性对PLACE命令的参数进行了严格的格式和类型校验防止非法输入导致程序崩溃。3.3 React 组件集成与状态管理在 React 层我使用useReducer来管理整个应用的状态。// AppStateReducer.js const initialState { robot: new ToyRobot(), commandParser: null, // 将在 useEffect 中初始化 commandHistory: [], currentInput: , reportOutput: , }; function appReducer(state, action) { switch (action.type) { case SET_INPUT: return { ...state, currentInput: action.payload }; case EXECUTE_COMMANDS: // 这里调用 commandParser.execute const results state.commandParser.execute(state.currentInput); const lastReport results.findLast(r r.command.includes(REPORT))?.output || ; return { ...state, commandHistory: [...state.commandHistory, ...results], reportOutput: lastReport, currentInput: , // 清空输入框 }; case INIT_PARSER: return { ...state, commandParser: new CommandParser(state.robot) }; // ... 其他 actions default: return state; } }主组件RobotSimulator将状态和dispatch函数传递给子组件。CommandInput组件是一个textarea用于接收用户输入ControlButtons提供预设命令按钮TableTop组件根据robot.x和robot.y动态渲染机器人的位置。UI/UX 考量实时可视化TableTop组件将抽象的坐标转换为视觉化的网格和机器人图标让用户直观地看到每一步命令的效果。命令历史显示所有已执行命令及其结果方便调试和回溯。预设示例提供题目中给出的几个示例按钮用户一键即可加载并运行方便验证功能。4. 测试策略与实现详解将测试视为生产代码的一部分我建立了三层测试体系。4.1 单元测试针对核心逻辑使用 Jest 对ToyRobot和CommandParser进行彻底的单元测试。这是测试金字塔的基石。// ToyRobot.test.js describe(ToyRobot, () { let robot; beforeEach(() { robot new ToyRobot(); }); test(should ignore MOVE before PLACE, () { expect(robot.move()).toBe(false); expect(robot.x).toBeNull(); }); test(PLACE with valid arguments sets position and facing, () { const placed robot.place(0, 0, NORTH); expect(placed).toBe(true); expect(robot.x).toBe(0); expect(robot.y).toBe(0); expect(robot.facing).toBe(NORTH); expect(robot.isPlaced).toBe(true); }); test(PLACE outside table should fail, () { expect(robot.place(5, 2, NORTH)).toBe(false); expect(robot.isPlaced).toBe(false); }); test(MOVE NORTH from (0,0) to (0,1), () { robot.place(0, 0, NORTH); robot.move(); expect(robot.x).toBe(0); expect(robot.y).toBe(1); }); test(MOVE prevents falling off the south edge, () { robot.place(0, 0, SOUTH); const moved robot.move(); // 尝试向南移动会掉落 expect(moved).toBe(false); expect(robot.x).toBe(0); expect(robot.y).toBe(0); // 位置不应改变 }); test(LEFT and RIGHT rotate correctly, () { robot.place(1, 1, NORTH); robot.left(); expect(robot.facing).toBe(WEST); robot.right(); expect(robot.facing).toBe(NORTH); robot.right(); expect(robot.facing).toBe(EAST); }); test(REPORT returns correct state, () { robot.place(3, 4, EAST); const report robot.report(); expect(report).toEqual({ x: 3, y: 4, facing: EAST }); }); test(Multiple sequential commands work as per example (a), () { robot.place(0, 0, NORTH); robot.move(); expect(robot.report()).toEqual({ x: 0, y: 1, facing: NORTH }); }); // ... 更多测试用例覆盖所有示例和边界情况 });CommandParser的测试则侧重于命令字符串的解析、顺序处理和错误反馈。4.2 集成测试验证模块协作集成测试确保ToyRobot和CommandParser能正确协同工作。例如测试一整个命令序列的输入和输出是否与题目示例一致。// integration.test.js describe(Integration: Full command sequence, () { const parser new CommandParser(new ToyRobot()); test(Example (a): PLACE 0,0,NORTH MOVE REPORT, () { const results parser.execute(PLACE 0,0,NORTH MOVE REPORT); const reportResult results.find(r r.command REPORT); expect(reportResult.output).toBe(Output: 0,1,NORTH); }); test(Example (c): Complex sequence, () { const results parser.execute(PLACE 1,2,EAST MOVE MOVE LEFT MOVE REPORT); const reportResult results.find(r r.command REPORT); expect(reportResult.output).toBe(Output: 3,3,NORTH); }); test(Commands before first PLACE are ignored, () { const results parser.execute(MOVE LEFT PLACE 2,2,SOUTH REPORT); const ignoredResults results.filter(r r.output.includes(Ignored)); expect(ignoredResults).toHaveLength(2); // MOVE 和 LEFT 被忽略 const reportResult results.find(r r.command REPORT); expect(reportResult.output).toBe(Output: 2,2,SOUTH); }); });4.3 组件测试确保 UI 行为正确使用 React Testing Library 测试关键组件。例如测试CommandInput组件在用户输入和提交时的行为。// CommandInput.test.js import { render, screen, fireEvent } from testing-library/react; import userEvent from testing-library/user-event; import CommandInput from ./CommandInput; describe(CommandInput, () { const mockOnExecute jest.fn(); beforeEach(() { mockOnExecute.mockClear(); }); test(renders textarea and execute button, () { render(CommandInput onExecute{mockOnExecute} /); expect(screen.getByRole(textbox)).toBeInTheDocument(); expect(screen.getByRole(button, { name: /execute/i })).toBeInTheDocument(); }); test(calls onExecute with input text when button is clicked, async () { const user userEvent.setup(); render(CommandInput onExecute{mockOnExecute} /); const textarea screen.getByRole(textbox); const button screen.getByRole(button, { name: /execute/i }); await user.type(textarea, PLACE 0,0,NORTH\nMOVE); await user.click(button); expect(mockOnExecute).toHaveBeenCalledTimes(1); expect(mockOnExecute).toHaveBeenCalledWith(PLACE 0,0,NORTH\nMOVE); }); test(textarea can be cleared after execution via prop, () { const { rerender } render(CommandInput onExecute{mockOnExecute} currentInputsome text /); expect(screen.getByRole(textbox)).toHaveValue(some text); // 模拟父组件清空输入后重新渲染 rerender(CommandInput onExecute{mockOnExecute} currentInput /); expect(screen.getByRole(textbox)).toHaveValue(); }); });测试覆盖率通过配置 Jest我生成了测试覆盖率报告。目标是核心业务逻辑ToyRobot,CommandParser达到 100% 分支和行覆盖UI 组件覆盖主要交互路径。这为代码重构提供了坚实的安全网。5. 部署与自动化流水线一个完整的生产项目离不开自动化的部署流程。我使用 GitHub Actions 配置了 CI/CD。5.1 GitHub Actions 工作流配置在.github/workflows/deploy.yml中定义工作流name: Deploy to GitHub Pages on: push: branches: [ main ] # 仅在推送到 main 分支时触发 pull_request: # 在 PR 时也运行测试但不部署 branches: [ main ] jobs: test-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: 18 # 使用稳定的 LTS 版本 - name: Install dependencies run: npm ci # 使用 ci 命令确保依赖锁的一致性 - name: Run tests run: npm test -- --coverage --watchAllfalse # 运行测试并生成覆盖率报告 - name: Build project run: npm run build # 只有测试通过才会执行构建 - name: Deploy to GitHub Pages if: github.ref refs/heads/main github.event_name push uses: peaceiris/actions-gh-pagesv3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build # CRA 构建的输出目录这个流水线实现了持续集成 (CI)每次推送或 PR 都会自动运行安装、测试。如果测试失败流程会中止防止有问题的代码被部署。持续部署 (CD)只有当代码推送到main分支且测试通过后才会自动执行npm run build并将构建产物build文件夹推送到gh-pages分支从而更新线上版本。5.2 适配 GitHub Pages由于 GitHub Pages 默认服务于站点根目录而单页应用 (SPA) 通常部署在子目录如username.github.io/repo-name需要处理路由问题。在package.json中设置homepage字段homepage: https://juanpablozunigahidalgo.github.io/astrazeneca-fullstack。这告诉 CRA 在构建时生成正确的静态资源路径。使用 HashRouter在 React 应用中我使用了HashRouter而非BrowserRouter。因为 GitHub Pages 不支持服务端配置BrowserRouter在刷新非根路径的页面时会得到 404 错误。HashRouter利用 URL 的 hash 部分#进行路由兼容性更好。5.3 项目结构与文档一个专业的仓库结构能极大提升可维护性。astrazeneca-fullstack/ ├── public/ # 静态资源 ├── src/ │ ├── components/ # React 组件 (TableTop, CommandInput等) │ ├── core/ # 核心业务逻辑 (ToyRobot, CommandParser) │ ├── hooks/ # 自定义 React Hooks (如有) │ ├── styles/ # 样式文件 │ ├── utils/ # 工具函数 │ ├── App.js # 主应用组件 │ ├── AppStateReducer.js # useReducer 逻辑 │ └── index.js # 应用入口 ├── tests/ │ ├── unit/ # 单元测试 │ └── integration/ # 集成测试 ├── .github/workflows/ # GitHub Actions 配置 ├── package.json ├── README.md # 详细的项目说明、运行指南 └── ... 其他配置文件README.md 内容项目简介和在线演示链接。本地运行指南git clone,npm install,npm start。运行测试指南npm test。技术栈说明。实现的功能和约束说明。设计决策的简要解释。6. 开发回顾与经验总结回顾整个项目从接到需求到最终部署有几个关键点值得分享。1. 从“生产代码”角度出发是制胜关键。很多候选人可能只关注算法实现但忽略了工程化实践。我通过展示完整的 Git 历史、清晰的提交信息、模块化的代码结构、全面的测试覆盖和自动化的部署流程证明了我具备开发可维护、可协作的软件产品的能力。这恰恰是高级工程师与初级工程师的核心区别之一。2. 测试是设计工具而非负担。在编写ToyRobot类之前我实际上先构思了它的公共接口place,move,left,right,report并思考了如何测试它们。测试驱动开发 (TDD) 的思维帮助我设计了更清晰、更松耦合的 API。例如move()方法返回布尔值表示是否成功这个设计最初就是为了便于测试和状态反馈。3. 用户体验 (UX) 即使在小项目中也很重要。虽然题目只要求一个能解析命令的程序但我提供了一个带有可视化网格、命令历史、预设示例按钮的交互式 Web 界面。这超出了预期展示了前端工程师对用户交互的敏感度和实现能力。评审者可以通过点击按钮立即看到效果比单纯阅读代码或运行命令行程序体验好得多。4. 对“简单”需求的深度挖掘。5x5 的桌面机器人听起来很简单但隐藏着许多边界情况初始未放置状态的处理、移动时的边界校验、连续命令的解析、无效命令的优雅处理等。我通过详尽的测试用例覆盖了这些边界并在 UI 上给出了明确的反馈例如“移动被忽略防止掉落”体现了对细节的把握和健壮性编程的思维。5. 工具链的熟练运用是效率的体现。熟练使用 Git 进行特性分支开发、有意义的提交配置 ESLint/Prettier 保证代码风格利用 GitHub Actions 自动化繁琐的部署步骤。这些看似“外围”的技能能显著提升个人和团队的开发效率是资深工程师的标配。最后这个项目最终成功帮助我获得了下一轮的面试机会。面试官反馈说他们不仅看到了正确的功能实现更看到了代码背后的工程思维和职业习惯这正是他们寻找的高级工程师特质。无论你是正在准备类似的技术测试还是希望提升自己的项目实践水平希望这个详细的拆解能给你带来一些启发。记住代码是写给人看的顺便让机器能运行。