设计模式精讲:掌握工厂方法与抽象工厂的精髓
- 一、引言:如何学习设计模式?
- 二、工厂方法(也叫工厂模式)
- 2.1、代码结构
- 2.2、符合的设计原则
- 2.3、小结
- 三、抽象工厂
- 3.1、代码结构
- 3.2、符合的设计原则
- 3.3、小结
- 总结
一、引言:如何学习设计模式?
学习设计模式最主要要抓住一点:就是怎么分析这个稳定点和变化点。自己实现一个框架,或者是实现一个具体的小功能,本质上分析问题的思路都是一样的,首先要去把稳定点给它抽象出来,然后针对这个变化点想着怎么去扩展它。所以这里还是要反复的介绍怎么分析这个稳定点和变化点;具体不同的设计模式是怎么来处理这个扩展(就是扩展的问题);稳定点它是怎么处理的;用C++的语言特性是怎么去解决这些问题的;沿着这个思路去学习。
前面已经介绍了设计模式当中的模板方法、观察的模式、以及策略模式,这里再次强调以下学习、掌握设计模式的学习步骤。
- 首先,需要来了解设计模式解决了什么问题。本质上是分析它的稳定点和变化点,实际在做具体功能开发的时候也需要去抽象具体的稳定点以及想办法去扩展变化点,这样在实际开发过程当中,尽量写少量的代码去应对未来需求的变化。
- 第二点,设计模式的代码结构是什么。需要培养一个看代码、看一些框架或者看项目代码结构的时候马上能够反应出来使用了什么设计模式,或者它符合什么设计原则,从而可以推断出代码具体的意图。熟悉实现具体设计模式的代码结构能够帮助我们对一个代码有一个敏感度,以便能够快速的进行推断和反应。
- 第三点,看这些设计模式符合了哪些设计原则。因为设计模式是由设计原则推导过来的,所以可以按照这一个设计模式的产生的流程重新去思考这一个问题,能够帮助我们去很好的去设计我们的代码。相信很多人在具体的工作当中都有自己不同的一些设计方式,它不一定符合某一些设计模式,未来大家应对的某些需求也会自己去设计一个框架,所以可以思考它符合哪些设计原则。
- 第四点,如何在上面扩展代码。尤其是对于初学者或刚刚参加工作的朋友们,对这个扩展代码一定要非常的清楚,就是如果在这个设计模式的基础上要修改哪些代码,这个是必须要掌握的。
- 第五点,按照自己的需求或者自己的项目以及自己的工作场景进行一个联系,哪些需求变化可以使用设计模式;在看开源框架的时候也可以去看一下它是怎么解决这一个问题的。记住几个关键设计模式的一些典型应用场景能够帮助我们快速的反应;当具体需求来了知道该怎么使用某一些设计模式。
这个就是设计模式具体的学习步骤。
二、工厂方法(也叫工厂模式)
首先从定义出发,工厂方法的定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使得一个类的实例化延迟到子类。
接下来就要分析它解决了什么问题。它的定义不需要特别的去关注,也不要记这个定义,主要分析这个定义当中的稳定点和变化点。
- 稳定点:创建同类对象的接口,并且同类对象有一个相同的职责。要提供同类对象的一个接口,同类对象只有一个相同的职责(职责就是功能的意思)。
- 变化点:对象的拓展。同一类对象会越来越多,要进行对象的扩展。
2.1、代码结构
举个例子来帮助理解:
实现一个导出数据的接口,让客户选择数据的导出方式。
这个的职责是导出数据,这里面的导出数据有很多方式可以让用户选择,即同类对象都有导出功能,而且都是导出数据。因此,要创建一个对象接口以满足同类对象有相同的职责(职责就是导出数据)。
不使用设计模式的时候,首先要使用设计原则,注意到的是“导出数据”是一个职责,马上要想到接口,对于这种问题想都不用想就是要考虑用接口去封装它,因为有多个不同导出数据的接口,就要用统一的接口,这个接口就代表它有一个什么样的功能(即导出功能),马上就可以写出这样的一个语句:
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:virtual bool Export(const std::string &data) = 0;virtual ~IExport(){}
};
就是导出这个具体的数据有很多格式,比如说有xml,有json,有txt,还有excel格式csv等等。然后尝试实现接口:继承IExport
,实现Export()
方法。
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:virtual bool Export(const std::string &data) = 0;virtual ~IExport(){}
};class ExportXml : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportJson : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};
// csv
class ExportTxt : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};// class ExportCSV : public IExport {
// public:
// virtual bool Export(const std::string &data) {
// return true;
// }
// };// =====1
int main() {std::string choose/* = */;if (choose == "txt") {/***/IExport *e = new ExportTxt();/***/e->Export("hello world");} else if (choose == "json") {/***/IExport *e = new ExportJson();/***/e->Export("hello world");} else if (choose == "xml") {IExport *e = new ExportXml();e->Export("hello world");} else if (choose == "csv") {IExport *e = new ExportXml();e->Export("hello world");}
}
实现导出功能,实现IExport
类就可以导出为xml、json、txt等格式,因为在这里只有一个导出职责(导出功能),都是一样的,同样的接口Export
把它实现好,未来可能会增加CSV也可以实现一下。接下来让用户选择数据的导出方式,int main()
就要实现这个功能了。在这里按用户的输入判断,然后new出来这个数据,然后再把它进行Export
。这个是没有使用设计模式的时候,仅仅考虑面向接口编程写出来的这样的一个代码。
接下来看一下符合设计模式是怎么进行开发的,其实这里创建同类对象的接口可以用一个工厂接口来实现,有相同的职责可以用一个具体的职责接口来实现,也就是说这里应该要抽象出两个接口:一个是对象产生的接口,一个是具体的职责的接口。要理解这句话,先思考一个问题:
为什么要有工厂模式?
再来了解一下为什么在这里面要用两个接口,而不直接new
对象来使用?刚刚举的例子,有一个业务可能没注意到,主要的原因是没有表现出来,因为通常如果使用模式的时候,new
的对象不是直接用一个对象这么简单,new
完之后还要去有复杂的构造流程。
设计模式通常是要解决创建过程比较复杂,而且希望对外隐藏这些细节的场景,也就是说某一个用户只使用需要使用的功能。比如说导出JSON数据除了要去把JSON库导出来(加载JSON库)可能还需要去调用一些配置参数(配置这个JSON的参数),还可能要进行初始化一些数据,把这些流程都封装起来,放到某一个对象的JSON导出的接口中,那么对于用户而言,只需要知道用JSON导出,用户根本不需要关注还要加载一个库、配置一些参数、初始化一些值才能够去使用它的导出功能。
当创建过程比较复杂,而用户只需要关注他的职责,并不关注他的创建过程,比较复杂的这个流程用户不需要知道,这种时候需要使用工厂方法模式。举个例子,比如说连接池、线程池等,使用连接池通常只需要把一个任务push
进去就行了,对于用户而言,只需要有一个push
的操作就行了,往里面去push
后会自动帮用户完成;如果真正把线程池封装的好,那么对于用户而言,应该只需要知道有一个push接口就行了。都知道线程池要去构建它要初始化(这个把线程加载进来,把线程初始化好),把一些数据初始化,然后把一些入口函数初始化等等,这里面有复杂的构造流程,这些构造流程对于用户而言没什么用,用户根本不需要关注这些东西,用户只需要push
任务,对于一个正确的封装者(做功能开发、做功能抽象的人)应该是让用户知道的越少越好。
要点:解决创建过程比较复杂,希望对外隐藏这些细节的场景。
- 比如连接池、线程池
- 隐藏对象真实类型;
- 对象创建会有很多参数来决定如何创建;
- 创建对象有复杂的依赖关系。
工厂方法实现的代码:
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:virtual bool Export(const std::string &data) = 0;virtual ~IExport(){}
};class ExportXml : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportJson : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportTxt : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportCSV : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class IExportFactory {
public:IExportFactory() {_export = nullptr;}virtual ~IExportFactory() {if (_export) {delete _export;_export = nullptr;}}bool Export(const std::string &data) {if (_export == nullptr) {_export = NewExport();}return _export->Export(data);}
protected:virtual IExport * NewExport(/* ... */) = 0;
private:IExport* _export;
};class ExportXmlFactory : public IExportFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportXml();// 可能之后有什么操作return temp;}
};
class ExportJsonFactory : public IExportFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportJson;// 可能之后有什么操作return temp;}
};
class ExportTxtFactory : public IExportFactory {
protected:IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportTxt;// 可能之后有什么操作return temp;}
};class ExportCSVFactory : public IExportFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportCSV;// 可能之后有什么操作return temp;}
};int main () {IExportFactory *factory = new ExportCSVFactory();factory->Export("hello world");return 0;
}
为了实现封装出复杂的对象的构建过程,给了一个对象创建的一个接口IExportFactory
,那么这里的IExport
是功能接口(具体到底实现了一个什么功能)。具体的构造流程在NewExport()
中实现,用户不会关注这个的实现,用户只关注Export
功能(导出功能)。知道这些接口之后,还需要不同对象的构造,IExportFactory
是基类,使用ExportTxtFactory
、ExportCSVFactory
、ExportXmlFactory
等实现不同的导出对象,复杂的操作流程就在NewExport
中实现。最后就是在main ()
中使用它。
代码结构:
- 对象创建接口。
- 功能接口。
- 对象扩展。
- 多态调用的使用方式。
2.2、符合的设计原则
(1)最小知道原则。
(2)面向接口原则。
用户知道的越少越好,让用户只需要知道最需要的东西就行;想让用户知道的越少就要面向接口编程,把用户只关注的行为抽象成接口。
需要哪些接口?
- 具体的职责类的功能接口。简单来说就是提供了一个什么功能给用户,示例中用户只需要有一个导出的功能,只要给用户这样一个导出接口,用户只关注一个导出接口就行了,即功能接口是做什么事情的用户要知道。
- 对象创建接口。虽然用户不关注,但是要知道怎么去创建;就是有一个具体的对象,到底用户应该用什么东西来实现这个功能,比如说是用JSON来实现这个导出功能、用txt来实现这个导出功能、用CSV来现这个导出功能,就需要一个对象创建的接口;这个对象创建的接口就是所说的工厂接口。
2.3、小结
稳定点都是用抽象去解决,抽象那些万成不变的东西,具体可以用接口来实现。通过分析稳定点,发现要有一个创建同类对象的一个职责接口以及一个功能接口,这两个东西是稳定点,需要注意这个功能接口对于用户而言是不可见的,用户不需要关注这一个功能接口,这个功能接口它具体起到一个什么作用可以看到示例中IExportFactory
的功能对象创建的流程。IExportFactory
只提供了Export
,并没有提供创建的流程,创建流程在NewExport()
实现。
扩展代码:
- 实现对象创建接口。
- 实现功能接口。
- 多态调用。
本质:延迟到子类来选择实现。
结构图:
思维导图:
三、抽象工厂
理解了工厂模式,抽象工厂就非常的简单,它其实就是在工厂模式当中增加了一个多职责,它除了创建同类对象的接口,它还有就是有多个相同的职责。
抽象工厂的定义:提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。
解决了什么问题:
- 稳定点:创建同类对象的接口,并且同类对象有多个相同的职责。和工厂方法模式唯一的区别是"有多个相同的职责"。
- 变化点:对象的拓展。和工厂方法模式一样
3.1、代码结构
看一个例子:
实现一个拥有导出导入数据的接口,让客户选择数据的导出导入方式。
跟前面的工厂方法中的例子差不多,只是多了一个导入的功能,并且让用户去选择导入的方式。也就是说某一个对象(比如说CSV),它刚刚只需要有一个导出功能,现在还要有一个导入的功能;同样的,要把这个导入功能职责也要抽象成一个接口。
这个抽象工厂就非常的简单,它的代码结构基本和工厂方法一样的:
- 对象创建接口。包括创建具体对象,提供多个功能接口来调用。在创建对象接口当中去调用功能接口。
- 多个功能接口。
抽象工厂在工厂方法上面就多了一个,它是有多个职责的,它有多个功能的。
抽象工厂的代码实现:
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:virtual bool Export(const std::string &data) = 0;virtual ~IExport(){}
};class ExportXml : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportJson : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportTxt : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class ExportCSV : public IExport {
public:virtual bool Export(const std::string &data) {return true;}
};class IImport {
public:virtual bool Import(const std::string &data) = 0;virtual ~IImport(){}
};class ImportXml : public IImport {
public:virtual bool Import(const std::string &data) {return true;}
};class ImportJson : public IImport {
public:virtual bool Import(const std::string &data) {return true;}
};class ImportTxt : public IImport {
public:virtual bool Import(const std::string &data) {return true;}
};class ImportCSV : public IImport {
public:virtual bool Import(const std::string &data) {// ....return true;}
};class IDataApiFactory {
public:IDataApiFactory() {_export = nullptr;_import = nullptr;}virtual ~IDataApiFactory() {if (_export) {delete _export;_export = nullptr;}if (_import) {delete _import;_import = nullptr;}}bool Export(const std::string &data) {if (_export == nullptr) {_export = NewExport();}return _export->Export(data);}bool Import(const std::string &data) {if (_import == nullptr) {_import = NewImport();}return _import->Import(data);}
protected:virtual IExport * NewExport(/* ... */) = 0;virtual IImport * NewImport(/* ... */) = 0;
private:IExport *_export;IImport *_import;
};class XmlApiFactory : public IDataApiFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportXml;// 可能之后有什么操作return temp;}virtual IImport * NewImport(/* ... */) {// 可能有其它操作,或者许多参数IImport * temp = new ImportXml;// 可能之后有什么操作return temp;}
};class JsonApiFactory : public IDataApiFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportJson;// 可能之后有什么操作return temp;}virtual IImport * NewImport(/* ... */) {// 可能有其它操作,或者许多参数IImport * temp = new ImportJson;// 可能之后有什么操作return temp;}
};
class TxtApiFactory : public IDataApiFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportTxt;// 可能之后有什么操作return temp;}virtual IImport * NewImport(/* ... */) {// 可能有其它操作,或者许多参数IImport * temp = new ImportTxt;// 可能之后有什么操作return temp;}
};class CSVApiFactory : public IDataApiFactory {
protected:virtual IExport * NewExport(/* ... */) {// 可能有其它操作,或者许多参数IExport * temp = new ExportCSV;// 可能之后有什么操作return temp;}virtual IImport * NewImport(/* ... */) {// 可能有其它操作,或者许多参数IImport * temp = new ImportCSV;// 可能之后有什么操作return temp;}
};// 相关性 依赖性 工作当中
int main () {IDataApiFactory *factory = new CSVApiFactory();factory->Import("hello world");factory->Export("hello world");return 0;
}
除了导出接口,还加入了导入接口,分别实现它们的职责。
3.2、符合的设计原则
基本和工厂方法的一样。
(1)最小知道原则。
(2)面向接口原则。
3.3、小结
抽象工厂模式和工厂方法的区别:工厂方法通常一个对象只有一个职责,而抽象工厂模式是一个对象有多个职责。
结构图:
思维导图:
总结
在本文中,我们深入探讨了设计模式中两个重要的概念:工厂方法和抽象工厂。首先介绍了设计模式的重要性和应用场景,然后重点讲解了工厂方法模式和抽象工厂模式的原理和实现方式。通过详细的代码结构和实例分析,我们揭示了它们如何提供灵活性、可扩展性和可维护性,并如何符合设计原则。最后,我们强调了工厂方法和抽象工厂的精髓,帮助读者深入理解并将其应用于实际项目中,从而提高编程技能和代码质量。