目录
一.专栏简介
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
本章将开始迭代器和组合模式的学习,以书中对象村餐厅和对象村煎饼屋为例子。
二.对象村餐厅和对象村煎饼屋合并
好消息!现在我们可以在同一个地方,享用煎饼屋美味的煎饼早餐,以及好吃的餐厅午餐。但好像有一点小麻烦......

三.检查菜单项
至少Lou和Mel都同意实现MenuItem。让我们检查每份菜单上的项,同时也看看其实现。

餐厅的菜单有许多午餐项,而煎饼屋的菜单包含的是早餐项。每个菜单都有名称、描述和价格。
代码如下:
cpp
#pragma once
#include <iostream>
#include <string>
using namespace std;
class MenuItem
{
public:
MenuItem(string name, string description, bool vegetarian, double price):
_name(name), _description(description), _vegetarian(vegetarian), _price(price)
{}
const string& getName() { return _name; }
const string& getDescription() { return _description; }
double getPrice() { return _price; }
bool isVegetarian() { return _vegetarian; }
private:
string _name;
string _description;
bool _vegetarian;
double _price;
};
四.Lou和Mel的菜单实现
现在让我们看看Lou和Mel在吵些什么。他们都在存储菜单项的方式上投入了很多时间和代码,还有许多其他代码依赖这些菜单项。
Lou的煎饼屋菜单实现代码如下:
cpp
class PancakeHouseMenu
{
public:
PancakeHouseMenu()
{
addItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs and toast", true, 2.99);
addItem("Regular Pancake Breakfast", "Panckes with fried eggs, sausage", false, 2.99);
addItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49);
addItem("Waffles", "Waffles with your choice of blueberries or strawberries", true, 3.59);
}
void addItem(string name, string description, bool vegetarian, double price)
{
MenuItem menuItem(name, description, vegetarian, price);
menuItems.push_back(menuItem);
}
const vector<MenuItem>& getMenuItems() { return menuItems; }
private:
vector<MenuItem> menuItems;
};
Lou用的是动态数组vector来管理菜单项,容易扩展菜单项。
Mel的餐厅菜单实现代码如下:
cpp
class DinerMenu
{
public:
DinerMenu()
{
menuItems = new MenuItem[MAX_ITEMS];
addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
addItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99);
addItem("Soup of the day", "Soup of the day,with a side of potato salad", false, 3.29);
addItem("Hotdog", "A hot dog,with sauerkraut,relish,onions, topped with cheese", false, 3.05);
}
~DinerMenu() { delete[] menuItems; }
void addItem(string name, string description, bool vegetarian, double price)
{
MenuItem menuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) cerr << "Sorry,menu is full!Can't add item to menu" << endl;
else
{
menuItems[numberOfItems] = menuItem;
++numberOfItems;
}
}
MenuItem* getMenuItems() { return menuItems; }
int size() { return numberOfItems; }
private:
static const int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem* menuItems;
};
Mel则是用了原生数组,有一个固定的菜单项大小。
五.有两种不同的菜单表现方式,这会带来什么问题?
为了了解为什么两种不同的菜单表现方式会让事情变得复杂,我们试着来实现一个使用这两个菜单的客户。假设我们被餐厅和煎饼屋合并的新公司雇用,我们的工作是创建一个C++版本的女招待。这个C++版本的女招待规格是:能按需为顾客打印定制的菜单,设置不需要问厨师就能高数我们是否某个菜单项是素食的。这可是一个创新!
我们来看看女招待的规格,然后看看怎样一步步实现她......
C++版本的女招待规格:

六.实现规格:我们的第一次尝试
我们先从实现printMenu()方法开始:
代码如下:
cpp
class Waitress
{
public:
void printMenu()
{
PancakeHouseMenu pancakeHouseMenu;
vector<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();
DinerMenu dinerMenu;
MenuItem* lunchItems = dinerMenu.getMenuItems();
for (int i = 0;i < breakfastItems.size();++i)
{
MenuItem menuItem = breakfastItems[i];
cout << menuItem.getName() << " " << menuItem.getPrice() << " " << menuItem.getDescription() << endl;
}
for (int i = 0;i < dinerMenu.size();++i)
{
MenuItem menuItem = lunchItems[i];
cout << menuItem.getName() << " " << menuItem.getPrice() << " " << menuItem.getDescription() << endl;
}
}
};
我们总是需要处理两个菜单,并且用两个循环遍历它们的项。如果再并购另一家有不同的实现的餐厅,我们就有三个循环。
基于我们实现的女招待,有以下问题:
- 我们在针对PancakeHouseMenu和DinerMenu的具体实现编程,而不是针对接口。
- 如果我们决定从使用DinerMenu切换到另一种菜单,此菜单的菜单项列表是用哈希表实现的,那么我们不得不修改女招待中的很多代码。
- 女招待需要知道每个菜单如何表达内部的菜单项集合,这违反了封装。
- 我们有重复的代码,printMenu()方法需要将两个分离的循环,来遍历两种不同的菜单。如果我们加上第三种菜单,我们又得加一个循环。
Mel和Lou让我们很为难。他们都不想改变他们的实现,因为这意味着要重写各自菜单类的很多代码。但如果没有人退让,我们只能实现一个难以维护和扩展的女招待。
如果我们能够找到方法,让他们的菜单实现同一个接口,该有多好(菜单是相似的,除了getMenuItem()方法的返回类型不同)。这样。我们就可以把女招待代码中的具体引用最小化,也有希望摆脱遍历两个菜单所需的多个循环。
七.可以封装遍历吗?
如果我们从本书中学到一件事情,那就是封装变化。很显然,在这里发生变化的是:菜单返回的不同对象集合所引起的遍历。但我们能封装这个吗?让我们来完成这个想法......
1.要遍历早餐项,我们用vector的size()和operator[]()方法:
cpp
for (int i = 0;i < breakfastItems.size();++i)
{
MenuItem menuItem = breakfastItems[i];
}
2.要遍历午餐项,我们用数组的固定大小以及数组下标:
cpp
for (int i = 0;i < dinerMenu.size();++i)
{
MenuItem menuItem = lunchItems[i];
cout << menuItem.getName() << " " << menuItem.getPrice() << " " <<
menuItem.getDescription() << endl;
}
3.现在我们创建一个对象,我们称为迭代器(Iterator),它封装遍历对象集合的方式。我们在vector上试试:
cpp
for (auto it = pancakeHouseMenu.begin();it != pancakeHouseMenu.end();++it)
{
cout << it->getName() << " " << it->getPrice() << " " << it->getDescription() << endl;
}
4.在数组上也试试:
cpp
for (auto it = dinerMenu.begin();it != dinerMenu.end();++it)
{
cout << it->getName() << " " << it->getPrice() << " " << it->getDescription() << endl;
}
八.认识迭代器模式
看起来我们封装迭代的计划已经奏效了,这也是一个设计模式,称为迭代器模式(Iterator Pattern)。
迭代器的实现C++和Java相差较大,我这里就花了比较大功夫来用C++实现(主要是复习了以下),代码如下:
Iterator.h:
cpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class MenuItem
{
public:
MenuItem():
_vegetarian(false), _price(0)
{}
MenuItem(string name, string description, bool vegetarian, double price):
_name(name), _description(description), _vegetarian(vegetarian), _price(price)
{}
const string& getName() const { return _name; }
const string& getDescription() const { return _description; }
double getPrice() const { return _price; }
bool isVegetarian() const { return _vegetarian; }
private:
string _name;
string _description;
bool _vegetarian;
double _price;
};
class PancakeHouseMenu
{
using iterator = vector<MenuItem>::iterator;
using const_iterator = vector<MenuItem>::const_iterator;
public:
PancakeHouseMenu()
{
addItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs and toast", true, 2.99);
addItem("Regular Pancake Breakfast", "Panckes with fried eggs, sausage", false, 2.99);
addItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49);
addItem("Waffles", "Waffles with your choice of blueberries or strawberries", true, 3.59);
}
void addItem(string name, string description, bool vegetarian, double price)
{
MenuItem menuItem(name, description, vegetarian, price);
menuItems.push_back(menuItem);
}
const_iterator begin() const { return menuItems.cbegin(); }
const_iterator end() const { return menuItems.cend(); }
const_iterator cbegin() const { return menuItems.cbegin(); }
const_iterator cend() const { return menuItems.cend(); }
private:
vector<MenuItem> menuItems;
};
class DinerMenu
{
class Iterator
{
// 迭代器类型标签(C++标准规范,必须加,决定迭代器能力)
using iterator_category = std::forward_iterator_tag;
using value_type = MenuItem;
using pointer = MenuItem*;
using reference = MenuItem&;
using difference_type = std::ptrdiff_t;
public:
Iterator(MenuItem* p) :
ptr(p)
{
}
Iterator& operator++()
{
++ptr;
return *this;
}
Iterator operator++(int)
{
Iterator temp = *this;
++ptr;
return temp;
}
reference operator*() const { return *ptr; }
pointer operator->() const { return ptr; }
bool operator==(const Iterator& other) const { return ptr == other.ptr; }
bool operator!=(const Iterator& other) const { return ptr != other.ptr; }
private:
MenuItem* ptr;
};
class ConstIterator {
private:
const MenuItem* ptr; // 只读指针,不可修改指向的内容
public:
using iterator_category = std::forward_iterator_tag;
using value_type = const MenuItem;
using pointer = const MenuItem*;
using reference = const MenuItem&;
using difference_type = std::ptrdiff_t;
explicit ConstIterator(const MenuItem* p) : ptr(p) {}
ConstIterator& operator++() { ptr++; return *this; }
ConstIterator operator++(int) { ConstIterator temp = *this; ptr++; return temp; }
reference operator*() const { return *ptr; }
pointer operator->() const { return ptr; }
bool operator==(const ConstIterator& other) const { return ptr == other.ptr; }
bool operator!=(const ConstIterator& other) const { return ptr != other.ptr; }
};
public:
DinerMenu()
{
menuItems = new MenuItem[MAX_ITEMS];
addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
addItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99);
addItem("Soup of the day", "Soup of the day,with a side of potato salad", false, 3.29);
addItem("Hotdog", "A hot dog,with sauerkraut,relish,onions, topped with cheese", false, 3.05);
}
~DinerMenu() { delete[] menuItems; }
// 对外暴露const迭代器入口(只读,推荐)
ConstIterator begin() const { return ConstIterator(&menuItems[0]); }
ConstIterator end() const { return ConstIterator(&menuItems[numberOfItems]); }
ConstIterator cbegin() const { return ConstIterator(&menuItems[0]); }
ConstIterator cend() const { return ConstIterator(&menuItems[numberOfItems]); }
private:
void addItem(string name, string description, bool vegetarian, double price)
{
MenuItem menuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) cerr << "Sorry,menu is full!Can't add item to menu" << endl;
else
{
menuItems[numberOfItems] = menuItem;
++numberOfItems;
}
}
static const int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem* menuItems;
};
class Waitress
{
public:
void printMenu()
{
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;
printMenu(pancakeHouseMenu);
printMenu(dinerMenu);
}
template <class T>
void printMenu(const T& obj)
{
for (const auto& e : obj)
{
cout << e.getName() << " " << e.getPrice() << " " << e.getDescription() << endl;
}
}
};
main.cpp:
cpp
#include "Iterator.h"
int main()
{
Waitress waitress;
waitress.printMenu();
return 0;
}
运行结果:

这段代码主要是设计C++迭代器的知识,我们需要在迭代器这个内置类里实现一系列运算符重载,让我们像用指针一样使用迭代器。我暂时不知道怎么让两个菜单类继承一个抽象类,这个抽象类里包含begin(),end()等一系列函数,进而做到面向接口编程,且女仆类只依赖这一个接口。
九.定义迭代器模式
迭代器模式提供一种方式,可以访问一个聚合对象中的元素而又不暴露其潜在的表示。
它也把遍历的任务放在迭代器对象上,而不是聚合上,这样就简化了聚合的接口和实现,让责任放在合适的地方。
十.单一职责原则
当我们允许一个类不但要关心自己的业务(管理某种聚合),而且还要承担更多的责任(像遍历)时,类就有了两个变化的原因。两个?对的,两个:如果集合变化,它要变;如果遍历方式变化,它也要变。

类的每个责任都是一个潜在变化的区域,超过一个责任,意味着超过一个变化的区域。
这个责任指导我们要让每个类保持单一职责原则。
