设计模式——简单工厂模式

目录

前言

一、UML类图

二、简单工厂模式

2.1初窥门径------代码无错就是优?

2.2简单工厂模式


前言

本系列的所有文章,都是基于《大话设计模式》这本书来进行编写,在该系列的文章中,既会包含该书的一部分原文与实例,也会有博主自己的见解,正所谓一千个人心中有一千个哈利波特,这边还是建议大家有空的话去读一读原文。

也许,在设计代码的时候不使用设计模式你也可以很好的完成功能要求,那为什么要学习设计模式------良好的设计模式可以提高我们程序的可扩展性、易用性、维护性。这么说起来可能比较晦涩,在之后的讲解中你一定也会感受的这一点。那么我们这里就不废话了,我们来一起学习第一种设计模式------简单工厂模式。


一、UML类图

在开始之前,还是需要给大家讲解一下有关UML类图的知识,因为设计模式通常是对面向对象语言来说的,而面向对象的基本实体是类,UML类图就是用来描述类与类之间的关系的,这里博主之前也写过一篇相关的文章,大家可以去看一下,本篇文章中也会言简意赅的给大家解释清楚。

这里引用一下《大话设计模式》这本书里的插图。
图1 UML类图示例

对于某一个类来说,我们需要确定它的类名、成员变量、成员函数,对于"动物"这个类来说,它的类名叫做"动物";它有一个成员变量用来标识其是否是一个有生命的个体,在这个变量的前面有一个"+",这个符号表示该成员变量的访问权限为public、"-"表示访问权限为private、"#"表示访问权限为protected;它有两个成员函数,分别叫做"新陈代谢"、"繁殖"。其中"新陈代谢"这个成员函数需要"水"、"氧气"这两个类。
图2 "动物"类

依赖关系: 依赖关系如同图1中"动物"类与"氧气"类、"水"类的关系,这种关系我们用 "虚线+箭头"的形式来表现,其中箭头靠近被依赖类。(本例中,箭头靠近"氧气"类、"水"类)。这种关系是临时的,且并不是每个行为都必须的。以本例为例,"繁殖"方法中就没有使用到"氧气"类和"水"类。这两个类只是在"新陈代谢"这个方法中被临时使用了。

继承关系: 这种关系就像其名字所说的那样,发生在"继承类"和"被继承类"之间,通常来说"被继承类"都不是一个确切的类,它一般是对类的一种抽象。就好比哲学上的一个问题:"我让你去买一斤水果,你可以买回来吗?"。答案是不能的,因为我们只能买回来苹果、香蕉、梨这样的商品,却买不回来名为"水果"的商品。继承关系使用"实线+空心三角形"来标识,其中空心三角形靠近被继承类。

合成(组合)关系: 这种关系是一种"必要"关系,即"目标合成类"和"合成必需类"的关系。"目标合成类"像是一个整体,"合成必需类"像是一个"零件"。将多个"零件"拼接后才能合成目标类。缺一不可。以图1为例,一只鸟一般来说都有一双翅膀、鸟喙、鸟足组成,这些是缺一不可的。合成关系使用"实心菱形+实线+箭头"来标识,其中实心菱形靠近"目标合成类"、箭头靠近"合成必需类"。

关联关系: 这种关系是一种"包含"关系,即"包含类"中存在着"被包含类",比较常见的关联关系就是我们在实现链表这种数据结构的时候,定义链表节点的时候,我们都会把节点的指针作为节点的一个成员变量接入到类中,以便我们可以使用该指针来链接多个链表节点。对于链表这种关联关系叫做"自关联",对于我们图1所示的"企鹅"类与"气候"类的关联则是一种"单向关联"。关联关系通常使用"实线+箭头"来表示。其中,箭头靠近被关联类。

聚合关系: 这种关系通常是"单数"与"复数"的关系,以图1中所示,"大雁"和"雁群"的关系就是聚合关系,"雁群"一定由"大雁"组成的。这种关系就是聚合关系。通常我们使用"空心菱形+实线+箭头"来标识。其中空心菱形靠近"复数"类,箭头靠近"单数"类。

实现关系: 这种关系常见于接口类(只包含纯虚函数的类叫做"接口类")与具体类之间的关系。这种关系通常使用"虚线+空心三角形"来标识,其中空心三角形靠近"接口类"。

二、简单工厂模式

2.1初窥门径------代码无错就是优?

现在需要你写一个控制台程序实现一个"计算器",要求输入两个数字,一个运算符,得到结果。你也许会心想:"这有何难?"。三下五除二之后呈现了你的第一版代码。

cpp 复制代码
class Program
{
public:
	static void Main()
	{
		while (1)
		{
			cout << "请输入第一个数字" << endl;
			string A;
			cin >> A;
			cout << "请输入运算符" << endl;
			string op;
			cin >> op;
			cout << "请输入第二个数字" << endl;
			string B;
			cin >> B;


			int numA = stoi(A);
			int numB = stoi(B);
			if (op == "+")
			{
				cout << A << " + " << B << " = " << numA + numB << endl;
			}
			else if (op == "-")
			{
				cout << A << " - " << B << " = " << numA - numB << endl;
			}
			else if (op == "*")
			{
				cout << A << " * " << B << " = " << numA * numB << endl;
			}
			else if (op == "/")
			{
				if (numB == 0)
				{
					cout << "除数不能为0" << endl;
					continue;
				}
				cout << A << " / " << B << " = " << numA / numB << endl;
			}
			else
			{
				cout << "输入非法运算符" << endl;
			}
		}
	}
};

int main()
{
	Program::Main();
	return 0;
}

这段代码确实可以可以完成加法器的功能,现在我们是在控制台上实现一个计算器,如果我现在我们需要在Windows上实现一个有界面的计算器,那么我们可以复用上面写的这段代码吗?显然是不可以的,因为我们把结果的呈现(控制台打印结果)和结果的计算(计算结果)混在了一起;现在我们只想复用这段代码中计算结果的片段不想复用控制台打印的片段。所以我们需要将结果的呈现和结果的计算分开实现。

那么我们据此实现一下第二版的计算器代码

cpp 复制代码
class Operation
{
public:
	static double GetReasult(double x, double y, char op)
	{
		double ans = 0;
		switch (op)
		{
		case '+':
			ans = x + y;
			break;
		case '-':
			ans = x - y;
			break;
		case '*':
			ans = x * y;
			break;
		case '/':
			if (y == 0)
			{
				exit(1);
				cout << "除数不可以为0" << endl;
			}
			ans = x / y;
			break;
		default:
			cout << "未知运算符" << endl;
		}
		return ans;
	}
};

int main()
{
	double numberA, numberB;
	char op;
	cout << "请输入操作数" << endl;
	cin >> numberA >> numberB;
	cout << "请输入操作符" << endl;
	cin >> op;
	cout << "结果是:" << Operation().GetReasult(numberA, numberB, op) << endl;

	return 0;
}

现在的计算部分的代码经过封装后似乎可以很容易的进行复用了,那么这个代码还有什么问题呢?如果我现在要对计算器增加新的功能,是不是就需要更改计算部分的代码,每新增一个运算符就都需要更改Operation类。

你心想:"改代码不是一件很正常的事,这有何不可?"。当然对于这个示例代码直接更改似乎并没有什么问题,但是假如公司让你去修改工资派发程序呢?这个时候你灵机一动在工资派发程序中对你每月工资进行修改。这肯定不是公司希望发生的事情。回到我们的示例代码上,我们不希望发生修改时可以影响现有的代码:我们现在如果新增运算逻辑,就可以看到其他的运算行为,这样的代码是"紧耦合"的,我们不希望这样。所以我们可以将每个运算符对应的运算逻辑也给单独实现。所以我们就有了第三版的计算器代码。

cpp 复制代码
class Operation
{
public:
	double numberA = 0;
	double numberB = 0;
	virtual double GetReasult()
	{
		return 0;
	}
};

class Add : public Operation
{
public:
	double GetReasult() override
	{
		return numberA + numberB;
	}
};

class Sub : public Operation
{
public:
	double GetReasult() override
	{
		return numberA - numberB;
	}
};

class Mul : public Operation
{
public:
	double GetReasult() override
	{
		return numberA * numberB;
	}
};

class Div : public Operation
{
public:
	double GetReasult() override
	{
		if (numberB == 0)
		{
			cout << "除数不能为0" << endl;
			exit(1);
		}
		return numberA / numberB;
	}
};

现在我们将每个运算的逻辑进行了拆分,如果我们现在需要添加一个新的运算符,只需要在新增一个运算类就可以了。而且在新增的同时不会影响其它的运算类。那么现在我们如何让计算器知道我想调用哪一种算法呢?这就需要"简单工厂模式"来帮助我们完成这个功能。

2.2简单工厂模式

先来看代码,稍后我们在对代码进行解释。

cpp 复制代码
class Operation
{
public:
	double numberA = 0;
	double numberB = 0;
	virtual double GetReasult()
	{
		return 0;
	}
};

class Add : public Operation
{
public:
	double GetReasult() override
	{
		return numberA + numberB;
	}
};

class Sub : public Operation
{
public:
	double GetReasult() override
	{
		return numberA - numberB;
	}
};

class Mul : public Operation
{
public:
	double GetReasult() override
	{
		return numberA * numberB;
	}
};

class Div : public Operation
{
public:
	double GetReasult() override
	{
		if (numberB == 0)
		{
			cout << "除数不能为0" << endl;
			exit(1);
		}
		return numberA / numberB;
	}
};

class OperationFactory
{
public:
	static Operation* CreateOperation(char op)
	{
		Operation* oper = nullptr;
		switch (op)
		{
		case '+':
			oper = new Add();
			break;
		case '-':
			oper = new Sub();
			break;
		case '*':
			oper = new Mul();
			break;
		case '/':
			oper = new Div();
			break;
		default:
			cout << "未知运算符" << endl;
		}

		return oper;
	}
};

int main()
{
	Operation* oper = OperationFactory().CreateOperation('+');
	oper->numberA = 10;
	oper->numberB = 20;
	double ret = oper->GetReasult();
	cout << ret << endl;
	delete oper;
	return 0;
}

让计算器知道我想调用哪一种算法的本质就是如何去实例化我们的计算类对象,当我们有很多的计算类的时候,我们就可以使用简单工厂模式来管理实例化这个过程。当我们的计算器需要新增一个运算符的时候我们只需要实现一个运算类并在工厂类中加入对应的分支即可。

我们可以绘制一个UML类图来看看这些类的关系。
图3 计算器UML类图