Skip to content

Commit 4edb149

Browse files
authored
Merge pull request liuzengh#7 from SayHelloRudy/command
Translate composite and command
2 parents f8ae020 + 6e01456 commit 4edb149

File tree

2 files changed

+610
-0
lines changed

2 files changed

+610
-0
lines changed

docs/chapter-08-composite.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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

Comments
 (0)