|
| 1 | +### 组合 |
| 2 | + |
| 3 | +#### 多个属性 |
| 4 | + |
| 5 | +组合设计模式通常适用于整个类,一个对象通常由多个对象构成。举个例子,方便理解。在一个游戏中,每个生物都有不同的强度值、敏捷值、智力值等,这就很容易定义: |
| 6 | +```c++ |
| 7 | +class Creature{ |
| 8 | + int strength, agility, intelligence; |
| 9 | +public: |
| 10 | + int get_strength() const |
| 11 | + { |
| 12 | + return strength; |
| 13 | + } |
| 14 | + |
| 15 | + void set_strength(int strength){ |
| 16 | + Creature::strength = strength; |
| 17 | + } |
| 18 | + |
| 19 | + int get_agility() const |
| 20 | + { |
| 21 | + return agility; |
| 22 | + } |
| 23 | + |
| 24 | + void set_agility(int agility){ |
| 25 | + Creature::agility = agility; |
| 26 | + } |
| 27 | + |
| 28 | + int get_intelligence() const |
| 29 | + { |
| 30 | + return intelligence; |
| 31 | + } |
| 32 | + |
| 33 | + void set_intelligence(int intelligence){ |
| 34 | + Creature::intelligence = intelligence; |
| 35 | + } |
| 36 | +}; |
| 37 | +``` |
| 38 | +接下来我们想要对这些属性进行操作,例如求多个属性的最大值、平均值、总和,如下: |
| 39 | + |
| 40 | + |
| 41 | +```c++ |
| 42 | +class Creature{ |
| 43 | + //其他的数据成员 |
| 44 | + int sum() const{ |
| 45 | + return strength + agility + intelligence; |
| 46 | + } |
| 47 | + |
| 48 | + double average const{ |
| 49 | + return sum() / 3.0; |
| 50 | + } |
| 51 | + int max() const{ |
| 52 | + return ::max(::max(strength, agility),intelligence); |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | +
|
| 57 | +然而这样并不理想,原因如下: |
| 58 | +
|
| 59 | + 1)在计算所有统计数据总和时候,我们容易犯错并且忘记其中一个 |
| 60 | + |
| 61 | + 2)3.0是代表属性的数目,在这里被设计成一个固定值 |
| 62 | + |
| 63 | + 3)计算最大值时,我们必须构建一对std::max() |
| 64 | +
|
| 65 | +想象一下如果再增加一个新的属性,这个时候我们需要对sum(),average(),max()重构,这是十分糟糕的。 |
| 66 | +
|
| 67 | +如何避免?如下: |
| 68 | +
|
| 69 | +```c++ |
| 70 | +class Creature{ |
| 71 | + enum Abilities {str, agl, intl, count}; |
| 72 | + array<int,count> abilities; |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +上面的枚举定义了一个名为count的额外值,标记着有多少个属性。现在我们这样定义属性的get和set方法: |
| 77 | +```c++ |
| 78 | +int get_strength() const { return abilities[str];} |
| 79 | + |
| 80 | +void set_strength(int value){ |
| 81 | + abilities[str]=value; |
| 82 | +} |
| 83 | +//对于其他属性同样 |
| 84 | +``` |
| 85 | +现在再让我们看看对sum(),average(),max()的计算,看看有什么改进: |
| 86 | + |
| 87 | +```c++ |
| 88 | +int sum() const{ |
| 89 | + return accumulate(abilities.begin(), abilities.end(),0); |
| 90 | +} |
| 91 | + |
| 92 | +double average() const{ |
| 93 | + return sum() / (double)count; |
| 94 | +} |
| 95 | + |
| 96 | +int max() const{ |
| 97 | + return *max_element(abilities.begin(), abilities.end()); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +是不是更棒了,不仅使代码更容易编写和维护,而且添加新属性时候,十分简单,总量根本不需要去改变,并不会影响sum(),average(),max()。 |
| 102 | + |
| 103 | + |
| 104 | +#### 组合图形对象 |
| 105 | + |
| 106 | +想想诸如PowerPoint等应用程序,在哪里您可以选择多个不同的对象并将其作为一个拖动。然而如果要选一个一个对象,您也可以抓取该对象。渲染也是相同的:您可以呈现单个图形对象,或者您可以将多个形状组合在一起,并将其绘制为一个组。这种方法的实现相当容易,因为它只是依赖于单个接口,如下所示: |
| 107 | + |
| 108 | +```c++ |
| 109 | +struct GraphicObject{ |
| 110 | + virtual void draw() = 0; |
| 111 | +}; |
| 112 | +``` |
| 113 | + |
| 114 | +现在从名字来看,你可能认为它总是代表一个单独的项目。然而,想想看:几个矩形和圆形组合在一起代表一个组合图形对象。正如我可以定义的,比如说,一个圆: |
| 115 | + |
| 116 | +```c++ |
| 117 | +struct Circle : GraphicObject |
| 118 | +{ |
| 119 | + void draw() override |
| 120 | + { |
| 121 | + std::cout << "Circle" << std::endl; |
| 122 | + |
| 123 | + } |
| 124 | +}; |
| 125 | +``` |
| 126 | +同样,我们可以定义一个由几个其他图形对象组成的图形对象。是的,关系可以无限递归: |
| 127 | + |
| 128 | +```c++ |
| 129 | +struct Group : GraphicObject |
| 130 | +{ |
| 131 | + std::string name; |
| 132 | + explicit Group(const std::string& name) : name(name){} |
| 133 | + |
| 134 | + void draw() override |
| 135 | + { |
| 136 | + std::cout << "Group" << name.c_str() << " contains:" << std::endl; |
| 137 | + for(auto&& o:obejct) |
| 138 | + o->draw(); |
| 139 | + } |
| 140 | + |
| 141 | + std::vector<GraphicObject*> objects; |
| 142 | +} |
| 143 | +``` |
| 144 | +单个圆和任意组都可以绘制,只要他们实现了draw()函数。组中有一个指向其他图形对象的指针数组,通过其访问多个对象的draw()方法,来渲染自身。 |
| 145 | +以下是编程接口的使用方法: |
| 146 | +```c++ |
| 147 | +Group root("root"); |
| 148 | +Circle c1, c2; |
| 149 | +root.obejects.push_back(&c1); |
| 150 | +
|
| 151 | +Group subgroup("sub"); |
| 152 | +subgroup.objects.push_back(&c2); |
| 153 | +
|
| 154 | +root.obejcts.push_back(&subgroup); |
| 155 | +
|
| 156 | +root.draw(); |
| 157 | +``` |
| 158 | +前面的代码生成以下输出: |
| 159 | +```c++ |
| 160 | +Group root contains: |
| 161 | +Circle |
| 162 | +Group sub contains: |
| 163 | +Circle |
| 164 | +``` |
| 165 | +这是组合设计模式最简单的实现,尽管我们自己已经定义了一个定制接口。现在,如果我们尝试采用其他一些更标准化的迭代对象的方法,这个模式会是什么样子呢? |
| 166 | + |
| 167 | +#### 神经网络 |
| 168 | +机器学习是热门的新事物。机器学习中的一部分是使用人工神经网络:试图模仿神经元在我们大脑中工作方式的软件结构。 |
| 169 | +神经网络的核心概念当然是神经元。神经元可以根据其输入产生(通常是数字)输出,我们可以将该值反馈给网络中的其他连接。我们将只关注连接,所以我们将这样对神经元建模: |
| 170 | + |
| 171 | +```c++ |
| 172 | +1 struct Neuron |
| 173 | +2 { |
| 174 | +3 vector<Neuron*> in, out; |
| 175 | +4 unsigned int id; |
| 176 | +5 |
| 177 | +6 Neuron() |
| 178 | +7 { |
| 179 | +8 static int id = 1; |
| 180 | +9 this->id = id++; |
| 181 | +10 } |
| 182 | +11 }; |
| 183 | +``` |
| 184 | +我在id字段输入了身份。现在,你可能想做的是把一个神经元连接到另一个神经元上,这可以用 |
| 185 | +```c++ |
| 186 | +1 template<> void connect_to<Neuron>(Neuron& other) |
| 187 | +2 { |
| 188 | +3 out.push_back(&other); |
| 189 | +4 other.in.push_back(this); |
| 190 | +5 } |
| 191 | +``` |
| 192 | +这个函数造当前神经元和另一个神经元之间建立了联系。目前为止,一切顺利。现在,假设我们也想创建神经元层。一个层很简单,就是特定数量的神经元组合再一起。 |
| 193 | +```c++ |
| 194 | +1 struct NeuronLayer : vector<Neuron> |
| 195 | +2 { |
| 196 | +3 NeuronLayer(int count) |
| 197 | +4 { |
| 198 | +5 while (count --> 0) |
| 199 | +6 emplace_back(Neuron{}); |
| 200 | +7 } |
| 201 | +8 }; |
| 202 | +``` |
| 203 | +看起来不错。但是现在有一个小问题。问题是这样的:我们希望神经元能够连接到神经元层。总的来说,我们希望像这样能够奏效: |
| 204 | +```c++ |
| 205 | +1 Neuron n1, n2; |
| 206 | +2 NeuronLayer layer1, layer2; |
| 207 | +3 n1.connect_to(n2); |
| 208 | +4 n1.connect_to(layer1); |
| 209 | +5 layer1.connect_to(n1); |
| 210 | +6 layer1.connect_to(layer2); |
| 211 | +``` |
| 212 | +如您所见,我们有四个不同的案例需要处理: |
| 213 | +1、神经元连接到另一个神经元 |
| 214 | +2、神经元连接到神经元层 |
| 215 | +3、神经元层连接到神经元 |
| 216 | +4、神经元层连接到另一个神经元层 |
| 217 | + |
| 218 | +正如你所猜到的,我们不可能对connect_to()函数进行四次重载。如果有三个不同的类,你会考虑创建九个函数吗?我不这么认为。相反,我们要做的是在基类中插入槽。由于多重继承,我们完全可以做到这一点。那么,下面呢? |
| 219 | + |
| 220 | +```c++ |
| 221 | + 1 template <typename Self> |
| 222 | + 2 struct SomeNeurons |
| 223 | + 3 { |
| 224 | + 4 template <typename T> void connect_to(T& other) |
| 225 | + 5 { |
| 226 | + 6 for (Neuron& from : *static_cast<Self*>(this)) |
| 227 | + 7 { |
| 228 | + 8 for (Neuron& to : other) |
| 229 | + 9 { |
| 230 | +10 from.out.push_back(&to); |
| 231 | +11 to.in.push_back(&from); |
| 232 | +12 } |
| 233 | +13 } |
| 234 | +14 } |
| 235 | +15 }; |
| 236 | +``` |
| 237 | +connect_to的实现绝对值得探讨。如您所见,它是一个模板成员函数,接受T,然后成对地迭代*this和T&的神经元,互相连接每个。但是有一个警告,我们不能只迭代*this,因为这会给我们一个SomeNeurons&和我们真正要找的类型。 |
| 238 | +这就是我们为什么被迫让一些神经元成为一个模板类,其中模板参数Self指的是继承类。然后我们在取消引用和迭代内容之前,将this指针转换为Self*。SomeNeurons<Neuron>是为了实现方便而付出的小小代价。 |
| 239 | +剩下的就是在Neuron和NeuronLayer中实现SomeNeurons::begin()和end(),让基于范围的循环真正工作。 |
| 240 | +由于NeuronLayer继承自vector,因此不用显示实现begin()/end(),它已经自动存在。但是神经元本身确实需要一种迭代的方法。它需要让自己成为唯一可重复的元素。这可以通过以下方式完成: |
| 241 | +```c++ |
| 242 | +1 Neuron* begin() override { return this; } |
| 243 | +2 Neuron* end() override { return this + 1; } |
| 244 | +``` |
| 245 | +正是这个神奇的东西让SomeNeurons::connect_to()成为可能。简单来说,我们使得单个对象的行为像一个可迭代的对象集合。这允许以下所有用途: |
| 246 | + |
| 247 | +```c++ |
| 248 | +1 Neuron neuron, neuron2; |
| 249 | +2 NeuronLayer layer, layer2; |
| 250 | +3 |
| 251 | +4 neuron.connect_to(neuron2); |
| 252 | +5 neuron.connect_to(layer); |
| 253 | +6 layer.connect_to(neuron); |
| 254 | +7 layer.connect_to(layer2); |
| 255 | +``` |
| 256 | +更不用说,如果您要引入一个新的容器(比如NeuronsRing),您所要做的就是从SomeNeurons<NeuronRing>继承,实现begin()/end(),新的类将立即连接到所有的神经元和神经元层。 |
| 257 | + |
| 258 | +#### 总结 |
| 259 | +复合设计模式允许我们为单个对象和对象集合提供相同的接口。这可以通过显式使用接口成员来完成,也可以通过duck typing(在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定)来完成。例如基于范围的for循环并不需要您继承任何东西,而是通过实现begin()和end()。 |
| 260 | +正是这些begin()/end()成员允许标量类型伪装成“集合”。同样有趣的是,我们的connect_to()函数的嵌套for循环能够将这两个构造连接在一起,尽管它们具有不同的迭代器类型:Neuron返回Neuron*而NeuronLayer返回vector::iterator——这两者并不完全相同。哈哈,这就是模板的魅力。 |
| 261 | +最后,我必须承认,只有当你想拥有一个单一成员函数时,所有这些跳跃才有必要。如果您可以调用一个全局函数,或者如果您对有多个connect_to()实现感到满意,那么基类SomeNeurons并不是必要的。 |
0 commit comments