实现Apollo服务器端井字棋游戏逻辑的设计模式咨询
问题
我正在学习GraphQL与Apollo,开发一款井字棋游戏。目前服务器存储所有游戏信息,客户端点击格子时会向服务器发送mutation来更新GameState类型并返回新状态。我想让服务器处理所有游戏逻辑,包括检查移动是否合法、执行电脑走棋、判断胜负,之后返回新的GameState。
我原本打算把所有逻辑都放进mutation解析器,但教程强烈建议保持解析器尽可能精简,这似乎不是最佳设计。但我想不到其他实现方式,想问下Apollo服务器通常不会这么用吗?
相关代码
Schema
const typeDefs = gql` type GameState { board: [[String!]!]! status: String! winner: String } type Mutation { placeO(x: Int!, y: Int!): GameState } type Query { "Query to get tracks array for the homepage grid" getGameState: GameState } `;
Mutation Resolver
Mutation: { placeO: (_, { x, y }, { dataSources }) => { // check_legal_move(x, y, dataSources.state.board); ?? dataSources.state.board[x][y] = 'O'; // computer_move(dataSources.state.board); ?? return dataSources.state; } },
Client Mutation Request
const PLACE_O = gql` mutation placeO($x: Int!, $y: Int!) { placeO(x: $x, y: $y) { board, status, winner, } } `;
回答
Apollo服务器完全可以处理游戏逻辑,但把所有逻辑塞在解析器里确实不是最佳实践——解析器的核心职责是衔接GraphQL Schema和业务逻辑层,本身不该承载复杂业务代码,否则会导致解析器臃肿、代码难以复用和维护。
你可以把游戏逻辑抽离到专门的业务服务层,比如创建一个GameService类,把检查合法移动、电脑走棋、判断胜负这些逻辑封装成独立方法,然后在解析器里调用这个服务的方法即可。
改造示例
1. 封装游戏逻辑服务
class GameService { constructor(state) { this.state = state; } // 检查移动是否合法 isMoveLegal(x, y) { return this.state.board[x][y] === null || this.state.board[x][y] === ''; } // 执行玩家走棋 makePlayerMove(x, y) { if (!this.isMoveLegal(x, y)) { throw new Error('Invalid move'); } this.state.board[x][y] = 'O'; } // 电脑走棋逻辑 makeComputerMove() { const emptyCells = []; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (this.isMoveLegal(i, j)) { emptyCells.push([i, j]); } } } if (emptyCells.length > 0) { const [x, y] = emptyCells[Math.floor(Math.random() * emptyCells.length)]; this.state.board[x][y] = 'X'; } } // 判断胜负 checkWinner() { const board = this.state.board; const lines = [ [[0,0], [0,1], [0,2]], [[1,0], [1,1], [1,2]], [[2,0], [2,1], [2,2]], [[0,0], [1,0], [2,0]], [[0,1], [1,1], [2,1]], [[0,2], [1,2], [2,2]], [[0,0], [1,1], [2,2]], [[0,2], [1,1], [2,0]], ]; for (const line of lines) { const [a, b, c] = line; if (board[a[0]][a[1]] && board[a[0]][a[1]] === board[b[0]][b[1]] && board[a[0]][a[1]] === board[c[0]][c[1]]) { this.state.winner = board[a[0]][a[1]]; this.state.status = 'Game Over'; return; } } // 检查平局 if (!board.flat().includes(null) && !board.flat().includes('')) { this.state.status = 'Draw'; return; } this.state.status = 'In Progress'; } // 处理完整走棋流程 handlePlayerMove(x, y) { this.makePlayerMove(x, y); this.checkWinner(); if (this.state.status === 'In Progress') { this.makeComputerMove(); this.checkWinner(); } return this.state; } }
2. 简化Mutation解析器
在Apollo Server的dataSources里注入这个服务,然后在解析器里调用:
// 配置dataSources const dataSources = () => ({ gameService: new GameService(initialState), }); // 改造后的Mutation解析器 Mutation: { placeO: (_, { x, y }, { dataSources }) => { return dataSources.gameService.handlePlayerMove(x, y); } },
这种设计的优势很明确:
- 解析器保持精简,只负责调用业务方法
- 游戏逻辑被封装在独立服务中,方便测试、复用和后续扩展(比如增加多人对战模式)
- 代码结构更清晰,符合关注点分离的原则
另外需要注意:如果你的dataSources.state是全局共享的,要考虑多用户场景下的状态隔离问题——单实例服务器会让所有用户共享同一个游戏状态,这显然不合理。可以用会话或者数据库存储每个用户的游戏状态,在GameService中读取和写入对应状态。
内容的提问来源于stack exchange,提问作者aProfessionalHelpNeeder
相关产品推荐
相关产品推荐

