- 示例工程
【免费下载链接】awesome-low-level-design
Learn Low Level Design (LLD) and prepare for interviews using free resources.
导读
本篇技术指南以awesome-low-level-design仓库中 problems/tic-tac-toe.md 为问题定义主体,系统讲解如何为经典的 3x3 井字棋游戏做面向面试的低级别系统设计(LLD)。文中完整继承需求文档中的 7 条功能需求与 4 个核心类(Player、Board、Game、TicTacToe),并深入仓库中 C++、Java 等多语言实现源码,剖析回合流转、落子校验、胜负判定、平局检测等关键逻辑,以及用 Strategy / State / Observer 模式、Minimax 算法扩展单机 AI 的工程化写法。读完本文,你将掌握从需求分析到类设计再到多语言落地的一整套 LLD 答题范式,可直接迁移到停车场、电梯、象棋等其他常见面试题。
一、需求分析:把一句话需求拆成可验收的规格
原文档 problems/tic-tac-toe.md 给出了 7 条需求,它们是整个设计的验收标准,任何实现都不能偏离:
- 游戏在 3x3 网格上进行;
- 两名玩家轮流在网格上放置自己的符号(X 或 O);
- 率先在横向、纵向或对角线上连成三个己方符号的玩家获胜;
- 网格全部填满且无人获胜时,游戏以平局结束;
- 游戏应提供用户界面来展示网格并允许玩家落子;
- 游戏应管理玩家回合并校验落子是否合法;
- 游戏应在结束时检测并宣布胜者或平局。
其中值得在面试中强调的两点:
- 需求 3 与需求 4 是互斥的终止条件:要么出现胜者,要么棋盘填满,二者必居其一,这是游戏主循环跳出(game over)的判定依据;
- 需求 6 里的“合法”包含两层校验:坐标必须落在 3x3 网格范围内,且该格子必须为空。仓库中 Board.cpp 的
makeMove正是同时执行这两层校验后才落子。
Java 版的问题陈述 solutions/java/src/tictactoe/README.md 将棋盘抽象为 NxN、并补充了“可扩展性:易于修改棋盘尺寸或新增功能”这条需求,这提示我们:面试答题时应把需求归纳为「双人轮流落子」「胜负/平局检测」「输入校验」「可扩展」四个维度去组织类设计。
二、UML 类图:从需求到类的映射
原文档给出的 UML 类图位于 class-diagrams/tictactoe-class-diagram.png,其核心实体可以归纳为 4 个类,职责划分如下:
| 类 | 职责 | 关键方法 |
|---|---|---|
| Player | 表示一名玩家,持有名字与符号(X/O) | getName()、getSymbol() |
| Board | 表示 3x3 棋盘,提供落子、胜负检测、满盘检测 | makeMove()、checkWin()、isFull()、display() |
| Game | 管理游戏流程与玩家交互:回合流转、校验、判定结果 | play()、makeMove()、switchPlayer()、displayResult() |
| TicTacToe | 应用入口,创建玩家与游戏实例并启动 | main() |
这个划分遵循典型的“数据(Board)+ 状态(Game)+ 实体(Player)+ 入口(TicTacToe)”分层:Board 只管网格数据与棋盘规则,不关心谁在下棋;Game 负责编排流程;入口类负责组装。面试时按“谁持有数据、谁驱动流程、谁组织启动”三步即可快速确定类边界。
三、核心类的职责与接口设计
3.1 Player:最小化的玩家实体
Player 只需三个字段:name、symbol('X' 或 'O')、以及标识是否人类玩家的isHuman(为 AI 扩展预留)。仓库 C++ 实现 Player.hpp 与 Player.cpp 通过构造函数默认参数bool isHuman = true提供了单机模式所需的扩展点——把isHuman设为false即得到一个由电脑控制的玩家。
3.2 Board:网格数据与棋盘规则
Board 在 Board.hpp 中声明的接口完整覆盖了需求中“落子、校验、判胜、满盘”四大能力:
isValidPosition(row, col):判断坐标是否在[0, size)范围内;isEmpty(row, col):判断格子是否为空(空位用'-'字符表示);makeMove(row, col, symbol):先校验再落子,失败返回false;isFull():遍历全盘,存在空格即返回false;checkWin(symbol):委托给checkRows / checkColumns / checkDiagonals三个私有方法;display():以 3 列对齐方式打印当前棋盘,即需求 5 中的“用户界面”(控制台版);reset():重置网格,便于复用棋盘实例。
3.3 Game:流程编排者
Game 在 Game.hpp 中维护board、两个玩家指针、currentPlayer与gameOver标志。核心循环play()的逻辑为:展示棋盘 → 若是人类玩家则读入 row/col 并调用makeMove,若是电脑玩家则调用computerMove→ 循环直到gameOver→ 调用displayResult()宣布胜者或平局。
四、胜负判定与平局检测:checkWin 的实现细节
Board 的胜负判定拆成三个方向分别检查,见 Board.cpp:
- 行检查(checkRows):对每一行遍历所有列,若整行都是某符号则胜;
- 列检查(checkColumns):对每一列遍历所有行,逻辑同构于行检查;
- 对角线检查(checkDiagonals):主对角线
grid[i][i],副对角线grid[i][size-1-i]。
checkWin用checkRows(symbol) || checkColumns(symbol) || checkDiagonals(symbol)组合三者。值得注意的工程细节是:仓库实现把循环写成通用size维度而非硬编码 3,这直接呼应了 Java 版 README 中“支持任意 NxN 棋盘”的可扩展性需求。
平局检测在 Game.cpp:落子合法后先查board.checkWin(currentPlayer->getSymbol()),若胜则gameOver = true;否则再查board.isFull(),若满盘则将currentPlayer置空——这一技巧让displayResult()只需判断currentPlayer是否为nullptr即可区分“某人获胜”与“平局”两种终局:
void Game::displayResult() const { board.display(); if (currentPlayer) { std::cout << currentPlayer->getName() << " wins!" << std::endl; } else { std::cout << "It's a draw!" << std::endl; } }这种用「状态语义复用」代替「额外布尔标志」的做法是 LLD 面试中的加分点。
五、回合流转与落子校验:Game 的 makeMove 主链路
回合管理依赖currentPlayer指针的交替:
void Game::switchPlayer() { currentPlayer = (currentPlayer == player1) ? player2 : player1; }makeMove的主链路(Game.cpp)是理解全流程的关键:
board.isValidPosition(row, col)不通过 → 打印 "Invalid position!" 并直接返回(不切换回合);board.isEmpty(row, col)不通过 → 打印 "Position already taken!" 并返回(不切换回合);board.makeMove落子;checkWin成立 → 结束;isFull成立 → 结束(平局);- 否则
switchPlayer()轮到下一位。
注意第 1、2 步失败时都不切换玩家,这保证了“非法落子不消耗回合”的规则正确性,与需求 6 “校验落子确保合法”严格一致。
六、从 3x3 到 NxN:尺寸无关的通用化设计
原需求只要求 3x3,但仓库两种实现都做了通用化处理:
- C++ 版:
Board构造函数带默认参数Board(int size = 3)(见 Board.hpp),所有判定循环都以size为界; - Java 版:Game.java 在构造时
new Board(3)显式指定尺寸,而 solutions/java/src/tictactoe/README.md 将“支持不同棋盘尺寸”列为扩展方向。
这意味着若面试官追问“棋盘改为 4x4、5x5 怎么办”,上述实现只需把落子判定从“三连”推广为“size连”即可,类结构无需任何改动——这正是“把维度参数化”设计带来的扩展收益。
七、工程化扩展:设计模式与 AI 对弈
原文档只给出 4 个基础类,仓库实现则在基础骨架之上展示了更强的工程化扩展,可作为面试时的进阶谈资。
7.1 Java 版:Strategy + State + Observer 三模式组合
Java 实现 solutions/java/src/tictactoe/ 采用了三层设计模式:
- Strategy 模式:把“行胜/列胜/对角胜”拆成三个独立的
WinningStrategy实现(strategy/RowWinningStrategy.java、strategy/ColumnWinningStrategy.java、strategy/DiagonalWinningStrategy.java),Game持有策略列表,checkWinner遍历调用(见 Game.java)。新增“四连”“斜向更长连法”只需加策略类,符合开闭原则; - State 模式:把游戏生命周期建模为
InProgressState、WinnerState、DrawState三个状态(state/),Game.makeMove委托给当前状态的handleMove,终局判定被状态机化; - Observer 模式:
Game继承GameSubject,当状态变为非 IN_PROGRESS 时通知Scoreboard等观察者(Game.java),为后续接 UI、记分板等外部系统提供了松耦合出口。
7.2 C++ 版:Minimax 算法驱动的电脑玩家
C++ 实现把需求“双人”扩展为“人机对战”:Game::initializePlayers("Human", "Computer")中第二个玩家以isHuman=false创建(TicTacToeDemo.cpp)。其 AI 核心是经典的Minimax 极大极小搜索(Game.cpp):
- 评分规则:电脑胜得
10 - depth,人类胜得depth - 10,满盘平局为 0(depth越小越优先,追求速胜、避免拖局); isMax层取最大值(电脑视角),isMin层取最小值(人类视角);- 搜索通过拷贝棋盘
Board tempBoard = board进行假设性落子,不污染真实棋盘; findBestMove()遍历每个空位,模拟落子后用minimax求分,取最高分位置落子(Game.cpp)。
对 3x3 棋盘,Minimax 全搜索即可保证电脑不输(最佳应对),是面试中“如何让机器不犯错”的标准答案。可以顺便指出:更大的棋盘(如 5x5)需要 Alpha-Beta 剪枝或蒙特卡洛方法,这能体现你对算法复杂度的意识。
八、多语言实现导航与运行方式
该题在仓库中提供了 5 种语言的完整实现,可直接对照阅读同一份设计的差异化落地:
| 语言 | 位置 | 入口/说明 |
|---|---|---|
| Java | solutions/java/src/tictactoe/ | TicTacToeDemo.java演示,Game/Board/Player/Cell分包组织,README 含类设计与示例代码 |
| C++ | solutions/cpp/tictactoe/ | TicTacToeDemo.cpp为入口,含人机对战与 Minimax AI |
| C# | solutions/csharp/tictactoe/ | 与 C++ 同套类设计的 .NET 版本 |
| Go | solutions/golang/tictactoe/ | 以 Go 结构体与接口组织的版本 |
| Python | solutions/python/tictactoe/ | Python 面向对象版本 |
C++ 版可直接编译运行体验完整人机对战流程:
# 在解决方案目录下编译并运行(需 C++17 以支持结构化绑定) g++ -std=c++17 TicTacToeDemo.cpp Board.cpp Game.cpp Player.cpp -o tictactoe ./tictactoe运行后程序会打印棋盘(空位显示-),提示人类玩家输入row与column(取值 0~2),电脑玩家则由 Minimax 自动落子,最终输出胜者或It's a draw!。Java 版则参考其 README 中的示例用法:
Player p1 = new Player("Alice", Symbol.X); Player p2 = new Player("Bob", Symbol.O); Game game = new Game(p1, p2); game.play();九、面试答题要点总结
结合需求文档与仓库实现,回答本题时的关键得分点归纳如下:
- 先定验收标准再画类图:7 条需求逐条映射到类的哪个方法,做到“需求可追踪”;
- 明确职责边界:Board 管数据与规则,Game 管流程,Player 是纯实体,入口类只做组装;
- 讲清两种终局:checkWin 三方向判定 + isFull 满盘平局,注意“非法落子不换回合”的细节;
- 主动展示扩展性:棋盘尺寸参数化(NxN)、Java 的 Strategy/State/Observer 组合、C++ 的 Minimax 人机对战,都是把“能跑”升级为“好扩展”的证明;
- 多语言对照:能指出 5 种语言的实现位置并对比其组织方式,说明你不仅会设计,还能工程化落地。
这套“需求 → 类职责划分 → 核心算法 → 设计模式扩展 → 多语言落地”的流程,可以直接套用到仓库中 problems/ 下的停车场、电梯、象棋、餐厅管理等同类 LLD 题目,是面试准备的通用方法论。
- 示例工程
【免费下载链接】awesome-low-level-design
Learn Low Level Design (LLD) and prepare for interviews using free resources.
相关推荐
wewe-rss 私有化部署实战:把微信公众号变成 RSS 订阅源的完整指南
wewe rss 私有化部署实战:把微信公众号变成 RSS 订阅源的完整指南 wewe rss 是一个可以私有化部署的微信公众号 RSS 生成服务。它借助微信读
示例工程从零设计井字棋(Tic Tac Toe):基于 C 的状态机、策略与观察者模式实战剖析
从零设计井字棋(Tic Tac Toe):基于 C 的状态机、策略与观察者模式实战剖析 井字棋(Tic Tac Toe)是低层设计(LLD)面试中最经典的入门题
示例工程python-mini-projects 双人井字棋(Tic Tac Toe)命令行游戏:运行方式与源码实现解析
python mini projects 双人井字棋(Tic Tac Toe)命令行游戏:运行方式与源码实现解析 本文以 python mini project
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考