设计模式综合实战三:基于Java Swing实现绘图工具(命令+状态+备忘录+组合)
说实话,这个题目一看就有点「硬核」。四个模式一起上,还要用Swing做界面。我当年第一次看到这种组合设计时,心里也犯嘀咕:有必要搞这么复杂吗?直到我自己动手做了一个绘图工具,才发现——嗯,真香。
今天我们就来拆解这个综合案例。我会带着你一步步看,这四个模式是怎么在绘图工具里各司其职的。
一、需求场景:我们要做什么?
先明确目标:做一个简单的绘图工具。用户能画矩形、圆形、线条,能撤销重做,能保存和恢复状态。听起来是不是很像Windows画图?对,就是那个意思。
但我们要做得更「设计模式」一点。说白了,就是让代码好扩展、好维护。你想想看,如果以后要加个「画三角形」的功能,总不能把整个代码翻个底朝天吧?
二、四个模式的分工
我习惯先理清每个模式负责什么。就像团队分工一样,各管一摊,互不干扰。
| 模式 | 职责 | 类比 |
|---|---|---|
| 命令模式 | 封装绘图操作(画一个形状、删除一个形状) | 遥控器上的按钮 |
| 状态模式 | 管理工具状态(当前是画矩形模式还是画圆模式) | 笔的粗细切换 |
| 备忘录模式 | 保存和恢复画布快照(撤销/重做) | 游戏存档 |
| 组合模式 | 管理图形对象(单个形状和组合形状统一处理) | 文件夹和文件 |
你看,每个模式都有自己的一亩三分地。组合模式管「数据」,命令模式管「操作」,状态模式管「上下文」,备忘录模式管「历史」。各司其职,代码就不会乱成一锅粥。
三、核心架构图
我画了一张图,帮你理清它们之间的关系。这张图我反复调整过几次,确保你看一眼就能明白。
这张图我画了三遍才满意。箭头方向代表调用关系,不是数据流。你仔细看,状态模式触发命令,命令执行时保存备忘录,同时操作组合对象。环环相扣,但每个环都是独立的。
四、代码实现:从接口开始
我个人习惯先定义接口,再写实现。这样思路清晰,不会写着写着就跑偏了。
4.1 组合模式:图形接口
// 图形接口 - 组合模式的核心
public interface Shape {
void draw(Graphics g);
void move(int dx, int dy);
Rectangle getBounds();
Shape clone();
}
// 单个图形:矩形
public class RectangleShape implements Shape {
private int x, y, width, height;
public RectangleShape(int x, int y, int w, int h) {
this.x = x; this.y = y;
this.width = w; this.height = h;
}
@Override
public void draw(Graphics g) {
g.drawRect(x, y, width, height);
}
@Override
public void move(int dx, int dy) {
x += dx; y += dy;
}
@Override
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
@Override
public Shape clone() {
return new RectangleShape(x, y, width, height);
}
}
// 组合图形:可以包含多个子图形
public class CompositeShape implements Shape {
private List<Shape> children = new ArrayList<>();
public void add(Shape shape) {
children.add(shape);
}
public void remove(Shape shape) {
children.remove(shape);
}
@Override
public void draw(Graphics g) {
for (Shape child : children) {
child.draw(g);
}
}
@Override
public void move(int dx, int dy) {
for (Shape child : children) {
child.move(dx, dy);
}
}
@Override
public Rectangle getBounds() {
// 计算所有子图形的并集边界
Rectangle bounds = null;
for (Shape child : children) {
Rectangle r = child.getBounds();
if (bounds == null) bounds = r;
else bounds = bounds.union(r);
}
return bounds;
}
@Override
public Shape clone() {
CompositeShape cloned = new CompositeShape();
for (Shape child : children) {
cloned.add(child.clone());
}
return cloned;
}
}
这里有个小细节:clone()方法。为什么要深拷贝?因为备忘录模式需要保存画布快照,如果只是浅拷贝,撤销时就会出问题。我曾经在这个坑里摔过一次——撤销后图形位置没变,排查了半天才发现是浅拷贝惹的祸。
4.2 命令模式:操作封装
// 命令接口
public interface Command {
void execute();
void undo();
}
// 具体命令:添加图形
public class AddShapeCommand implements Command {
private CompositeShape canvas;
private Shape shape;
public AddShapeCommand(CompositeShape canvas, Shape shape) {
this.canvas = canvas;
this.shape = shape;
}
@Override
public void execute() {
canvas.add(shape);
}
@Override
public void undo() {
canvas.remove(shape);
}
}
// 命令管理器:支持撤销/重做
public class CommandManager {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
public void executeCommand(Command cmd) {
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // 新操作清空重做栈
}
public void undo() {
if (!undoStack.isEmpty()) {
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
}
你想想看,如果没有命令模式,撤销功能得怎么写?可能每个操作都要写一段撤销逻辑,散落在各个地方。有了命令模式,撤销逻辑就集中在undo()方法里,清爽多了。
4.3 状态模式:工具状态切换
// 状态接口
public interface ToolState {
void onMouseDown(int x, int y);
void onMouseUp(int x, int y);
void onMouseDrag(int x, int y);
}
// 具体状态:画矩形模式
public class RectangleToolState implements ToolState {
private int startX, startY;
private CompositeShape canvas;
private CommandManager cmdManager;
public RectangleToolState(CompositeShape canvas, CommandManager cmdManager) {
this.canvas = canvas;
this.cmdManager = cmdManager;
}
@Override
public void onMouseDown(int x, int y) {
startX = x;
startY = y;
}
@Override
public void onMouseUp(int x, int y) {
int w = Math.abs(x - startX);
int h = Math.abs(y - startY);
int minX = Math.min(startX, x);
int minY = Math.min(startY, y);
Shape rect = new RectangleShape(minX, minY, w, h);
Command cmd = new AddShapeCommand(canvas, rect);
cmdManager.executeCommand(cmd);
}
@Override
public void onMouseDrag(int x, int y) {
// 可以在这里绘制预览效果
}
}
// 上下文:绘图面板
public class DrawingPanel extends JPanel {
private ToolState currentState;
private CompositeShape canvas = new CompositeShape();
private CommandManager cmdManager = new CommandManager();
public void setToolState(ToolState state) {
this.currentState = state;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
canvas.draw(g);
}
// 鼠标事件委托给状态对象
@Override
public void mousePressed(MouseEvent e) {
currentState.onMouseDown(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
currentState.onMouseUp(e.getX(), e.getY());
repaint();
}
}
状态模式的好处是什么?你加一个新工具(比如画三角形),只需要新增一个TriangleToolState类,实现ToolState接口就行。完全不用改现有代码。这就是开闭原则的体现。
4.4 备忘录模式:保存和恢复
// 备忘录:保存画布快照
public class CanvasMemento {
private List<Shape> shapesSnapshot;
public CanvasMemento(List<Shape> shapes) {
// 深拷贝:每个Shape都要clone
this.shapesSnapshot = new ArrayList<>();
for (Shape s : shapes) {
this.shapesSnapshot.add(s.clone());
}
}
public List<Shape> getShapes() {
return shapesSnapshot;
}
}
// 发起人:绘图面板(扩展)
public class DrawingPanelWithMemento extends DrawingPanel {
private Stack<CanvasMemento> history = new Stack<>();
public void saveState() {
// 获取当前所有图形
List<Shape> currentShapes = getCanvasShapes();
CanvasMemento memento = new CanvasMemento(currentShapes);
history.push(memento);
}
public void restoreState() {
if (!history.isEmpty()) {
CanvasMemento memento = history.pop();
List<Shape> restoredShapes = memento.getShapes();
setCanvasShapes(restoredShapes);
repaint();
}
}
// 辅助方法:获取/设置画布中的图形列表
private List<Shape> getCanvasShapes() {
// 从CompositeShape中提取所有子图形
// 这里需要根据CompositeShape的具体实现来写
return new ArrayList<>();
}
private void setCanvasShapes(List<Shape> shapes) {
// 清空画布,重新添加
}
}
五、整合:让一切运转起来
好了,四个模式都实现了。现在把它们组装起来。
public class DrawingApp {
public static void main(String[] args) {
JFrame frame = new JFrame("绘图工具 - 设计模式实战");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
// 创建绘图面板
DrawingPanel panel = new DrawingPanel();
// 创建命令管理器
CommandManager cmdManager = new CommandManager();
// 创建各种工具状态
ToolState rectTool = new RectangleToolState(panel.getCanvas(), cmdManager);
ToolState circleTool = new CircleToolState(panel.getCanvas(), cmdManager);
// 默认使用矩形工具
panel.setToolState(rectTool);
// 添加工具栏按钮
JToolBar toolbar = new JToolBar();
JButton rectBtn = new JButton("矩形");
rectBtn.addActionListener(e -> panel.setToolState(rectTool));
JButton circleBtn = new JButton("圆形");
circleBtn.addActionListener(e -> panel.setToolState(circleTool));
JButton undoBtn = new JButton("撤销");
undoBtn.addActionListener(e -> cmdManager.undo());
JButton redoBtn = new JButton("重做");
redoBtn.addActionListener(e -> cmdManager.redo());
toolbar.add(rectBtn);
toolbar.add(circleBtn);
toolbar.add(undoBtn);
toolbar.add(redoBtn);
frame.add(toolbar, BorderLayout.NORTH);
frame.add(panel, BorderLayout.CENTER);
frame.setVisible(true);
}
}
六、避坑指南
做这个综合案例时,有几个坑我踩过,分享给你:
- 深拷贝问题:备忘录模式一定要做深拷贝。我建议在
Shape接口中定义clone()方法,每个子类都实现它。 - 状态切换时的清理:切换工具状态时,要确保当前状态没有未完成的拖拽操作。否则会出现「鼠标还按着,但工具已经换了」的诡异情况。
- 组合图形的边界计算:
CompositeShape.getBounds()要递归计算所有子图形的边界。如果子图形也是组合图形,要一直递归下去。 - 撤销栈的大小:如果不限制撤销栈的大小,画了一整天后,内存可能会爆。我一般限制最多100步撤销。
七、总结
这个综合案例,说白了就是让四个设计模式各司其职,协同工作。你不需要一次性记住所有代码,但一定要理解它们的分工逻辑:
- 组合模式管「数据」——图形怎么组织
- 命令模式管「操作」——怎么画、怎么删
- 状态模式管「上下文」——当前是什么工具
- 备忘录模式管「历史」——怎么撤销、怎么恢复
这种分层思想,在大型项目中特别有用。我后来做的一个CAD软件,核心架构就是从这个案例演变而来的。只不过把Swing换成了JavaFX,把矩形圆形换成了更复杂的工程图元。但底层的设计模式组合,几乎一模一样。
好了,今天的实战就到这里。代码你可以直接拿去用,但更重要的是理解背后的设计思路。下次遇到类似的需求,你也能自己搭出这样的架构来。
公众号:蓝海资料掘金营,微信deep3321