当前位置: 首页 > news >正文

设计模式Design Patterns:组合Composite、命令Command、策略Strategy

组合(Composite)、命令(Command)和策略(Strategy)这三个设计模式。很多人看书时觉得模式很“玄学”,落地时又不知道什么时候应该用。和你一样,我也喜欢用能跑起来的小例子来理解。所以这篇我打算用非常接地气的方式,把这三位老朋友从“教科书”里拉到“键盘边”,一句话带你入门,每个模式都配上能跑的代码,再顺手聊聊它们怎么在同一个小项目里协作。

先说一个大前提。设计模式不是银弹,它更像是给你的一套“沟通词汇”和“可复用的结构”。当你在团队里说“这儿用命令模式做撤销重做”,别人立刻就能接上,就像说“冒泡排序太慢了”的那种默契。把这三种模式学扎实,你的代码可维护性和可扩展性会非常有感地提升。

接下来,我们一个个来。

组合模式:把对象拼成树

如果你做过目录树、UI 组件树、表达式解析器,你其实已经跟组合模式打过照面了。组合模式要解决的问题很朴素:一棵树里同时有“叶子”和“组合节点”,但我希望对外暴露一套统一的接口,让使用者不用关心“你是叶子还是树枝”,一视同仁地遍历、操作。

我用一个算术表达式的例子来讲。把数字看成叶子,把加减乘除看成可以连接两个子表达式的“组合节点”。最终我们拿到一棵树,评价值时只要递归就可以走完整棵树。

下面是一个完整可跑的 Python 版本,结构上非常贴近组合模式的“教科书定义”。把它保存成一个文件直接运行就行。

from abc import ABC, abstractmethodclass Expression(ABC):@abstractmethoddef evaluate(self):pass@abstractmethoddef __str__(self):passclass Number(Expression):def __init__(self, value):self.value = float(value)def evaluate(self):return self.valuedef __str__(self):v = self.valuereturn str(int(v)) if v.is_integer() else str(v)class BinaryOperation(Expression):def __init__(self, left, right, operator):self.left = leftself.right = rightself.operator = operatordef __str__(self):return f"({self.left} {self.operator} {self.right})"class AddOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "+")def evaluate(self):return self.left.evaluate() + self.right.evaluate()class SubtractOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "-")def evaluate(self):return self.left.evaluate() - self.right.evaluate()class MultiplyOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "*")def evaluate(self):return self.left.evaluate() * self.right.evaluate()class DivideOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "/")def evaluate(self):return self.left.evaluate() / self.right.evaluate()class ModuloOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "%")def evaluate(self):return self.left.evaluate() % self.right.evaluate()class PowerOperation(BinaryOperation):def __init__(self, left, right):super().__init__(left, right, "**")def evaluate(self):return self.left.evaluate() ** self.right.evaluate()# 手工拼一棵表达式树:(2 + 3) * (4 - 1) ** 2
expr = MultiplyOperation(AddOperation(Number(2), Number(3)),PowerOperation(SubtractOperation(Number(4), Number(1)), Number(2))
)
print(expr, "=", expr.evaluate())  # ((2 + 3) * ((4 - 1) ** 2)) = 45.0

你能感受到它的“味道”:Expression 是组件基类,Number 是叶子,BinaryOperation 是组合基类,具体的加减乘除取幂取模是具体的组合节点。使用者始终面对的是 Expression 这一致的抽象,递归时完全无需分支判断“你是 Task 还是 TaskGroup”这种 IF。这个统一抽象就是组合模式的灵魂。

组合模式最常见的两个好处是统一与扩展。统一体现在对外接口一致,扩展体现在想加新的“节点类型”非常简单,比如今天多了一个“绝对值”或“正弦函数”,你就新增一个类,接上 Expression,其余地方不动。缺点也有,最大的问题是有时你不得不在抽象里塞进一些“叶子实现不了的方法”,这就开始违反接口隔离原则了。比如你非要让叶子也实现 add/remove,这在叶子上只能抛异常。解决办法是把抽象再细分,比如给组合节点单独一个接口。

如果你做的是 Todo 树,也是同样的套路。一个可运行的简化版本如下,TodoItem 是抽象,Task 是叶子,TaskGroup 是组合。展示时一棵树走下来就完事。

from abc import ABC, abstractmethod
from datetime import datetimeclass TodoItem(ABC):def __init__(self, name, description=""):self.name = nameself.description = descriptionself.created_at = datetime.now().isoformat()self.completed = False@abstractmethoddef display(self, indent=0):pass@abstractmethoddef is_group(self):passclass Task(TodoItem):def display(self, indent=0):status = "[Done]" if self.completed else "[ ]"return "  " * indent + f"{status} {self.name}"def is_group(self): return Falseclass TaskGroup(TodoItem):def __init__(self, name, description=""):super().__init__(name, description)self.items = []def add(self, item): self.items.append(item)def remove(self, item): self.items.remove(item)def display(self, indent=0):header = "  " * indent + f"Group: {self.name}"if self.description:header += f" - {self.description}"lines = [header]for it in self.items:lines.append(it.display(indent + 1))return "\n".join(lines)def is_group(self): return Trueroot = TaskGroup("Study", "CS patterns")
root.add(Task("Read Composite"))
root.add(Task("Implement Command"))
algos = TaskGroup("Algorithms")
algos.add(Task("Graph practice"))
root.add(algos)
print(root.display())

跑出来是一棵树,非常直观。组合模式就是这样优雅。

命令模式:把“动作”封装成对象

命令模式的口号是把“请求”也当成对象。说人话,就是把“干某件事”变成一个 class,它要能执行,也要能撤销。典型场景就是编辑器的撤销重做、UI 里把操作推到队列里异步执行、甚至把一系列操作保存成宏。

我们实现一个袖珍文本编辑器,支持插入、删除整行、清空内容,最关键是支持撤销和重做。命令模式里会有四个角色。命令基类 Command 管生命周期。具体命令 InsertTextCommandDeleteTextCommandClearTextCommand 实现 _do_execute_do_undo。接收者 TextEditor 负责真正的文本与光标操作。调用者 EditorInvoker 管历史栈、撤销重做的游标。

可以直接复制这段代码跑一下感受手感。

from abc import ABC, abstractmethodclass Command(ABC):def __init__(self):self._executed = Falsedef execute(self):if not self._executed:self._do_execute()self._executed = Truedef undo(self):if self._executed:self._do_undo()self._executed = False@abstractmethoddef _do_execute(self): pass@abstractmethoddef _do_undo(self): passclass TextEditor:def __init__(self):self._content = ""self._cursor_position = 0def insert_text(self, text, position=None):if position is None:position = self._cursor_positionself._content = self._content[:position] + text + self._content[position:]self._cursor_position = position + len(text)def delete_text(self, start_pos, length):deleted = self._content[start_pos:start_pos+length]self._content = self._content[:start_pos] + self._content[start_pos+length:]if self._cursor_position > start_pos:self._cursor_position = max(start_pos, self._cursor_position - length)return deleteddef clear(self):old = self._contentself._content = ""self._cursor_position = 0return olddef get_content(self): return self._contentdef get_cursor_position(self): return self._cursor_positiondef set_cursor_position(self, pos):self._cursor_position = max(0, min(pos, len(self._content)))class InsertTextCommand(Command):def __init__(self, editor: TextEditor, text, position=None):super().__init__()self._editor = editorself._text = textself._position = position if position is not None else editor.get_cursor_position()def _do_execute(self):self._editor.insert_text(self._text, self._position)def _do_undo(self):self._editor.delete_text(self._position, len(self._text))class DeleteTextCommand(Command):def __init__(self, editor: TextEditor):super().__init__()self._editor = editorself._start_pos = 0self._length = 0self._deleted_text = ""self._cursor_before_delete = 0def _do_execute(self):content = self._editor.get_content()if not content: returnself._cursor_before_delete = self._editor.get_cursor_position()cursor_pos = self._cursor_before_deletelines = content.split('\n')current_pos = 0current_line = 0for i, line in enumerate(lines):line_len = len(line) + (1 if i < len(lines) - 1 else 0)if current_pos + line_len > cursor_pos:current_line = ibreakcurrent_pos += line_lenline_start_pos = 0for i in range(current_line):line_start_pos += len(lines[i]) + 1line_length = len(lines[current_line])if current_line < len(lines) - 1:line_length += 1self._start_pos = line_start_posself._length = line_lengthself._deleted_text = self._editor.delete_text(self._start_pos, self._length)new_cursor_pos = self._start_posif new_cursor_pos > len(self._editor.get_content()):new_cursor_pos = len(self._editor.get_content())self._editor.set_cursor_position(new_cursor_pos)def _do_undo(self):self._editor.insert_text(self._deleted_text, self._start_pos)self._editor.set_cursor_position(self._cursor_before_delete)class ClearTextCommand(Command):def __init__(self, editor: TextEditor):super().__init__()self._editor = editorself._old_content = ""self._cursor_before_clear = 0def _do_execute(self):self._cursor_before_clear = self._editor.get_cursor_position()self._old_content = self._editor.clear()def _do_undo(self):if self._old_content:self._editor.insert_text(self._old_content, 0)self._editor.set_cursor_position(self._cursor_before_clear)class EditorInvoker:def __init__(self):self._history = []self._current_position = -1def execute_command(self, cmd: Command):cmd.execute()if self._current_position < len(self._history) - 1:self._history = self._history[:self._current_position + 1]self._history.append(cmd)self._current_position += 1def undo(self):if self._current_position >= 0:self._history[self._current_position].undo()self._current_position -= 1return Truereturn Falsedef redo(self):if self._current_position < len(self._history) - 1:self._current_position += 1self._history[self._current_position].execute()return Truereturn False# 体验一下
ed = TextEditor()
inv = EditorInvoker()
inv.execute_command(InsertTextCommand(ed, "Hello"))
inv.execute_command(InsertTextCommand(ed, "\nWorld"))
print("Now:", repr(ed.get_content()))
inv.undo(); print("Undo:", repr(ed.get_content())))
inv.redo(); print("Redo:", repr(ed.get_content()))
inv.execute_command(DeleteTextCommand(ed)); print("Delete line:", repr(ed.get_content()))
inv.undo(); print("Undo delete:", repr(ed.get_content()))
inv.execute_command(ClearTextCommand(ed)); print("Cleared:", repr(ed.get_content()))
inv.undo(); print("Undo clear:", repr(ed.get_content()))

这个结构一旦搭好,扩展新命令就非常舒服,比如“替换选区”、“粘贴”、“将光标移动到第 n 行”,都只是新增一个命令类,不会触碰历史管理逻辑。

命令模式常见的坑在于状态保存的粒度。撤销需要“逆操作的信息”,比如插入要知道插入位置和插入长度,删除要保存被删文本和起始位置。这个信息是放在命令里,还是让接收者提供“快照”?规模小的时候直接放命令里足够清晰,规模大时可以引入备忘录(Memento)模式做快照,撤销时把对象恢复到快照状态,代价是内存会涨。

策略模式:把“算法/方案”可插拔

策略模式直觉上最好懂。你有一个“抽象的算法”,它的具体实现可以互换。你对外暴露一个统一的接口,比如 save/load,具体到底是 JSON 存储还是纯文本存储,调用方不关心。好处是运行时也能换,测试时还能注入一个假的实现。

我拿上面的 Todo 做例子。定义一个存储策略接口 StorageStrategy,两个实现 JSONStorageTextStorage,管理器 TodoListManager 持有一个 self.storage,只跟接口对话。

import json, os
from abc import ABC, abstractmethodclass StorageStrategy(ABC):@abstractmethoddef save(self, data, filename): pass@abstractmethoddef load(self, filename): passclass JSONStorage(StorageStrategy):def save(self, data, filename):def to_dict(item):if hasattr(item, 'items'):return {"type":"group","name":item.name,"description":item.description,"completed":item.completed,"items":[to_dict(c) for c in item.items]}else:return {"type":"task","name":item.name,"description":item.description,"completed":item.completed}with open(filename, 'w', encoding='utf-8') as f:json.dump([to_dict(i) for i in data], f, ensure_ascii=False, indent=2)def load(self, filename):if not os.path.exists(filename): return []with open(filename, 'r', encoding='utf-8') as f:arr = json.load(f)def from_dict(d):if d["type"] == "group":g = TaskGroup(d["name"], d.get("description",""))g.completed = d.get("completed", False)for c in d.get("items", []):g.add(from_dict(c))return gelse:t = Task(d["name"], d.get("description",""))t.completed = d.get("completed", False)return treturn [from_dict(x) for x in arr]class TextStorage(StorageStrategy):def save(self, data, filename):def item_to_text(item, indent=0):space = "  " * indentif hasattr(item, 'items'):header = f"{space}Group: {item.name}"if item.description: header += f" - {item.description}"header += "\n"return header + "".join(item_to_text(c, indent+1) for c in item.items)else:return f"{space}[{'Done' if item.completed else ' '}] {item.name}\n"with open(filename, 'w', encoding='utf-8') as f:for it in data:f.write(item_to_text(it))def load(self, filename):# 简化:只作为演示return []class TodoListManager:def __init__(self, storage: StorageStrategy):self.items = []self.storage = storageself.filename = "todos.data"def save(self):self.storage.save(self.items, self.filename)def load(self):self.items = self.storage.load(self.filename)todo_json = TodoListManager(JSONStorage())
todo_txt  = TodoListManager(TextStorage())

这个模式落地非常频繁。日志系统支持本地文件、控制台、远端;图片处理支持不同压缩算法;推荐系统支持不同召回策略;甚至 HTTP 请求的重试策略、退避算法,也可以用策略模式封装成可插拔组件。

跟工厂有什么关系?很多时候你会配合一个工厂来“选择策略”,例如读配置发现要用 JSON,就 new 一个 JSONStorage;要用 Text,就 new 一个 TextStorage。这个选择可以在启动时做,也可以在运行中切换。

小提醒,如果你的“策略”只有两个分支,且将来不太可能扩展,直接 if-else 就够了,不要为了模式而模式。模式的价值来自未来的变化,如果变化概率很小,简单就是王道。

三个模式怎么“串起来”

说完单体,我们来做点“混搭菜”。一个看起来很合理的需求是这样的:我们做一个命令行 Todo 应用,用户可以用命令新增任务、创建分组、标记完成,也可以随时把数据持久化到本地。我们还想把 UI 操作做成可撤销的,以防误操作。数据结构上,一定是一个树状结构的 Todo 项。

在这个场景里,组合模式是数据模型的骨架,任务和任务分组自然形成树;策略模式负责数据的持久化方案,JSON 和文本互相切换;命令模式负责用户操作的封装和撤销重做,比如“添加任务”、“移动任务”、“删除整组”等,历史记录可以很自然地管理起来。

一个简化的流程是这样的。用户在 CLI 输入命令,比如 add,“添加任务”被封装成一个 AddTaskCommand,里面的执行逻辑是定位到目标分组(或根),创建一个 Task 并挂上去,然后触发保存,保存调用的是当前注入的 StorageStrategy。如果用户后悔了,调用 invoker 的 undo,把 AddTaskCommand_do_undo 执行,delete 掉刚才加的节点,再次保存即可。你会发现三个模式像齿轮一样完美咬合,各司其职。

这套设计的妙处在于解耦。组合模式隔离了数据结构操作;策略模式把 IO 的细节挪出业务逻辑;命令模式让操作的生命周期和历史有了清晰的边界。解耦之后,任意一个地方的变更都不至于牵一发动全身,比如要加一个 CSVStorage,或者要加一个“批量标记完成”的命令,或者要支持子任务嵌套多层,改动点都很明确。

再多聊两句:什么时候不用它们

模式不是越多越好。有几种典型的“误用”场景,尽量避开。

第一种是“为了用模式而用”。例如策略模式只有两个分支,且可以预见不会扩展,强行抽象出接口反而把逻辑分散开了。简单的 if 更好维护。

第二种是“抽象层次不合适”。组合模式经常被误用在并非树状的数据上,或者把叶子硬要求实现“增删子节点”的方法,导致接口逼着抛异常。这其实是抽象没有分层到位,应该把“可组合”的接口单独出来。

第三种是“状态管理太随意”。命令模式最怕“不知道撤销要撤回到哪里”,这通常是因为命令内部没有保存足够的信息,或者把状态的一部分藏在了调用者之外。保持命令的“自描述性”,也就是它能够独立地执行与撤销,是实践命令模式的关键。

常见问答:你可能会关心的问题

有没有组合和装饰器的区别?很多人会混淆。装饰器是“包装一个对象以增强功能”,通常保持接口完全一致;组合是“把多个对象组织成一棵树以表达整体-部分关系”。装饰器更像是一个对象外面套一层;组合是多个对象之间的层级关系。

命令模式和回调函数有啥不同?回调也能把“动作”封装起来,但命令强调“对象 + 状态 + 可撤销”,回调一般是一次性的小函数,没有自己的历史和生命周期管理。你可以把简单命令用回调实现,但一旦涉及撤销重做,命令的类形式更加自然。

策略模式和工厂模式如何搭配?策略解决“算法可插拔”,工厂解决“实例化的选择与创建”。很多时候你会用工厂来屏蔽策略的选择过程,例如通过配置或运行时条件动态选择策略,工厂返回给你一个策略实例。

补几段更贴地的代码

刚才的三个例子已经能跑,这里再放一个把三者拼在一起的小玩具,逻辑上完整但为了篇幅简单很多。它展示了“添加任务命令”“JSON 存储策略”“任务树结构”之间的连接。

# 假设 Task/TaskGroup 和 JSONStorage 已经在上文定义
class AddTaskCommand(Command):def __init__(self, manager, name, group=None):super().__init__()self.manager = managerself.name = nameself.group = groupself._parent = Noneself._task = Nonedef _do_execute(self):self._task = Task(self.name)if self.group:for it in self.manager.items:if getattr(it, "name", None) == self.group and getattr(it, "items", None) is not None:self._parent = itbreakif self._parent is None:self._parent = self.manager  # 把 manager 当作根容器if hasattr(self._parent, "add"):self._parent.add(self._task)else:self.manager.items.append(self._task)self.manager.save()def _do_undo(self):if self._parent and hasattr(self._parent, "remove"):self._parent.remove(self._task)else:self.manager.items.remove(self._task)self.manager.save()class TodoListManagerWithRoot(TodoListManager):def __init__(self, storage):super().__init__(storage)def add(self, item): self.items.append(item)def remove(self, item): self.items.remove(item)# 组装
mgr = TodoListManagerWithRoot(JSONStorage())
inv = EditorInvoker()
inv.execute_command(AddTaskCommand(mgr, "写一篇 CSDN 博客"))
print("Items:", [it.name for it in mgr.items])
inv.undo()
print("After undo:", [it.name for it in mgr.items])

把它跑起来,你能体会到命令在“发生副作用”前后如何配合策略去落地到磁盘,同时 Todo 的树状结构又让 UI 展示变得自然。

对初学者的三点建议

第一,先用起来,再总结抽象。很多模式读起来云里雾里,一上手写两个小例子就开窍了。你看本文的算术表达式树、文本编辑器、持久化切换,都是很轻的小例子,但能把精髓讲清楚。

第二,不要一来就“重模式”。写业务时从最简单的代码开始,当你感觉 if-else 在扩展时变得臃肿,或者撤销重做的状态满天飞,这时再抽象成策略或命令,水到渠成。过早抽象会让你掉进“模式地狱”。

第三,保持类的职责单一。无论是组合、命令还是策略,最终落地的代码质量,取决于每个类是否只做一件事。组合节点只关心组合关系,命令只关心一次操作的执行与撤销,策略只关心一类算法或 IO。职责清晰,依赖关系自然就清晰。

收个尾:怎么把它们带到工作里

如果你在做工具型应用(编辑器、IDE 插件、绘图工具),命令模式几乎是标配,撤销重做是用户的肌肉记忆,先把命令的框架搭好,会让你后面的迭代舒服很多。

如果你在做层级数据(组织架构、目录、图形场景树、表单容器),组合模式能让你的渲染、序列化、权限控制和批量操作统一起来,代码可读性会明显提升。

如果你在做可配置系统(多种存储后端、多种推荐算法、多种调度策略),策略模式就是把“选择权”交给配置和运行环境的一把钥匙,测试也会快得多。

希望这篇把三件套讲“顺了”。最后的建议是别背定义,去写一段能跑的代码,用你自己的业务场景套一遍。你会惊喜地发现,模式不是约束你,而是帮你把变化装进小盒子里,代码因此更优雅、更稳、也更好讲给同事听。


文章转载自:

http://8PcOImzl.ybgdL.cn
http://oXdp7jTp.ybgdL.cn
http://qgrKwlDM.ybgdL.cn
http://g0zAckdY.ybgdL.cn
http://5UMLrOLD.ybgdL.cn
http://jSBhhc8F.ybgdL.cn
http://XBeNnkeR.ybgdL.cn
http://qbOzQpad.ybgdL.cn
http://N29jY3z8.ybgdL.cn
http://eMSOBO8r.ybgdL.cn
http://Yn8jHPnx.ybgdL.cn
http://dN4x3IB8.ybgdL.cn
http://bZWHzDyO.ybgdL.cn
http://TTDOLdiQ.ybgdL.cn
http://tUFYSksa.ybgdL.cn
http://XCErORf7.ybgdL.cn
http://tRC1Vfmt.ybgdL.cn
http://MMcOLEGb.ybgdL.cn
http://r5oOO5H1.ybgdL.cn
http://WLnLfsIp.ybgdL.cn
http://lZySgfHR.ybgdL.cn
http://Y102O059.ybgdL.cn
http://pY9BJaJ0.ybgdL.cn
http://jLzHjxdu.ybgdL.cn
http://t0Jjg8q7.ybgdL.cn
http://QXEsEvZ2.ybgdL.cn
http://85KCJIJk.ybgdL.cn
http://tAQEZpH7.ybgdL.cn
http://lO3x0DVI.ybgdL.cn
http://MEk4VTLS.ybgdL.cn
http://www.dtcms.com/a/370762.html

相关文章:

  • 【Mysql-installer-community-8.0.26.0】Mysql 社区版(8.0.26.0) 在Window 系统的默认安装配置
  • 【STM32HAL-----NRF24L01】
  • cocos2d. 3.17.2 c++如何实现下载断点续传zip压缩包带进度条
  • gcloud cli 使用 impersonate模拟 服务帐号
  • leetcode 3495. 使数组元素都变为零的最少操作次数-C语言
  • 把装配想象成移动物体的问题
  • mac-intel操作系统go-stock项目(股票分析工具)安装与配置指南
  • 【问题记录】IIS 报500.19,如何解决
  • 【LLM】Openai分析大模型出现幻觉的原因
  • C++算法学习——链表
  • 驱动——Platform
  • LeetCode 139. 单词拆分 - 动态规划解法详解
  • 开源AI智能名片链动2+1模式S2B2C商城小程序服务提升复购率和转介绍率的研究
  • HTTP协议——Cookie的相关概念和使用
  • redis的数据类型:Hash
  • PiscCode使用 Mediapipe 实时人脸表情识别与可视化
  • EG2104 SOP-8 带SD功能 内置600V功率MOS管 栅极驱动芯片
  • 【审核问题——托管式首次进入APP展示隐私政策弹窗】
  • MySQL+Canal同步ES延时问题全链路解决方案
  • 【高等数学】第十一章 曲线积分与曲面积分——第三节 格林公式及其应用
  • Android Kotlin 动态注册 Broadcast 的完整封装方案
  • OceanBase容量统计:租户、数据库、表大小
  • SpringAMQP
  • 软件设计师备考-(十四)数据库设计
  • Fast DDS原生程序ROS2 Rviz Debug工具接入--Overview
  • 深入理解 Next.js 的路由机制
  • 鸿蒙 BLE 蓝牙智能设备固件升级之DFU升级方式(Nordic芯片)
  • 5-10数组元素添加和删除(数组基础操作)
  • echarts实现两条折线区域中间有线连接,custom + renderItem(初级版)
  • 机器人控制器开发(传感器层——奥比大白相机适配)