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();
}

好了,就这些。

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

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

相关推荐
玲小珑5 小时前
LangChain.js 完全开发手册(六)Vector 向量化技术与语义搜索
前端·langchain·ai编程
晓得迷路了5 小时前
栗子前端技术周刊第 97 期 - Viteland:8 月回顾、Redux Toolkit 2.9、Nuxt 4.1...
前端·javascript·nuxt.js
前端双越老师5 小时前
前端开发 AI Agent 智能体,需要掌握哪些知识?
前端·node.js·agent
EndingCoder5 小时前
Electron 安全性最佳实践:防范常见漏洞
前端·javascript·electron·前端框架·node.js·桌面端
学前端搞口饭吃6 小时前
React props的使用
前端·javascript·react.js
灵感__idea6 小时前
JavaScript高级程序设计(第5版):前端的能力边界
前端·javascript·程序员
华洛6 小时前
SEO还没死,GEO之战已经开始
前端·javascript·产品
IT_陈寒6 小时前
Python性能优化:5个被低估的魔法方法让你的代码提速50%
前端·人工智能·后端
As33100106 小时前
Chrome 插件开发入门指南:从基础到实践
前端·chrome