这篇学习加总结的文章写了很久了,但是因为之前是使用马克飞象来写的,其中的图片保存在本地,再上传到博客上非常麻烦,于是就一直趴在电脑里。后来使用了MWeb之后,Mweb推出了图床的功能,懒癌的我找到了最爱,终于可以开心的将一些文章扔博客里去了。鉴于这篇文章太长,大约有两万五千多字,拆分成两篇,作为设计模式的学习理解。设计模式这个东西,说他厉害他确实在工程里占有很高的地位,但是不应当过分的信仰这种东西,他其实只是解决工程问题的有效途径,但并不一定是最佳的途径。

当然试图去寻找到最佳的途径,显示是要先对这些人们已经充分认同的设计模式有所理解。

策略模式(引子)

重新设计类结构,创造更多接口,而不是简单地增加进程。比如我们建立一个鸭子的超类,超类中拥有呱呱叫和飞的两个函数。因为我们认为鸭子普遍是可以飞和叫的。当我们在分别建立绿头鸭红头鸭子类的时候,可以直接继承了两种方法。但是此时如果要建立橡皮鸭呢?建立木头鸭呢?这个时候,我们就面临着这个超类的可复用的弹性问题了。所以,我们采用的方法是,把呱呱叫和飞行封装,形成借口,封装的行为里,定义一组行为,有可以飞不可以飞,呱呱叫吱吱叫或者是完全不叫,这一组行为,可以叫一个『算法族』。这样就大大提高了系统的弹性。

简单来讲,定义算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计的原则:

  1. 找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
  2. 针对接口编程,而不是针对实现编程。
  3. 多用组合,少用继承。(使用组合建立系统具有更大弹性)

观察者模式

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会收到通知并自动更新。观察者模式提供了一种对象设计,让主题和观察者之间松耦合。当两个对象松耦合,他们依然可以交互,但是不清楚彼此的细节。

有多个观察者的时候,不可以依赖特定的通知次序。

Java API 内置了观察者模式,java.util 包内包含了最基本的 Observer 接口和 Observalbe 类。不过 java.util.Observable 是一个类而不是接口,所以在实现上,他还有一些问题,限制了它的使用和福永,所以在使用中应当注意。(OBservable 是一个类,必须设计一个类继承它,如果某个类想要同时具有Observable 类和另外一个超类的行为,就会陷入两难,因为 Java 不支持多重继承。)

JDK 中 Swing 大量使用了观察者模式,很多GUI 框架也是如此。

我们常听说的MVC 其实就是观察者模式中的代表人物。

设计的原则(增):

  1. (找出变化,独立出来)在观察者模式中,会改变的是主题的状态,以及观察者的数目和类型。用这个模式,你可以改变依赖于主题状态的对象,却不必改变主题。这叫前提规则。
  2. (针对接口编程,而非针对实现)主题与观察者都是用接口:观察者利用主题的接口向主题注册,而主题利用观察者接口通知观察者。这样可以让两者之间运作正常,又同时具有松耦合的优点。
  3. (多用组合,少用继承)观察者利用『组合』将许多观察者组合进主题中,对象之间的这种关系不是通过继承产生的,而是在运行时利用组合的方式而产生的。
  4. 为了交互对象之间的松耦合设计而努力。松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。

装饰者模式

首先了解一个开放-关闭原则,是装饰者模式的一个重要设计原则,我们的目标是允许类更容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。如能实现这样的目标,这样的设计就具有弹性可以应对改变,可以接受新的功能来应对改变的需求。

装饰者模式动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

以咖啡馆为例:咖啡馆提供各种饮料,现在要设计一个订单系统,来优化他们的饮料供应要求。如果我们用最基本的类设计,让一个超类具有提供通用的方法,子类继承并实现cost() 等方法,这样我们就看到一个爆炸式的类继承图。

所以,这个时候我们考虑装饰者模式,如果一个顾客需要摩卡和奶泡深焙咖啡,那么我们就先拿一个深焙咖啡对象,然后用摩卡对象装饰它(包起来),以奶泡对象装饰它,最后调用 cost()方法,一来委托将调料的价钱加上去。如图:

在 Java 中, java.io 包就是一个使用装饰者模式最好的例子。

缺点

  1. 因为在设计中加入了大量的小类,所以导致不容易理解这种设计方式。
  2. 类型问题。人们在客户代码中依赖某种特殊类型,然后忽然导入装饰者,就会出现各种状况。
  3. 采用装饰者在实例化组件时,将增加代码的复杂度。

设计原则:

  1. 类应该对扩展开放,对修改关闭。

工厂模式

人们普遍认为工厂模式分三类,简单工厂模式,工厂方法模式,和抽象工厂模式,不过要澄清一下,其实简单工厂模式只是在概念上符合了工厂模式,而其实还存在有缺陷。简单工厂就是最简单的创造了一个工厂,在实例化对象的时候,由工厂来决定实例化哪一种对象。

以下面代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum CTYPE {COREA, COREB};
class SingleCore
{
public:
virtual void Show() = 0;
};
//单核A
class SingleCoreA: public SingleCore
{
public:
void Show() { cout<<"SingleCore A"<<endl; }
};
//单核B
class SingleCoreB: public SingleCore
{
public:
void Show() { cout<<"SingleCore B"<<endl; }
};
//唯一的工厂,可以生产两种型号的处理器核,在内部判断
class Factory
{
public:
SingleCore* CreateSingleCore(enum CTYPE ctype)
{
if(ctype == COREA) //工厂内部判断
return new SingleCoreA(); //生产核A
else if(ctype == COREB)
return new SingleCoreB(); //生产核B
else
return NULL;
}
};

这个设计的缺点在于,如果我们要增加新的类型,就需要进入工厂类中去修改,这就违反了我们上次提到的原则:类应当向扩展开放,向修改封闭

所以,此时我们的工厂方法模式就出现了,工厂方法模式的定义是:工厂模式的特点就是我们定义一个创建对象的接口,但是由子类来决定实例化的类是哪一个。工厂方法就是让类把实例化推迟到了子类。 工厂方法用来处理对象的创建,然后将这个行为封装到子类中去,本身并不进行实例化,这样的话,客户程序中关于超类的代码,就和子类对象创建的代码解耦了。

结合实例讲解就是,这家生产处理器核为自己设立了一个总厂,总厂并不做生产的活动,而是再开设一个工厂专门用来生产B型号的单核,和另一个工厂专门用来生产A型号的单核。这时,客户要做的是找好工厂,比如要A型号的核,就找A工厂要;否则找B工厂要,不再需要告诉工厂具体要什么型号的处理器核了。下面这个是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class SingleCore
{
public:
virtual void Show() = 0;
};
//单核A
class SingleCoreA: public SingleCore
{
public:
void Show() { cout<<"SingleCore A"<<endl; }
};
//单核B
class SingleCoreB: public SingleCore
{
public:
void Show() { cout<<"SingleCore B"<<endl; }
};
class Factory
{
public:
virtual SingleCore* CreateSingleCore() = 0;
};
//生产A核的工厂
class FactoryA: public Factory
{
public:
SingleCoreA* CreateSingleCore() { return new SingleCoreA; }
};
//生产B核的工厂
class FactoryB: public Factory
{
public:
SingleCoreB* CreateSingleCore() { return new SingleCoreB; }
};

在《HeadFirst设计模式》书中,利用另外一个例子讲述了这个问题,有一家披萨店,在纽约和芝加哥开了披萨的分店,而每个分店为了满足当地人的口味,有着当地口味的各种披萨。于是我们就看到了这个UML图:

这里边工厂方法是创造一个框架,让子类去决定如何实现。在工厂方法中,orderPizza() 方法提供了一般框架,以便创建披萨,orderPizza()方法依赖工厂方法创建具体类,然后制造出具体的披萨出来。而简单工厂的做法是可以将对象的创建封装起来,但是不具备工厂方法的弹性。这种方法,相比于简单工厂的模式,拥有了更多的弹性。

下面我们再引入一个原则:
设计原则:

  1. 要依赖抽象,不要依赖具体类。(依赖倒置原则 Dependency Inversion Principle)无论是高层组件,和低层组件,都应当依赖于抽象,而非具体的类。

下面几个指导方针,可以避免在设计中违反依赖倒置原则:

那么,下面我们就能引入抽象工厂模式, 抽象工厂模式,提供了一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。。下面这张图将有助于理解其中的关系。

我们看到,抽象工厂定义了一个接口,所有的具体工厂都必须实现此接口,这个接口包括了一组方法用来生产产品。下面回到那个披萨店的问题,下面这张图就更加复杂,而其中抽象出来的披萨原料工厂接口,正是整个抽象工厂概念的精髓。

Alt text

具体的披萨工厂负责生产披萨原料,每个工厂都知道如何产生符合自己区域的正确对象,而披萨店有两个具体事例,纽约披萨店和芝加哥披萨店,他们就是抽象工厂的客户。

工厂方法模式和抽象工厂模式的区别
工厂方法在创建对象的方法是利用继承,而抽象工厂是通过对象的组合。这意味着,利用工厂方法创建对象,需要扩展一个类,并覆盖它的工厂方法,这个工厂方法用来创建对象,而整个工厂方法模式,不过就是通过子类来创建对象,用户在使用时候,只需要知道他们所使用的抽象类型就可以了。而抽象工厂提供了一个用来创建一个产品家族的抽象类型,这个类型的子类定义了产品被生产的方法。要想使用这个工厂,必须先实例化它,然后将它传入一些针对抽象类型所写的代码中去。

所以,当你需要创建一个产品将组,想让制造的相关产品集合起来的时候,可以使用抽象工厂模式。而当你目前还不知道到底需要实例化哪些具体类,可以使用工厂方法模式,因为扩展和修改很快。

单件模式(Singleton Pattern)

看完了一个相当复杂的工厂模式,下面转入一个比较简单的模式,单件模式。定义如下:

单件模式确保一个类只有一个实例,并提供一个全局访问点。

有一些对象其实我们只需要一个,比方说:线程池(threadpool)、缓存(cache)、对话框、处理偏好设置、注册表(registry)的对象、日志对象、打印机显卡等设备的对象。 由于普通的全局变量,必须在程序已开始就创建好对象,那么如果这个对象非常的消耗资源,而在程序的运行过程中一直没有用到它,不就形成了浪费了么。所以使用单件模式,就可以在需要的时候去创建这个对象。

适用性

  1. 对于一个类,如果他比较大,而且这些资源可以被全局共享,就可以设计成单件模式。
  2. 对于一个类,需要对实例进行计数。可以在 Instance 中进行,并可以对实例的个数进行限制。
  3. 对于一个类,需要对其实例的具体行为进行控制。例如,期望返回的实例实际上是自己子类的实例。这样可以通过单件模式,对客户端代码保持透明。

单件模式,没有公开的构造器,是私有的。当为了取得实例,必须请求得到一个实例,而不是自行实例化得到一个一个实例。类中有一个静态方法 ,叫做GetInstance() ,调用这个方法,就可以让这个唯一的实例现身,这个实例也许是第一次创建,也许是已经创建了。下面是一个单件模式的通用写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
作用:保证一个class只有一个实体(instance),并为它提供一个全局唯一的访问点
*/
class singleton
{
public:
~singleton()
{//析构时使用
}
static singleton* getInstance()
{
if(_instance == NULL)
{
_instance = new singleton();
}
return _instance;
}
private:
static singleton *_instance;
private:
//最好将所有此类的实例化的进口全部堵死
singleton()
{
}
singleton(const singleton&)
{
}
singleton& operator=(const singleton &)
{
}
};
singleton *singleton::_instance = NULL;

看起来很美好,不过这个代码仍然存在问题。

  1. 释放的问题,上述例子中的_instance,不会自动释放,而需要手动去释放,尤其是做借口时候,需要告知使用方调用 delete singleton::getInstance()语句。
  2. 多线程使用的环境下,极有可能会出现同时创造了前后不一致的对象,失去了单件模式的本意,这个问题就严重了。

解决方法:
针对释放问题的解决方法:

  1. 调用 delete singleton::getInstance()语句。
  2. 注册一个atexit()函数,将释放内存的方法放进去,此方法可以将多个单件放在一起调用。
1
2
3
4
5
6
void releasefun()
{
delete singleton::getInstance();
}
//在使用完成后调用
atexit(releasefun);
  1. 使用智能指针,C++ STL中的auto_ptr就是一个。static auto_ptr<singleton> _instance;
  2. 利用c++ 内嵌类和一个静态成员自动释放机制,将这个类嵌入到单件模式的类中去。然后在getInstance() 中声明一个静态的实例对象。
1
2
3
4
5
6
7
8
9
10
class clearer
{
public:
clearer(){}
~clearer()
{
if(singleton::getInstance())
{
delete singleton::getInstance();
} } };

针对多线程的问题:
在这里引入一个著名的双检测锁机制,看到代码,一定会觉得非常的有想法。

1
2
3
4
5
6
7
8
9
10
11
12
13
static singleton* getInstance()
{
if(_instance == NULL)
{
//加入临界区
if(NULL == _instance)
{
_instance = new singleton();
}
//释放临界区
}
return _instance;
}

这里边这个临界区的思路,正好解决了多线程的问题。

同时,如果有多个类加载器存在的时候,很有可能创建各自不同的单件实例,这个时候,就要小心,应该自行指定类加载器,并指定同一个类加载器。

命令模式(Command Pattern)

命令模式将『请求』封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

一个简单的生活中的例子就是,我们去餐厅吃饭,通过服务员来点餐,具体谁来做这些菜还什么时候完成这些菜我们并不知道。而服务员则只是将我们下的订单,传递给厨师,具体的做菜过程是由厨师完成的。所以,『菜单请求者』和『菜单实现者—厨师』之间是解耦的。

图中的几个角色有:

在刚才那个餐厅的例子,我们建立一一的对应关系,这样理解起来就更加容易了。

下面我们看一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <vector>
using namespace std;
// 烤肉师傅
class RoastCook
{
public:
void MakeMutton() { cout << "烤羊肉" << endl; }
void MakeChickenWing() { cout << "烤鸡翅膀" << endl; }
};
// 抽象命令类
class Command
{
public:
Command(RoastCook* temp) { receiver = temp; }
virtual void ExecuteCmd() = 0;
protected:
RoastCook* receiver;
};
// 烤羊肉命令
class MakeMuttonCmd : public Command
{
public:
MakeMuttonCmd(RoastCook* temp) : Command(temp) {}
virtual void ExecuteCmd() { receiver->MakeMutton(); }
};
// 烤鸡翅膀命令
class MakeChickenWingCmd : public Command
{
public:
MakeChickenWingCmd(RoastCook* temp) : Command(temp) {}
virtual void ExecuteCmd() { receiver->MakeChickenWing(); }
};
// 服务员类
class Waiter
{
public:
void SetCmd(Command* temp);
// 通知执行
void Notify();
protected:
vector<Command*> m_commandList;
};
void Waiter::SetCmd(Command* temp)
{
m_commandList.push_back(temp);
cout << "增加订单" << endl;
}
void Waiter::Notify()
{
vector<Command*>::iterator it;
for (it=m_commandList.begin(); it!=m_commandList.end(); ++it)
{
(*it)->ExecuteCmd();
}
}
int main()
{
// 店里添加烤肉师傅、菜单、服务员等顾客
RoastCook* cook = new RoastCook();
Command* cmd1 = new MakeMuttonCmd(cook);
Command* cmd2 = new MakeChickenWingCmd(cook);
Waiter* girl = new Waiter();
// 点菜
girl->SetCmd(cmd1);
girl->SetCmd(cmd2);
// 服务员通知
girl->Notify();
return 0;
}

命令模式有几个常见的可适用功能:

适配器模式和外观模式(the Adapter and Facade Patterns)

比如我们中国的电压是220v,而美国的电压是110v,为了一个中国的电脑,能在美国使用,就必须需要一个变压器转换电压之后才可以使用,这就是真实世界里的适配器。而我们OO世界里的适配器和它是一个道理,就是将一个接口转换成另一个接口,以符合客户的期望。适配器让原本接口不兼容的类可以合作。

客户使用适配器的过程:

  1. 客户通过目标接口调用适配器的方法对适配器发出请求。
  2. 适配器使用被适配者接口把请求转换成被适配者的一个或多个调用接口。
  3. 客户接受到调用的结果,并未察觉到这一切是适配器在起转换作用。

下面是它的类图:

Adaptee类没有Request方法,而客户期待这个方法。为了使客户能够使用Adaptee类,提供一个中间环节,即类Adapter类,Adapter类实现了Target接口,并继承自Adaptee,Adapter类的Request方法重新封装了Adaptee的SpecificRequest方法,实现了适配的目的。

以下是一个简单的c++ 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
using namespace std;
// "ITarget"
class Target
{
public:
// Methods
virtual void Request(){};
};
// "Adaptee"
class Adaptee
{
public:
// Methods
void SpecificRequest()
{
cout<<"Called SpecificRequest()"<<endl;
}
};
// "Adapter"
class Adapter : public Adaptee, public Target
{
public:
// Implements ITarget interface
void Request()
{
// Possibly do some data manipulation
// and then call SpecificRequest
this->SpecificRequest();
}
};
int main()
{
// Create adapter and place a request
Target *t = new Adapter();
t->Request();
return 0;
}

适配器分为对象的适配器,和类的适配器。上边给出的类图,是对象适配器的图。而类适配器与其的差别是适配器继承了Target和Adaptee。而对象适配器利用组合的方式将请求传送给被适配者。下边是类适配器。由于类适配器使用了多重继承,所以在java上不能实施。

我们看到适配者模式的几个要点:

  1. 适配者模式主要用于『希望复用一些现存的类,但是接口又与复用环境要求不一致的情况』,在遗留代码复用,类库迁移等方面非常有用。
  2. 适配者模式有对象适配器和类适配器两种形式的实现结构,但是类适配器采用的是『多继承』的实现方式,带有不良的高耦合,所以一般不推荐采用。对象适配器采用『对象组合』的方式,更符合松耦合的精神。
  3. 适配者模式的实现可以非常的灵活,不必拘泥于两种结构,例如完全可以将适配者模式中的『现存对象』作为新的接口方法参数,来达到适配的目的。
  4. 适配者模式本身要求我们尽可能的使用『面向接口的编程』风格,这样才能在后期很方便的进行适配。

而与适配者模式很相近的一个模式,叫外观模式。而实际上他的作用其实是为了简化接口。比如我们建立了一套家庭影院,这套家庭影院里拥有各种各样的方法类,但是当我们想要看电影的时候,去逐个完成准备的动作,将变得非常的繁琐。而有效的方式,则是将一系列的任务和在一起,外观类将家庭影院的诸多组件视为一个子系统,通过调用这个子系统,来实现一个方法,这个方法包含了子系统的各种方法。

所以,我们知道,外观类并未将原来的子系统阻隔起来,只是提供了更简洁的接口,这种方法,也可以将客户从组件的子系统中解耦。

新的设计原则

  1. 最少知识原则,只和你的密友谈话。(意思是,当你设计一个系统,不管是任何对象,你都要注意它所交互的类有哪些,并注意它和这些类是如何交互的。)不要让太多的类耦合在一起,免得修改系统中的一部分,会影响到其他的部分。

最后我们再看一下 ,适配者模式,装饰模式,外观模式的区别:
适配者模式将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增加新的行为和责任。外观将一群对象包装起来以简化其接口。

模板方法模式(Template Method Pattern)

《设计模式》对模板方法模式的定义是:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以再不改变算法结构的情况下,重新定义算法中的某些步骤。

抽象模板角色(AbstractClass):
定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,他们是一个顶级逻辑的组成步骤。定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体的方法。这个就是我们定义了我们固定的操作顺序。

具体模板角色(ConcreteClass):
实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。

来自Head First 设计模式中的一个例子的c++实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template <typename T> class CaffeineBeverage //咖啡因饮料
{
public:
void PrepareRecipe() //咖啡因饮料冲泡法
{
BoilWater(); //把水煮沸
Brew(); //冲泡
PourInCup(); //把咖啡因饮料倒进杯子
AddCondiments(); //加调料
}
void BoilWater()
{std::cout << "把水煮沸" << std::endl;}
void Brew()
{static_cast<T *>(this)->Brew();}
void PourInCup()
{std::cout << "把咖啡倒进杯子" << std::endl;}
void AddCondiments()
{static_cast<T *>(this)->AddCondiments();}
};
class Coffee : public CaffeineBeverage<Coffee>
{
public:
void Brew()
{std::cout << "用沸水冲泡咖啡" << std::endl;}
void AddCondiments()
{std::cout << "加糖和牛奶" << std::endl;}
};
class Tea : public CaffeineBeverage<Tea>
{
public:
void Brew()
{std::cout << "用沸水浸泡茶叶" << std::endl;}
void AddCondiments()
{std::cout << "加柠檬" << std::endl;}
};
int main(void)
{
std::cout << "冲杯咖啡:" << std::endl;
Coffee c;
c.PrepareRecipe();
std::cout << std::endl;
std::cout << "冲杯茶:" << std::endl;
Tea t;
t.PrepareRecipe();
return 0;
}

其实在模板父类里,我们可以定义一个『默认不做事』的方法,我们称这种方法为『Hook』钩子,子类可以视情况决定要不要覆盖它们。这就让子类在实现的时候有了更多的灵活性。
钩子

适用性:

  1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
  2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke和Johnson所描述过的“重分解以一般化”的一个很好的例子。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  3. 控制子类扩展。模板方法只在特定点调用“Hook”操作,这样就只允许在这些点进行扩展。

新的设计原则
戏称为好莱坞原则,别调用我们,我们会调用你。我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。模板方法模式就是典型的好莱坞原则。其他还有工厂方法,观察者等等。

迭代器(Iterator Pattern)

现在有两种储存数据的模式,一种用的是ArrayList , 而另一种则用的是数组,两种结构所拥有的方法各不相同。假如我们想要将两种数据存储方式整合在一起,然后用相同的方法使用遍历它们的时候,这个时候就要用到了鼎鼎大名的迭代器了。

当我们说『集合』(collection)的时候,我们指的是一群对象。其储存方式可以使各种各样的数据结构,如,列表,数组,散列表。无论用什么方式,一律可以视为集合,有时候也称为『聚合』(aggregate)。

迭代器模式的精髓:提供一个方法顺序访问一个聚合对象的各个元素,而又不暴露其内部的表示。把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。

重要角色:
迭代器角色:迭代器负责定义访问和遍历元素的接口。
具体迭代器角色(Concrete Iterator) : 具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置。
集合角色(Aggregate): 集合角色负责提供创建具体迭代器角色的接口。
具体集合角色(Concrete Aggregate): 具体集合角色实现创建具体迭代器角色的接口——这个具体迭代器角色与该集合的结构有关。

回到实例中去看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream>
#include <vector>
using namespace std;
template<class Item>
class Iterator
{
public:
virtual void first()=0;
virtual void next()=0;
virtual Item* currentItem()=0;
virtual bool isDone()=0;
virtual ~Iterator(){}
};
template<class Item>
class ConcreteAggregate;
template<class Item>
class ConcreteIterator : public Iterator <Item>
{
ConcreteAggregate<Item> * aggr;
int cur;
public:
ConcreteIterator(ConcreteAggregate<Item>*a):aggr(a),cur(0){}
virtual void first()
{
cur=0;
}
virtual void next()
{
if(cur<aggr->getLen())
cur++;
}
virtual Item* currentItem()
{
if(cur<aggr->getLen())
return &(*aggr)[cur];
else
return NULL;
}
virtual bool isDone()
{
return (cur>=aggr->getLen());
}
};
template<class Item>
class Aggregate
{
public:
virtual Iterator<Item>* createIterator()=0;
virtual ~Aggregate(){}
};
template<class Item>
class ConcreteAggregate:public Aggregate<Item>
{
vector<Item >data;
public:
ConcreteAggregate()
{
data.push_back(1);
data.push_back(2);
data.push_back(3);
}
virtual Iterator<Item>* createIterator()
{
return new ConcreteIterator<Item>(this);
}
Item& operator[](int index)
{
return data[index];
}
int getLen()
{
return data.size();
}
};
int main()
{
Aggregate<int> * aggr =new ConcreteAggregate<int>();
Iterator<int> *it=aggr->createIterator();
for(it->first();!it->isDone();it->next())
{
cout<<*(it->currentItem())<<endl;
}
delete it;
delete aggr;
return 0;
}

迭代器的特点:

新的设计原则
单一责任: 一个类应该只有一个引起变化的原因。

script>