精读C++20设计模式——结构型设计模式:组合模式

​ 组合模式不是说的将对象组合起来构成一个新对象——他不是这个意思!但是实际上,他也确实是这个意思:我这么说很奇怪。因为组合模式不打算将成员们物理组合,而是逻辑组合。举个例子:我们想要为不同对象相似逻辑提供同一接口。组合模式就是在这个时候发挥它的功能的。

​ 我认为作者也是觉得这样的表达实在是太拗口和晦涩。这里我们跟随作者一起来动手把实验搞定。

先说说什么是组合模式吧!

组合模式(Composite)在 GoF《设计模式》中被归类为结构型设计模式。它的核心思想是:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性

换句话说:组合模式并不是“物理组合”成员,而是通过逻辑组合来提供统一接口。它让调用者不必关心对象是单个元素还是一簇元素。在 C++20 中,我们既可以利用 std::arraystd::vector 等容器简化属性组合,也可以通过虚函数、智能指针来优雅实现对象树结构。

简化属性访问——组合同一类对象为数组

​ 现在,我们打算做一个游戏。还是挺复杂的——

class Creature
{
	int strength, agility, intelligence;
public:
    int get_strength() const { return strength; }
    void set_strength(const int _strength) { strength = _strength; }
    // 然后后面咱们省略一大堆接口,太繁琐了
};

​ 写的时候你就发现了,实在是太抽象了。Getter Setter一大堆,但是麻烦的事情没有结束。假设我们现在准备分析Creature的自身特性——比如说我们来看看这些能力的平均分配,最出色的是哪个能力的时候——我们必须要发现,我们不得不手动处理:

int sum() const {
	return strength + agility + intelligence;
}

double simple_avg() const { return sum() / 3.0; }

int max() const {return ::max(::max(strength, agility), intelligence); }

​ 妈耶!要是后面扩展我们的属性的时候,我相信你会抓狂的!

​ 我们仔细看看这种情况——我们注意到,strength,agility和intelligence本身就是一种能力,不是吗?至少在这个场景。所以,我们完全可以做一个分析仪:

// 原书在这里将他们耦合进了Creature中,笔者不太赞同,毕竟——分析Creature的能力不属于Creature自身的诉求

class CreatureAnalyzer
{
public:  
    enum Abilities { strength, agility, intelligence}
    // 我不赞成将表达最大的Abilities个数放到Abilities的枚举中,这是一种误用
    // 现代C++中已经支持了更好的constexpr,咱们用这个
    static constexpr const size_t ABILITIES_MAX = static_cast<int>(intelligence) + 1;
    CreatureAnalyzer(const Creature& analyzee){ ... }
    
    ...
    
private:
	std::array<int, ABILITIES_MAX>  abilities_containers;  
};

​ 太棒了,现在,我们就把这些零散的对象在分析领域上组合成了一个属性集合。以至于我们马上就能用上标准库的代码了:

int sum() const {
	return std::accumulate(abilities_containers.begin(), 
                           abilities_containers.end(), 0);
}

double avg() const { return sum() / (double)ABILITIES_MAX; }

int max() const { return *max_element(abilities_containers.begin(), 
                           			 abilities_containers.end()); }

​ 看到我们的做法了嘛?对于对象们本身,如果他们就是一个东西(int),或者是我们在关心的上下文中可以转化为同一个东西,那么,我们就把这些对象们集合为一个数组进行处理。

提示:现代C++可以做的更出色一些:

#include <array>
#include <numeric>
#include <algorithm>
#include <functional>
#include <iostream>

struct Creature {
 int strength, agility, intelligence;
 int get_strength() const { return strength; }
 int get_agility() const { return agility; }
 int get_intelligence() const { return intelligence; }
};

// 通用 Analyzer:传入一组可调用对象(成员指针 / lambda)
template<typename T, typename... Getters>
class Analyzer {
public:
 static constexpr std::size_t N = sizeof...(Getters);
 std::array<int, N> vals;

 Analyzer(const T& obj, Getters... getters)
     : vals{ std::invoke(getters, obj)... } {}

 int sum() const { return std::accumulate(vals.begin(), vals.end(), 0); }
 double avg() const { return sum() / static_cast<double>(N); }
 int max() const { return *std::max_element(vals.begin(), vals.end()); }
};

template<typename T, typename... G>
auto make_analyzer(const T& t, G... g) {
 return Analyzer<T, G...>(t, g...);
}

关于C++20的编程细节 + 模板编程 + 函数式,笔者打算后续专门开一个博客系列分享。这里感兴趣的朋友可以先自行了解。

简化编程接口——组合共同基类的子类对象为数组

​ 但是上面的条件出现的相对苛刻。我们更多有可能遇到的是——对于具备相同父类的子类集合,绘制的时候完全可以进行聚合,这个例子就很简单——

struct GeoObject {virtual void draw() = 0; }
struct Rectangle : public GeoObject { ... };
struct Circle : public GeoObject { ... }; //

struct Group : GeoObject {
  	std::string name;
    explicit Group(const std::string& name) { ... }
    void draw() override { 
    	for(auto&& each : objects) each->draw();
    } 
    void add(const GeoObject* p) { objects.emplace_back(p); }
private:
    std::vector<GeoObject*> objects;
};

// process the sessions
Group pRoot("root");
pRoot.add(new Rectangle());

Group sub("AnyCast");
sub.add(new Circle());
pRoot.add(&sub);

pRoot->draw();

​ 看到了嘛?我们将一簇对象模拟成为一个对象,这样的话,对象就具备的了层级的性质!我们实际上构造了不光是一个数组,更加像是一个多叉树!

总结

我们在解决什么问题?

其实说穿了就一个:当对象的线性组合带来API的数量爆炸的时候,我们如何进行有效的扩展。

咱们如何解决的?

把一类对象聚合为具备相同接口的一个对象,本质上接口成立要求我们需要视这样的组合对象也属于对象领域本身。

方案评估?

优点

  • 统一性:叶子节点与组合节点对外接口一致,客户端无需区分,极大简化调用逻辑。
  • 可扩展性强:新增叶子节点或组合节点时,不需要修改已有代码,符合开闭原则。
  • 层次清晰:天然适合表达树形结构,符合人类对“整体—部分”关系的直观理解。

缺点

  • 类型安全降低:叶子与组合节点接口一致,有时可能导致客户端调用了无意义的方法(例如叶子节点调用 add())。
  • 设计复杂度增加:为了保证统一接口,某些方法在不同节点类型中实现会变得冗余或不合理。
  • 性能开销:在层级特别深的结构中,递归遍历可能带来额外性能损耗。

适用场景

  • 表示 整体–部分 的树状层级结构(如文件系统、GUI 组件树、组织架构)。
  • 客户端希望以 一致方式 使用叶子与组合对象,而不关心具体对象类型。
  • 系统需要动态地增加/删除树节点,并保持对外操作简单统一。