基于C++的《Head First设计模式》笔记——迭代器模式

目录

一.专栏简介

二.对象村餐厅和对象村煎饼屋合并

三.检查菜单项

四.Lou和Mel的菜单实现

五.有两种不同的菜单表现方式,这会带来什么问题?

六.实现规格:我们的第一次尝试

七.可以封装遍历吗?

八.认识迭代器模式

九.定义迭代器模式

十.单一职责原则


一.专栏简介

本专栏是我学习《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()等一系列函数,进而做到面向接口编程,且女仆类只依赖这一个接口。

九.定义迭代器模式

迭代器模式提供一种方式,可以访问一个聚合对象中的元素而又不暴露其潜在的表示。

它也把遍历的任务放在迭代器对象上,而不是聚合上,这样就简化了聚合的接口和实现,让责任放在合适的地方。

十.单一职责原则

当我们允许一个类不但要关心自己的业务(管理某种聚合),而且还要承担更多的责任(像遍历)时,类就有了两个变化的原因。两个?对的,两个:如果集合变化,它要变;如果遍历方式变化,它也要变。

类的每个责任都是一个潜在变化的区域,超过一个责任,意味着超过一个变化的区域。

这个责任指导我们要让每个类保持单一职责原则。

相关推荐
saoys2 小时前
Opencv 学习笔记:remap 实现图像映射(缩放 / 对称 / 旋转)
笔记·opencv·学习
客卿1232 小时前
力扣--数组 入门三题-485/283/27---刷题笔记+思路分析+C语言
c语言·笔记·leetcode
浩瀚地学12 小时前
【Java】JDK8的一些新特性
java·开发语言·经验分享·笔记·学习
JeffDingAI13 小时前
【Datawhale学习笔记】深入大模型架构
笔记·学习
a不是橘子13 小时前
03在Ubuntu中验证PV操作
笔记·ubuntu·操作系统·虚拟机·os·pv操作
tangyal13 小时前
渗透笔记1
笔记·网络安全·渗透
fanged14 小时前
STM32(5)--HAL1(TODO)
笔记
一条闲鱼_mytube15 小时前
智能体设计模式(三)多智能体协作-记忆管理-学习与适应
人工智能·学习·设计模式
grd416 小时前
RN for OpenHarmony 小工具 App 实战:屏幕尺子实现
笔记·harmonyos