学习软件设计,向OO高手迈进!
设计模式(Design pattern)是软件开发人员在软件开发过程中面临的一般问题的解决方案。
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
是前辈大神们留下的软件设计的"招式"或是"套路"。
什么是迭代器模式
在本文末尾会给出解释,待耐心看完demo再看定义,相信你会有更深刻的印象
实例讲解
背景
爆炸性新闻:隔壁的餐厅和煎饼屋被A公司收购了,A公司想用煎饼屋菜单当做早餐的菜单,用餐厅菜单当做午餐的菜单。
餐厅有餐厅的代码实现自己的菜单,煎饼屋也有自己的代码实现菜单。
A公司找到了我们,让我们帮它实现一个能应对顾客需要打印定制的菜单的功能。
目前我们得知,餐厅和煎饼屋都同意实现 MenuItem,MenuItem 是A公司的菜单类
菜单类包含了名称、叙述、是否为素食,还有价格。来看看代码
class MenuItem {
public:
MenuItem(string name, string description, bool vegetarian, double price) {
m_strName = name;
m_strDescription = description;
m_bVegetarian = vegetarian;
m_fPrice = price;
}
string GetName(void) {
return m_strName;
}
string GetDescription(void) {
return m_strDescription;
}
bool IsVegetarian(void) {
return m_bVegetarian;
}
double GetPrice(void) {
return m_fPrice;
}
private:
string m_strName;
string m_strDescription;
bool m_bVegetarian;
double m_fPrice;
};
再来看看餐厅和煎饼屋的菜单实现
煎饼屋的菜单实现,用的是 vector 容器来存储菜单对象,他们认为这样可以轻易的扩展菜单。
class PancakeHouseMenu {
public:
PancakeHouseMenu() {
m_aMenuItems.clear();
// 炒蛋吐司煎饼
AddItem("A Pancake", "Pancakes with scrambled eggs and toast", true, 2.99);
// 煎蛋香肠煎饼
AddItem("B Pancake", "Pancakes with fried eggs and sausage", false, 2.99);
// 蓝莓煎饼
AddItem("C Pancake", "Pancakes with fresh blueberries", true, 3.49);
// 草莓华夫饼
AddItem("D Pancake", "Waffles with strawberries", true, 3.59);
}
void AddItem(string name, string description, bool vegetarian, double price) {
MenuItem *pMenuItem = new MenuItem(name, description, vegetarian, price);
m_aMenuItems.push_back(pMenuItem);
}
vector<MenuItem *> GetMenuItems(void) {
return m_aMenuItems;
}
private:
vector<MenuItem *> m_aMenuItems; // 容器的形式
};
餐厅的菜单实现,用的是数组来存储菜单对象,他们认为这样可以控制菜单的长度,而且在取出菜单的时候,也不需要强转。
class DiningRoomMenu {
public:
DiningRoomMenu() {
memset(m_aMenuItems, 0, sizeof(m_aMenuItems));
m_nNumberOfItems = 0;
// 素食全麦培根
AddItem("A Dinner", "Vegetarian bacon with lettuce & tomato on whole wheat", true, 2.99);
// 全麦培根
AddItem("B Dinner", "Bacon with lettuce & tomato on whole wheat", false, 2.99);
// 例汤
AddItem("C Dinner", "Soup of the day, with a side of potato salad", false, 3.29);
// 热狗
AddItem("D Dinner", "Hotdog with saurkraut topped with cheese", false, 3.05);
}
void AddItem(string name, string description, bool vegetarian, double price) {
MenuItem *pMenuItem = new MenuItem(name, description, vegetarian, price);
if(m_nNumberOfItems >= m_nMaxItems) {
printf("Sorry, menu is full! Can not add item to menu!\n");
} else {
m_aMenuItems[m_nNumberOfItems] = pMenuItem;
m_nNumberOfItems++;
}
}
MenuItem** GetMenuItems(int &maxItems) {
maxItems = m_nMaxItems;
return m_aMenuItems;
}
private:
const static int m_nMaxItems = 6;
int m_nNumberOfItems;
MenuItem *m_aMenuItems[m_nMaxItems]; // 数组的形式
};
餐厅和煎饼屋在菜单的存储方式上花费了很多时间和代码,煎饼屋有许多的菜单代码,都依赖于这个 vector 容器,所以他们不希望重写全部的代码;同样,餐厅也有很多菜单代码都依赖于这个数组,他们没空重写这么多代码
有两种不同的菜单表现方式,这会带来什么问题?
这会让事情变得复杂(客户代码不好写),事实说话,让我们来实现一个同时使用这两个菜单的客户代码
Version 1.0
客户代码,在此是 main 函数
GetMenuItems() 方法所返回的类型是不一样的,所以我们必须实现两个不同的循环,个别处理不同的菜单
int main(int argc, char *argv[]) {
PancakeHouseMenu *pPancakeHouseMenu = new PancakeHouseMenu();
DiningRoomMenu *pDiningRoomMenu = new DiningRoomMenu();
vector<MenuItem *> aBreakfastItems = pPancakeHouseMenu->GetMenuItems();
int nMaxItems = 0;
MenuItem **aLunchItems = pDiningRoomMenu->GetMenuItems(nMaxItems);
// 只打印早餐项
for(int i = 0; i < aBreakfastItems.size(); i++) {
MenuItem *pMenuItem = aBreakfastItems.at(i);
printf("%s - ", pMenuItem->GetName().c_str());
printf("%.2f - ", pMenuItem->GetPrice());
printf("%s\n", pMenuItem->GetDescription().c_str());
}
// 只打印午餐项
for(int i = 0; i < nMaxItems && aLunchItems[i]; i++) {
MenuItem *pMenuItem = aLunchItems[i];
printf("%s - ", pMenuItem->GetName().c_str());
printf("%.2f - ", pMenuItem->GetPrice());
printf("%s\n", pMenuItem->GetDescription().c_str());
}
delete pPancakeHouseMenu;
delete pDiningRoomMenu;
return 0;
}
运行结果没问题
A Pancake - 2.99 - Pancakes with scrambled eggs and toast
B Pancake - 2.99 - Pancakes with fried eggs and sausage
C Pancake - 3.49 - Pancakes with fresh blueberries
D Pancake - 3.59 - Waffles with strawberries
A Dinner - 2.99 - Vegetarian bacon with lettuce & tomato on whole wheat
B Dinner - 2.99 - Bacon with lettuce & tomato on whole wheat
C Dinner - 3.29 - Soup of the day, with a side of potato salad
D Dinner - 3.05 - Hotdog with saurkraut topped with cheese
显然,我们是针对 PancakeHouseMenu 和 DiningRoomMenu 的具体实现编程,而不是针对接口。
客户代码需要知道每个菜单如何表达内部的菜单项集合,这违反了封装。
如果还有第三家餐厅以不同的实现出现,我们就需要有三个循环!
思考改进
餐厅和煎饼屋的代码现状让我们很为难,他们都不想改变自身的实现,因为意味着要重写很多代码。这就导致了我们写出来的客户代码将难以维护、难以扩展。
还记得封装变化吗?
很显然,在这里发生变化的是:由不同的集合类型所造成的遍历。
但是,这个也能被封装吗?我们来看看这个想法…
现在,我们创建一个对象,称之为迭代器 (Iterator),利用它来封装遍历集合内的每一个对象的过程
先在 vector 上试试:
Iterator *it = pancakeHouseMenu->CreateIterator();
while(it->HasNext()) {
MenuItem *menuItem = it->Next();
}
客户代码只需要调用 HasNext() 和 Next(),而迭代器会暗中调用 vector 的 size() 和 at() 方法
在数组上也试试:
Iterator *it = diningRoomMenu->CreateIterator();
while(it->HasNext()) {
MenuItem *menuItem = it->Next();
}
这个代码和上面的 pancakeHouseMenu 代码完全一样。客户代码只需调用 HasNext() 和 Next(),而迭代器会暗中使用数组的下标。
Version 2.0
首先,我们需要一个迭代器的接口,代码如下
加上了模板,避免使用 void*,客户代码就不用做类型强转了!
template<class Item>
class IIterator {
public:
virtual bool HasNext(void) = 0;
virtual Item *Next(void) = 0;
};
有了迭代器接口,就可以为各种集合(数组、vector等)实现迭代器(C++已经为 vector 实现了迭代器,在此为了演示,自行实现一个)
PancakeHouseIterator 实现了迭代器接口,是一个具体的迭代器,知道如何遍历 vector 菜单项
class PancakeHouseIterator : public IIterator<MenuItem> {
public:
PancakeHouseIterator(vector<MenuItem *> items) {
m_nPosition = 0;
m_aItems = items;
}
bool HasNext(void) {
if(m_nPosition >= m_aItems.size()) {
return false;
} else {
return true;
}
}
MenuItem *Next(void) {
MenuItem *pItem = m_aItems.at(m_nPosition);
m_nPosition++;
return pItem;
}
private:
int m_nPosition;
vector<MenuItem *> m_aItems;
};
同理,DiningRoomIterator 也实现了迭代器接口,知道如何遍历数组菜单项
class DiningRoomIterator : public IIterator<MenuItem> {
public:
DiningRoomIterator(MenuItem *items[], int size) {
m_nPosition = 0;
m_nSize = size;
m_aItems = items;
}
bool HasNext(void) {
if(m_nPosition >= m_nSize || m_aItems[m_nPosition] == NULL) {
return false;
} else {
return true;
}
}
MenuItem *Next(void) {
MenuItem *pItem = m_aItems[m_nPosition];
m_nPosition++;
return pItem;
}
private:
int m_nPosition;
int m_nSize;
MenuItem **m_aItems;
};
有了两个具体的迭代器,就利用它们来改写餐厅和煎饼屋菜单
我们只需加入一个 CreateIterator() 方法来创建具体的迭代器返回给客户代码即可
注意:我们不再需要 Version1.0 的 GetMenuItems() 方法了,因为它会暴露菜单内部的实现,删掉该方法
class PancakeHouseMenu {
public:
......
IIterator<MenuItem> *CreateIterator(void) {
return new PancakeHouseIterator(m_aMenuItems);
}
private:
vector<MenuItem *> m_aMenuItems; // 容器的形式
};
CreateIterator() 返回迭代器接口,客户代码不需要知道煎饼屋菜单是如何维护菜单的,也不需要知道迭代器是如何实现的,客户只需直接使用这个迭代器遍历菜单项即可。
同样,餐厅菜单也做一样的修改,去掉 GetMenuItems() 方法,添加 CreateIterator() 方法
class DiningRoomMenu {
public:
......
IIterator<MenuItem> *CreateIterator(void) {
return new DiningRoomIterator(m_aMenuItems, m_nMaxItems);
}
private:
const static int m_nMaxItems = 6;
int m_nNumberOfItems;
MenuItem *m_aMenuItems[m_nMaxItems]; // 数组的形式
};
好了,最后来看看客户代码怎么写
// 现在我们只需要一个循环就可以
void PrintMenu(IIterator<MenuItem> *it) {
while(it->HasNext()) {
MenuItem *pMenuItem = it->Next();
printf("%s - ", pMenuItem->GetName().c_str());
printf("%.2f - ", pMenuItem->GetPrice());
printf("%s\n", pMenuItem->GetDescription().c_str());
}
}
int main(int argc, char *argv[]) {
// 客户需要创建两个菜单
PancakeHouseMenu *pPancakeHouseMenu = new PancakeHouseMenu();
DiningRoomMenu *pDiningRoomMenu = new DiningRoomMenu();
// 客户只需要创建两个迭代器
IIterator<MenuItem> *pPancakeIterator = pPancakeHouseMenu->CreateIterator();
IIterator<MenuItem> *pDiningIterator = pDiningRoomMenu->CreateIterator();
// 使用迭代器来遍历菜单项并打印出来
PrintMenu(pPancakeIterator);
PrintMenu(pDiningIterator);
delete pPancakeHouseMenu;
delete pDiningRoomMenu;
return 0;
}
可见,客户代码不用关心两个菜单内部是用什么方式来存储菜单项的
运行结果跟 Version1.0 一样
大功告成,餐厅和煎饼屋他们可以保持自己的实现又可以摆平差别。只要给他们这两个迭代器,他们只需要加入一个 CreateIterator() 方法就可以了。
同时,客户代码将会更容易维护和扩展。
用类图看看目前的设计
Version 2.1
有没有发现,Version2.0 的客户代码里,我们还是依赖了 PancakeHouseMenu 和 DiningRoomMenu 这两个具体的菜单,我们得再改进一下
PancakeHouseMenu 和 DiningRoomMenu 都有 CreateIterator() 方法,可以把它提取出来放在接口里,如 IMenu 接口
template<class Item>
class IMenu {
public:
virtual IIterator<Item> *CreateIterator(void) = 0;
};
PancakeHouseMenu 和 DiningRoomMenu 只要实现 IMenu 接口就可以了
class PancakeHouseMenu : public IMenu<MenuItem> {
......
};
class DiningRoomMenu : public IMenu<MenuItem> {
......
};
客户代码只需要将原来的具体菜单类改成 IMenu 接口就可以了
int main(int argc, char *argv[]) {
// 客户需要创建两个菜单
IMenu<MenuItem> *pPancakeHouseMenu = new PancakeHouseMenu();
IMenu<MenuItem> *pDiningRoomMenu = new DiningRoomMenu();
......
}
改进后的类图如下
清爽了许多,客户代码利用接口(而不是具体类)引用每一个菜单对象。通过针对接口编程,而不是针对实现编程,我们就可以减少客户代码和具体类之间的依赖。
迭代器模式定义
现在,我们来说下什么是迭代器模式?
迭代器模式,属于行为型模式的一种。它提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露聚合对象的内部表示。
迭代器模式就是分离了聚合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露聚合的内部结构,又可以让外部代码透明地访问聚合内部的数据。
迭代器模式的优缺点
无论哪种模式都有其优缺点,当然我们每次在编写代码的时候需要考虑下其利弊
迭代器模式的优点:
- 简化了遍历方式。用户不需要了解聚合内部结构就可以遍历
- 可以提供多种遍历方式。比如说对有序列表,我们可以根据需要提供正序遍历、倒序遍历两种迭代器
- 封装性良好。用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历的方法,这样可以简化聚合类的设计
迭代器模式的缺点:
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性
- 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐
总结
当你需要访问一个集合对象,而且不管这些对象是什么都需要遍历的时候,就应该考虑用迭代器模式。
迭代器模式是与集合同生共死的,一般来说,只要实现一个集合,就需要同时提供这个集合的迭代器,就像 C++ 中的vector、map 等,这些集合都有自己的迭代器。
但是,由于集合与迭代器的关系太密切了,所以大多数编程语言在实现容器的时候都提供了迭代器,在绝大多数情况下就可以满足我们的需要。所以现在需要我们自己去实现迭代器模式的场景还是比较少见的。
参考资料
https://blog.csdn.net/u012611878/article/details/78010435
https://blog.csdn.net/wwwdc1012/article/details/83020422
Head+First设计模式(中文版).pdf