痛點場景:繪圖應用的操作管理
假設你在開發一個繪圖App,需要支持:
- 添加/刪除圖形
- 修改圖形屬性
- 撤銷/重做操作
- 批量執行命令
傳統實現方式:
void _handleAddShape(ShapeType type) {final shape = _createShape(type);setState(() => _shapes.add(shape));
}void _handleDeleteShape(Shape shape) {setState(() => _shapes.remove(shape));
}void _handleChangeColor(Shape shape, Color newColor) {setState(() => shape.color = newColor);
}// 撤銷操作?需要自己維護狀態...
問題爆發點:
- 🔄 撤銷/重做功能難以實現
- 📦 批量操作無法封裝
- 🔗 操作與執行代碼緊密耦合
- ?? 延遲執行或排隊操作困難
命令模式解決方案
核心思想: 將請求封裝為對象,從而允許:
- 參數化客戶端不同請求
- 排隊或記錄請求
- 支持可撤銷操作
四個關鍵角色:
- 命令接口(Command): 聲明執行操作
- 具體命令(ConcreteCommand): 綁定接收者與動作
- 接收者(Receiver): 知道如何執行操作
- 調用者(Invoker): 觸發命令執行
Flutter繪圖命令實現
1. 定義命令接口
abstract class DrawingCommand {void execute();void undo();String get description; // 用于命令歷史顯示
}
2. 實現具體命令
// 添加圖形命令
class AddShapeCommand implements DrawingCommand {final List<Shape> _shapes;final Shape _shape; String get description => '添加 ${_shape.type}';AddShapeCommand(this._shapes, this._shape);void execute() => _shapes.add(_shape);void undo() => _shapes.remove(_shape);
}// 修改顏色命令
class ChangeColorCommand implements DrawingCommand {final Shape _shape;final Color _newColor;Color _previousColor; String get description => '修改顏色';ChangeColorCommand(this._shape, this._newColor);void execute() {_previousColor = _shape.color;_shape.color = _newColor;}void undo() {_shape.color = _previousColor;}
}// 刪除圖形命令
class DeleteShapeCommand implements DrawingCommand {final List<Shape> _shapes;final Shape _shape;int _index = -1; String get description => '刪除 ${_shape.type}';DeleteShapeCommand(this._shapes, this._shape);void execute() {_index = _shapes.indexOf(_shape);_shapes.remove(_shape);}void undo() {if (_index != -1) {_shapes.insert(_index, _shape);}}
}
3. 創建命令管理器
class CommandManager {final List<DrawingCommand> _commandHistory = [];final List<DrawingCommand> _undoStack = [];void executeCommand(DrawingCommand command) {command.execute();_commandHistory.add(command);_undoStack.clear();notifyListeners();}void undo() {if (_commandHistory.isEmpty) return;final command = _commandHistory.removeLast();command.undo();_undoStack.add(command);notifyListeners();}void redo() {if (_undoStack.isEmpty) return;final command = _undoStack.removeLast();command.execute();_commandHistory.add(command);notifyListeners();}bool get canUndo => _commandHistory.isNotEmpty;bool get canRedo => _undoStack.isNotEmpty;// 與ChangeNotifier結合final _changeNotifier = ChangeNotifier();void addListener(VoidCallback listener) => _changeNotifier.addListener(listener);void removeListener(VoidCallback listener) => _changeNotifier.removeListener(listener);void notifyListeners() => _changeNotifier.notifyListeners();
}
4. 在Flutter中使用
class DrawingApp extends StatefulWidget { _DrawingAppState createState() => _DrawingAppState();
}class _DrawingAppState extends State<DrawingApp> {final List<Shape> _shapes = [];final CommandManager _commandManager = CommandManager();void initState() {super.initState();_commandManager.addListener(_refresh);}void _refresh() => setState(() {});void _addCircle() {_commandManager.executeCommand(AddShapeCommand(_shapes, Circle(Colors.blue)),);}void _changeColor(Shape shape) {_commandManager.executeCommand(ChangeColorCommand(shape, Colors.red),);} Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('繪圖應用'),actions: [IconButton(icon: Icon(Icons.undo),onPressed: _commandManager.canUndo ? _commandManager.undo : null,),IconButton(icon: Icon(Icons.redo),onPressed: _commandManager.canRedo ? _commandManager.redo : null,),],),body: Stack(children: [..._shapes.map((shape) => DraggableShape(shape: shape,onColorChange: () => _changeColor(shape),)),],),floatingActionButton: FloatingActionButton(onPressed: _addCircle,child: Icon(Icons.add),),);}
}
Flutter中的實際應用場景
場景1:宏命令(批量操作)
class MacroCommand implements DrawingCommand {final List<DrawingCommand> _commands = [];void addCommand(DrawingCommand command) {_commands.add(command);} String get description => '批量操作 (${_commands.length}個命令)';void execute() {for (final command in _commands) {command.execute();}}void undo() {for (var i = _commands.length - 1; i >= 0; i--) {_commands[i].undo();}}
}// 使用
final macro = MacroCommand()..addCommand(AddShapeCommand(_shapes, Circle(Colors.red)))..addCommand(AddShapeCommand(_shapes, Rectangle(Colors.blue)))..addCommand(ChangeColorCommand(_shapes[0], Colors.green));_commandManager.executeCommand(macro);
場景2:事務操作
class TransactionCommand implements DrawingCommand {final List<DrawingCommand> _commands = [];bool _executed = false;void addCommand(DrawingCommand command) {if (_executed) throw StateError('事務已執行');_commands.add(command);} String get description => '事務操作';void execute() {try {for (final command in _commands) {command.execute();}_executed = true;} catch (e) {// 任何一個命令失敗就回滾for (var i = _commands.length - 1; i >= 0; i--) {_commands[i].undo();}rethrow;}}void undo() {if (!_executed) return;for (var i = _commands.length - 1; i >= 0; i--) {_commands[i].undo();}_executed = false;}
}
場景3:遠程控制(跨平臺命令)
abstract class RemoteCommand {Future<void> execute();Map<String, dynamic> toJson();factory RemoteCommand.fromJson(Map<String, dynamic> json) {// 根據json創建具體命令}
}class SaveDrawingCommand implements RemoteCommand {final List<Shape> shapes; Future<void> execute() async {await Api.saveDrawing(shapes);} Map<String, dynamic> toJson() => {'type': 'save','shapes': shapes.map((s) => s.toJson()).toList(),};
}// 通過平臺通道執行
MethodChannel('commands').setMethodCallHandler((call) async {final command = RemoteCommand.fromJson(call.arguments);await command.execute();
});
命令模式與狀態管理結合
將命令歷史與Provider結合:
class CommandHistoryProvider extends ChangeNotifier {final CommandManager _manager;CommandHistoryProvider(this._manager) {_manager.addListener(notifyListeners);}List<String> get commandHistory => _manager._commandHistory.map((c) => c.description).toList();List<String> get undoStack => _manager._undoStack.map((c) => c.description).toList();
}// 在UI中顯示歷史
Consumer<CommandHistoryProvider>(builder: (context, provider, child) {return Column(children: [Text('操作歷史:'),...provider.commandHistory.map((desc) => Text(desc)),SizedBox(height: 20),Text('可重做操作:'),...provider.undoStack.map((desc) => Text(desc)),],);}
)
命令模式最佳實踐
-
何時使用命令模式:
- 需要實現撤銷/重做功能
- 需要支持事務操作
- 需要將操作排隊或延遲執行
- 需要支持宏命令(命令組合)
- 需要支持跨平臺操作
-
Flutter特化技巧:
// 將命令與Shortcuts綁定 Shortcuts(shortcuts: {LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.z): const UndoIntent(),LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.y): const RedoIntent(),},child: Actions(actions: {UndoIntent: CallbackAction(onInvoke: (_) => _commandManager.undo(),),RedoIntent: CallbackAction(onInvoke: (_) => _commandManager.redo(),),},child: Builder(builder: (context) => ...),), )
-
性能優化:
// 懶執行命令 class LazyCommand implements DrawingCommand {final Future<void> Function() _action;bool _executed = false;void execute() async {if (!_executed) {await _action();_executed = true;}} }
-
測試策略:
test('撤銷應恢復原始狀態', () {final shapes = [Circle(Colors.red)];final command = ChangeColorCommand(shapes[0], Colors.blue);command.execute();expect(shapes[0].color, Colors.blue);command.undo();expect(shapes[0].color, Colors.red); });
命令模式 vs 策略模式
特性 | 命令模式 | 策略模式 |
---|---|---|
目的 | 封裝操作請求 | 封裝算法 |
關注點 | 何時/如何執行操作 | 如何完成特定任務 |
典型應用 | 撤銷/重做/事務 | 算法替換/策略切換 |
執行時機 | 可延遲/排隊執行 | 通常立即執行 |
命令模式的高級變體
1. 可逆命令工廠
class CommandFactory {static DrawingCommand createAddCommand(List<Shape> shapes, ShapeType type) {return AddShapeCommand(shapes, _createShape(type));}static DrawingCommand createDeleteCommand(List<Shape> shapes, Shape shape) {return DeleteShapeCommand(shapes, shape);}// 注冊自定義命令static void register(String type, DrawingCommand Function() creator) {_customCommands[type] = creator;}
}
2. 命令持久化
class PersistentCommand implements DrawingCommand {final SharedPreferences _prefs;final String _key;void execute() async {await _prefs.setString(_key, 'executed');}void undo() async {await _prefs.remove(_key);} Future<bool> get isExecuted async {return _prefs.containsKey(_key);}
}
3. 時間旅行調試
class TimeTravelManager {final List<List<DrawingCommand>> _timeline = [];int _currentState = -1;void snapshot(List<DrawingCommand> commands) {// 移除當前狀態之后的所有狀態if (_currentState < _timeline.length - 1) {_timeline.removeRange(_currentState + 1, _timeline.length);}_timeline.add(List.from(commands));_currentState = _timeline.length - 1;}void goToState(int index) {if (index >= 0 && index < _timeline.length) {_currentState = index;// 重新執行到目標狀態的所有命令}}
}
總結:命令模式是你的操作保險箱
- 核心價值: 將操作封裝為對象,實現操作管理的高級功能
- Flutter優勢:
- 實現撤銷/重做功能
- 支持事務和批量操作
- 解耦操作發起者和執行者
- 與Flutter快捷鍵系統完美結合
- 適用場景: 繪圖應用、文本編輯器、事務系統、操作歷史記錄
? 設計啟示: 當你需要控制操作的"時間維度"(撤銷/重做)或"空間維度"(跨平臺執行)時,命令模式就是你的"時間機器"!