组合模式

组合模式

组合模式的核心思想如下:

  1. 统一单个对象和组合对象的处理方式:让客户端可以以相同的方式处理单个对象(叶节点)和组合对象(容器节点)。
  2. 树形结构:允许将对象组合成树形结构来表示”部分-整体”的层次关系。
  3. 递归组合:组合中的对象可以是单个对象,也可以是组合对象。

其典型结构如下:

  1. 组件(Component):为所有对象定义统一接口
  2. 叶节点(Leaf):表示单个对象,没有子节点
  3. 组合节点(Composite):包含子节点的组件

示例

#include <vector>
#include <iostream>

struct Neuron;

// 基类模板,使用CRTP模式
template<typename Self>
struct SomeNeuron
{
    template<typename T>
    void connect_to(T& other)
    {
        for (Neuron& from : *static_cast<Self*>(this))
        {
            for (Neuron& to : other)
            {
                from.out.push_back(&to);
                to.in.push_back(&from);
            }
        }
    }
};

// 神经元类
struct Neuron : public SomeNeuron<Neuron>
{
    std::vector<Neuron*> in, out;
    unsigned int id;

    Neuron()
    {
        static int id = 1;
        this->id = id++;
    }
    
    // 让Neuron可迭代,实现begin()和end()
    Neuron* begin() { return this; }
    Neuron* end() { return this + 1; }
    
    // 打印神经元信息的函数
    friend std::ostream& operator<<(std::ostream& os, const Neuron& obj)
    {
        os << "Neuron " << obj.id << std::endl;
        os << "  Connections from: ";
        for (Neuron* n : obj.in)
            os << n->id << " ";
        os << std::endl;
        
        os << "  Connections to: ";
        for (Neuron* n : obj.out)
            os << n->id << " ";
        os << std::endl;
        
        return os;
    }
};

// 神经元层,继承自vector<Neuron>
struct NeuronLayer : std::vector<Neuron>, SomeNeuron<NeuronLayer>
{
    NeuronLayer(int count)
    {
        while (count-- > 0)
        {
            emplace_back(Neuron{});
        }
    }
    
    friend std::ostream& operator<<(std::ostream& os, const NeuronLayer& obj)
    {
        os << "Layer with " << obj.size() << " neurons" << std::endl;
        return os;
    }
};

这段代码有很多有趣的地方,首先我们的层次结构由神经元(Neuron)为标量组成,神经层(NeuronLayer)由多个神经元组成。

然后我们需要对神经元之间连接的函数,首先可以想到有 神经元<—>神经元,或者 神经元<—>神经层,也可 神经层<—>神经层 之间相连接。

这里使用组合模式的方法去设计 connect_to ,也就是在这段代码中 SomeNeuron 是之前所述中的组件,Neuron作为叶节点,NeuronLayer就是组合节点

其设计模式的目的就是让用户能用相同的接口处理单个对象和对象组合,而不需要单独为两者或两者之间单独编写代码。


这里需要单独解释一下SomeNeuron的设计,这是一个有趣的设计不是吗:

// 基类模板,使用CRTP模式
template<typename Self>
struct SomeNeuron
{
    template<typename T>
    void connect_to(T& other)
    {
        for (Neuron& from : *static_cast<Self*>(this))
        {
            for (Neuron& to : other)
            {
                from.out.push_back(&to);
                to.in.push_back(&from);
            }
        }
    }
};

CRTP:Curiously Recurring Template Pattern 也称奇异递归模板模式,特点在于继承自 将派生类自身作为模板参数 的基类,其基本形式如下:

template <typename Derived>
class Base {
    // 基类可以使用Derived类的方法或属性
    // 通过static_cast<Derived*>(this)
};

class Derived : public Base<Derived> {
    // 派生类的实现
};

它的目的在于让基类获得访问其派生类的特性,在编译期就决定了正确的函数调用,实现的是静态多态。

在我们的代码中,如此书写可以让

for (Neuron& from : *static_cast<Self*>(this))
{
    ...
}

在第一层循环中将 基类 转化为 派生类从而正确地调用相应的迭代函数。如此在后续如果要添加其他种类的神经组合类如:NeuronRing,那么我们也只需要设计对应的迭代函数 begin()/end() 就可以在不修改 connect_to 的情况下完成连接。这就十分符合我们的开闭原则,不会对已经实现的功能做任何修改就可以进行扩展。

封装组合模式

在上述代码中我们并没有强制告诉用户其实我们需要为标量实现begin()/end(),那么我们可以将这个功能进行进一步封装。

template<typename T>
class Scalar : public SomeNeurons<T>
{
public:
    T* begin() { return reinterpret_cast<T*>(this); }
    T* end() { return reinterpret_cast<T*>(this) + 1; }
};

class Neuron : public Scalar<Neuron>
{
    // as before
}

C++20 concept

C++20 更新了 concep 语义,让我们试试看能不能应用到这个示例上。

因为 connect_to 函数运用了迭代器,所以我们必须给 SomeNeuron 的对象派生类实现 begin()/end()。可以运用 C++20 的语义对这个条件进行模板参数的限制:

template<typename T>
concept Iterable = requires(T& t)
{
    t.begin();
    t.end();
} || requires(T& t)
{
    begin(t);
    end(t);
};

看起来不错对吧,这样就能写如下的代码:

template<Iterable Self>
struct SomeNeurons
{
    template<Iterable T>
    void connect_to(T& other)
    {
        // as before
    }
};

但是物体来了,模板函数那里的Iterable是可以实现的,但是将 Self 声明为 Iterable 却会出现问题。

考虑之前定义的 Scalar 类,它继承自 SomeNeurons< T > 所以我们需要将 T 约束为可迭代的:

template<Iterable T>
class Scalar : public SomeNeurons<T>
{
    // as before
};

但是这样的方法不能让我们定义 Neuron 类:

struct Neuron : Scalar<Neuron>

按要求我们需要 Neuron 本身具备迭代能力,但事实上它是通过继承 Scalar 获得的迭代能力。Neuron 类需要先继承 Scalar< Neuron > 才能满足 Iterable 概念,但 Scalar< Neuron > 的基类 SomeNeurons< Neuron > 又要求 Neuron 已经满足 Iterable 概念,这形成了无法解决的循环依赖。

可能基于 concept 的 CRTP 确实是不可能实现的吧。