Flutter:纯函数与不可变模型

欢迎关注我的公众号:OpenFlutter,感恩。

一个纯函数是可预测的,并且没有副作用的函数。

dart 复制代码
int increase(int x) {
  int y = x + 1;
  return y;
}

如果参数值相同,它总是会产生相同的结果。

并非总是能够写出纯函数。一些功能,例如 API 请求、本地和远程数据库连接,默认就带有副作用。

然而,我最近发现一个相当普遍的场景,即为模型添加不可变性会自动净化一个函数。

我以国际跳棋游戏为例,但这种情况显然不局限于游戏,它可以是任何应用功能。

假设我们有一个国际跳棋游戏界面,其架构如下:

  • GameView --- 在屏幕上显示游戏;依赖于 GameViewModel
  • GameViewModel --- 维护状态和展示逻辑;依赖于 GameService
  • GameService --- 包含游戏逻辑;
  • GameModel --- 是游戏状态的模型对象。

GameView 目前与我们无关。这里有一个来自另一篇文章的 GIF 动图,只是为了说明它可能是什么样子。

让我们看一下(简化后的)GameModel

dart 复制代码
class GameModel{
  List<List<CheckersPiece?>> board;
  CheckersPiece? selectedPiece;

  const GameModel({
    required this.board,
    this.selectedPiece,
  });
}

实现细节并不重要。不过,请注意它(GameModel)是可变的。

GameViewModel 中,我们有一个 select 方法,当用户点击棋盘时,该方法会被调用:

dart 复制代码
GameService _gameService;
late GameModel _state;

void select(int row, int col) {
  _gameService.selectPiece(_state, row, col);
  update(); // 或者如果我们的 ViewModel是一个ChangeNotifier,就调用notifyListeners。
}

GameService 中,我们有一个 selectPiece 方法,它有副作用(不是纯函数):

dart 复制代码
void selectPiece (GameModel state, int row, int col) {  
...  
    state.selectedPiece = state.board[row][col];  
}

它(selectPiece 方法)隐式地更新了状态,当我们查看 GameViewModel 时,我们看不到这一点。(我们知道这一点是因为这个例子特意简化了 😎,但在更复杂的代码中,我们可能并不知道。)

我们可以尝试让代码更明确地表达这一点。

GameViewModel

dart 复制代码
void select(int row, int col) {
  var newState  = _gameService.selectPiece(_state, row, col);
  _state =  newState;
  update();
}

GameService

dart 复制代码
GameModel selectPiece (GameModel state, int row, int col) {
  ...
  state.selectedPiece = state.board[row][col];
  return state;
}

但是我们实际上做得更糟了,因为现在 selectPiece 方法假装 是纯函数,但它修改了 state 参数并将其作为新结果返回。

如果我们在 GameViewModel 中想要比较新旧状态,它们将永远相等,因为它们引用的是同一个对象。

ini 复制代码
void select(int row, int col) {
  var newState  = _gameService.selectPiece(_state, row, col);
  ...
  if (_state == newState) {  //永远都是 true !!!
  return;
  }
  _state = newState;
  update();
}

那解决方案在哪里呢?

很简单,就是让我们的 GameModel 不可变

dart 复制代码
class GameModel{
  final List<List<CheckersPiece?>> board;
  final CheckersPiece? selectedPiece;

  const GameModel({
    required this.board,
    this.selectedPiece,
  });

  GameModel copyWith({
    List<List<CheckersPiece?>>? board,
    CheckersPiece? selectedPiece,
  }) {
    return GameModel(
      board: board ?? this.board,
      selectedPiece: selectedPiece,
    );
  }
}

现在,我们的 GameService 无法修改 state 参数,只能被迫创建一个新对象:

dart 复制代码
GameModel selectPiece (GameModel state, int row, int col) {  
    ...  
    var newState = state.copyWith(state.board, state.board[row][col]);  
  
    return newState;  
}

selectPiece 方法现在不再有副作用,变得像天使一样纯粹。

我们 GameViewModel 中的 select 方法没有改变,但它不再具有误导性了。

dart 复制代码
void select(int row, int col) {
  var newState  = _gameService.selectPiece(_state, row, col);
  ...
  if (_state == newState) {  //不再总是 true !!!
  return;
  }
  _state = newState;
  update();
}

好了,就这些。

让你的模型不可变,然后快乐地编程吧!

如果你觉得这篇文章值得一读,那就找到👏按钮,然后尽可能久地长按它。

相关推荐
转转技术团队2 分钟前
验证码识别实战:前端不写页面,改训模型了?
前端
MomentYY6 分钟前
Temperature:AI 的“脑洞旋钮”
前端·llm·ai编程
远航_26 分钟前
OpenSpec 完整详细介绍
前端·后端
召钱熏37 分钟前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
SkyWalking中文站38 分钟前
认识 Horizon UI · 1/17:SkyWalking 新一代可观测性控制台
运维·前端·监控
cidy_9838 分钟前
Dify 操作教程:工作流编排 & Chat 对话编排
前端·工作流引擎
tangdou36909865541 分钟前
AI真好玩系列-2分钟快速了解DeepAgents | Quick Guide to DeepAgents in 2 Minutes
前端·javascript·后端
小四的小六44 分钟前
AI Agent效果评测实战——搭完Agent才是噩梦的开始
前端
梨子同志1 小时前
JavaScript
前端
彭于晏爱编程1 小时前
纯 JS + Node,一个下午手搓了能读懂公司代码的 AI 助手,老板以为我转行了
前端·javascript