新谈设计模式 Chapter 08 — 组合模式 Composite

张开发
2026/4/11 20:58:55 15 分钟阅读

分享文章

新谈设计模式 Chapter 08 — 组合模式 Composite
Chapter 08 — 组合模式 Composite灵魂速记文件夹套文件夹——树形结构统一操作叶子和容器。秒懂类比你的电脑文件系统文件是叶子节点不能再包含别的东西文件夹是容器节点可以包含文件和文件夹但不管是文件还是文件夹你都可以对它们做同样的操作查看大小、删除、移动、重命名。组合模式让你用统一的方式对待单个对象和组合对象。问题引入// 灾难现场计算国际快递费// 一个包裹里面可能套着小盒子小盒子里面又套着小盒子……// 你怎么算总价doublecalculatePrice(???){// 如果是单个商品返回商品价格// 如果是盒子递归遍历里面所有东西……// 但类型不统一到处 dynamic_cast一团糟}模式结构┌──────────┐ │Component │ ← 统一接口 ├──────────┤ │operation│ └────┬─────┘ │ ┌─────────┴──────────┐ │ │ ┌───┴──┐ ┌──────┴─────┐ │ Leaf │ │ Composite │ │(叶子)│ │ (容器) │ ├──────┤ ├────────────┤ │operation│ │operation │ ← 遍历子节点递归调用 │ │ │add/remove │ └──────┘ │children │ └────────────┘C 实现#includeiostream#includememory#includestring#includevector#includenumeric// 统一接口 classFileSystemNode{public:virtual~FileSystemNode()default;virtualstd::stringname()const0;virtualintsize()const0;// KBvirtualvoidprint(intindent0)const0;protected:voidprintIndent(intindent)const{for(inti0;iindent;i)std::cout ;}};// 叶子节点文件 classFile:publicFileSystemNode{public:File(std::string name,intsize):name_(std::move(name)),size_(size){}std::stringname()constoverride{returnname_;}intsize()constoverride{returnsize_;}voidprint(intindent0)constoverride{printIndent(indent);std::cout name_ (size_KB)\n;}private:std::string name_;intsize_;};// 容器节点文件夹 classFolder:publicFileSystemNode{public:explicitFolder(std::string name):name_(std::move(name)){}std::stringname()constoverride{returnname_;}// 递归计算所有子节点的大小之和intsize()constoverride{inttotal0;for(constautochild:children_){totalchild-size();}returntotal;}voidprint(intindent0)constoverride{printIndent(indent);std::cout name_ (size()KB)\n;for(constautochild:children_){child-print(indent1);// 递归打印}}Folderadd(std::unique_ptrFileSystemNodenode){children_.push_back(std::move(node));return*this;}private:std::string name_;std::vectorstd::unique_ptrFileSystemNodechildren_;};intmain(){// 构建一棵文件树autorootstd::make_uniqueFolder(project);autosrcstd::make_uniqueFolder(src);src-add(std::make_uniqueFile(main.cpp,12));src-add(std::make_uniqueFile(utils.cpp,8));src-add(std::make_uniqueFile(utils.h,3));autobuildstd::make_uniqueFolder(build);build-add(std::make_uniqueFile(app.exe,2048));root-add(std::move(src));root-add(std::move(build));root-add(std::make_uniqueFile(README.md,5));// 统一操作——不管是文件还是文件夹接口完全一样root-print();std::cout\n项目总大小: root-size()KB\n;}输出 project (2076KB) src (23KB) main.cpp (12KB) utils.cpp (8KB) utils.h (3KB) build (2048KB) app.exe (2048KB) README.md (5KB) 项目总大小: 2076KB核心洞察客户端代码面对的是FileSystemNode*完全不知道也不关心它是文件还是文件夹。递归结构统一操作。这就是组合模式的威力voidprocessNode(constFileSystemNodenode){std::coutnode.name(): node.size()KB\n;// 不管 node 是 File 还是 Folder行为一致// 如果是 Foldersize() 内部会递归}什么时候用✅ 适合❌ 别用数据结构是树形的数据是扁平列表想统一对待叶子和容器叶子和容器行为差异极大递归遍历是核心操作不需要递归遍历UI 组件树、文件系统、组织架构简单的一层包含关系防混淆Composite vs DecoratorCompositeDecorator结构树一对多链一对一目的统一操作树形结构动态添加功能子节点多个 children只包装一个对象类比文件夹套文件夹套娃一层套一层一句话分清Composite 是树形展开Decorator 是链式叠加。Composite vs Iterator两者经常配合使用Composite 定义了树形数据结构Iterator 定义了如何遍历这个结构设计争议add/remove 放哪里透明方式放在 Component 基类中classFileSystemNode{virtualvoidadd(std::unique_ptrFileSystemNode){throwstd::runtime_error(不支持添加子节点);// 叶子会抛异常}};优点客户端完全统一对待缺点对 File 调 add 是运行时错误不安全安全方式本章用的只放在 Composite 中classFolder:publicFileSystemNode{voidadd(std::unique_ptrFileSystemNode);// 只有 Folder 有};优点编译期安全缺点需要知道具体类型才能 add实践中安全方式更常用宁可多一次dynamic_cast也别运行时炸。现代 C 小贴士Folder::size()里的手写循环可以用numeric中的std::accumulate表达得更声明式#includenumericintsize()constoverride{returnstd::accumulate(children_.begin(),children_.end(),0,[](intsum,constautochild){returnsumchild-size();});}C20 的std::ranges::fold_left或 Ranges 的std::ranges::fold_left_first更进一步// C23 fold_left观念相同语法更简洁intsize()constoverride{returnstd::ranges::fold_left(children_,0,[](intsum,constautochild){returnsumchild-size();});}这些都是风格选择——手写 for 循环同样完全正确并且对初学者来说可读性更强。 上一章 | 目录 | 下一章 装饰器

更多文章