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

好了,就这些。

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

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

相关推荐
usdoc文档预览2 小时前
USDOC系列之PPT文件在线编辑-VUE前端实现在线浏览器在线编辑PPT文档
前端·vue.js·编辑器·office365·ppt在线编辑·office文件在线编辑·文件在线编辑
用户2705077725242 小时前
Agent 调工具卡住?3 种超时与取消策略实测对比
前端
LuHang3 小时前
我一行代码没写,把领导派的活做成了公司内部系统😭
前端
明月_清风3 小时前
Meta 开源了一个 AI Native 组件库:Astryx,150+ React 组件,专为 AI Agent 而设计
前端·人工智能·ai编程
用户850869276153 小时前
秒杀系统中 Shell 脚本并发控制 Bug:究竟哪里出了
前端
炫酷的大高个6 小时前
6 亿播放的闪身步,我用前端算出 15 倍重心差
前端·javascript
不可能片场6 小时前
内容运营自动化 配图折行别把词切两半
前端·electron
Lank_M6 小时前
HEIC转JPG统一转sRGB,P3高饱和色就回不来了
前端
补码缩写6 小时前
sharp默认转JPG颜色没错,ICC和EXIF却全没了
前端
用户7783366132116 小时前
SerpBase AI Overview 引用卡片:React 组件实战
前端