好的架构让代码像积木一样可拼装、可替换

为什么需要架构设计

五子棋看似简单,但如果把所有逻辑写在一个文件里,很快就会变成一团乱麻。本项目采用了类MVC的三层架构:

┌─────────────────────────────────┐
│         View 层 (UI)             │
│  Index / TwoPlayerPage /         │
│  AIBattlePage / ChessBoardView   │
└──────────┬──────────────────────┘
           │ 调用引擎方法 / 接收回调
┌──────────▼──────────────────────┐
│      Controller/Engine 层        │
│      GomokuEngine (核心引擎)      │
│      AIPlayer (AI控制器)          │
└──────────┬──────────────────────┘
           │ 操作
┌──────────▼──────────────────────┐
│         Model 层 (数据)           │
│  board[][] / history[] / result  │
│  GameConstants (常量与类型)       │
└─────────────────────────────────┘

Model层:GameConstants

// GameConstants.ets — 纯数据定义,无任何逻辑
export const BOARD_SIZE: number = 15;
export const EMPTY: number = 0;
export const BLACK: number = 1;
export const WHITE: number = 2;

export enum Difficulty { EASY = 1, NORMAL = 2, HARD = 3 }
export enum GameResult { PLAYING = 0, BLACK_WIN = 1, WHITE_WIN = 2, DRAW = 3 }

export class Move {
  row: number;
  col: number;
  constructor(row: number, col: number) {
    this.row = row;
    this.col = col;
  }
}

Model层只定义"是什么",不关心"怎么做"。这些常量和类型被所有层共享。

Engine层:GomokuEngine

引擎层是纯逻辑,不依赖任何UI框架:

export class GomokuEngine {
  board: number[][];
  currentPlayer: number;
  result: GameResult;
  history: Move[];
  lastMove: Move | null;

  placePiece(row: number, col: number, player: number): boolean { /* ... */ }
  checkWin(row: number, col: number, player: number): boolean { /* ... */ }
  undo(): boolean { /* ... */ }
  reset(): void { /* ... */ }
  toFlatArray(): number[] { /* ... */ }
}

关键设计决策:

  1. 引擎不import任何ArkUI模块——可以在Node.js中测试
  2. 引擎不主动通知UI——由调用方负责刷新
  3. 引擎返回基本类型(boolean/number[])——UI层自行解读

View层:页面与组件

View层负责渲染引擎状态,并将用户输入传递给引擎:

@Entry
@Component
struct TwoPlayerPage {
  private engine: GomokuEngine = new GomokuEngine();  // 持有引擎实例

  @State boardData: number[] = [];      // 从引擎同步的状态
  @State currentPlayer: number = BLACK;
  @State result: GameResult = GameResult.PLAYING;

  // View → Engine:用户点击落子
  private onCellClick(row: number, col: number): void {
    const success = this.engine.placePiece(row, col, this.currentPlayer);
    if (success) {
      this.refreshBoardData();  // Engine → View:同步状态
    }
  }

  // 从引擎同步数据到UI状态
  private refreshBoardData(): void {
    this.boardData = this.engine.toFlatArray();
    this.currentPlayer = this.engine.currentPlayer;
    this.result = this.engine.result;
  }
}

数据流向

用户点击棋盘
    ↓
ChessBoardView.handleClick()  →  onCellClick回调
    ↓
TwoPlayerPage.onCellClick()   →  engine.placePiece()
    ↓
GomokuEngine修改board[][],更新result
    ↓
refreshBoardData() 同步到 @State
    ↓
@State变化触发UI重绘
    ↓
ChessBoardView的@Watch('onDataChange')触发drawBoard()

可复用组件设计

ChessBoardView是纯粹的表现组件:

@Component
export struct ChessBoardView {
  @Prop @Watch('onDataChange') boardData: number[] = [];  // 输入
  @Prop lastMoveRow: number = -1;                          // 输入
  @Prop lastMoveCol: number = -1;                          // 输入
  onCellClick: (row: number, col: number) => void = () => {};  // 输出回调
}

它不关心数据从哪来(双人对战还是AI对战),只负责:

  1. 根据boardData绘制棋盘
  2. 将点击事件通过回调传出

这种设计使得ChessBoardView被两个页面复用而无需任何修改。

AIPlayer的架构角色

AIPlayer本质上是一个"虚拟控制器"——它读取引擎状态,计算出最佳落子,然后由页面层调用引擎落子:

// AIBattlePage中
const move = this.aiPlayer.getMove(this.engine.board);  // AI计算
this.engine.placePiece(move.row, move.col, WHITE);      // 引擎执行

AIPlayer不直接修改棋盘,保持了引擎对棋盘的独占控制权。

架构优势

  1. 可测试性:GomokuEngine和AIPlayer可以脱离UI单独测试
  2. 可复用性:ChessBoardView被两个页面复用
  3. 可扩展性:添加新AI算法只需继承或替换AIPlayer
  4. 可维护性:修改UI不影响游戏逻辑,反之亦然

总结

好的架构不是过度设计,而是恰到好处的分层。本项目通过Model-Engine-View三层分离,实现了逻辑与表现的解耦。引擎层是项目的核心,UI层是引擎的"显示器",AI层是引擎的"智能大脑"。

附:

在这里插入图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐