对象创建模式:工厂模式、单例模式、原型模式
对象创建模式
- 通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
- 典型模式
- Factory Method
- Abstract Factory
- Prototype
- Builder
简单工厂
- 简单工厂 Simple Factory :
- 把对象的创建封装在一个接口函数里面,通过传入不同的标识,返回创建的对象
- 好处(是个工厂都有这好处):客户不用自己负责new对象,不用了解对象创建的详细过程
- 缺点:提供创建对象实例的接口函数不闭合,不能对修改关闭
1 |
|
工厂方法模式
动机
在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合
定义
工厂定义一个用于创建对象的接口,让子类决定实例化哪一个类。
Factory Method
使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。过程推演
virtual:一种延迟,延迟绑定到运行时
C++语言本身没有提供多态new,但是我们通过virtual和指针,创造出了多态new
虽然之后还会在类外面new 具体的 factory,但是 MainForm里面,再也没有对具体Factory的依赖了。
设计模式的松耦合设计,很多时候并不是把变化消灭,也即:并不是把依赖具体类的这个事情消灭掉,而是把它们转移到某个局部的地方。
- 也就是,把“变化”这只猫关进笼子里,而不是让他在代码里跳来跳去
最终
抽象类和工厂基类
1
2
3
4
5
6
7
8
9
10
11
12
13//抽象类
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
};
//工厂基类
class SplitterFactory{
public:
virtual ISplitter* CreateSplitter()=0;
virtual ~SplitterFactory(){}
};具体类和具体工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//具体类
class BinarySplitter : public ISplitter{};
class TxtSplitter: public ISplitter{};
class PictureSplitter: public ISplitter{};
class VideoSplitter: public ISplitter{};
//具体工厂
class BinarySplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new BinarySplitter();
}
};
class TxtSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new TxtSplitter();
}
};
class PictureSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new PictureSplitter();
}
};
class VideoSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new VideoSplitter();
}
};依赖代码
1
2
3
4
5
6
7
8
9
10
11
12
13class MainForm : public Form
{
SplitterFactory* factory;//工厂基类
public:
MainForm(SplitterFactory* factory){
this->factory=factory;
}
void Button1_Click(){
ISplitter * splitter=
factory->CreateSplitter(); //多态new
splitter->split();
}
};为什么要有一个抽象ISplitter基类?因为需要这样一个抽象引用去接收实际的具体对象。
为什么要有一个抽象factory基类?因为需要依赖于抽象去动态绑定。
原本
工厂模式之后,不再依赖于具体类(变化),而是依赖于抽象类(稳定)
结构
总结
- Factory Method模式用于隔离类对象的使用者和具体类型之间的 耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导 致软件的脆弱。
- Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到子类(virtual函数),从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
- Factory Method模式解决“单个对象”的需求变化。缺点在于要 求创建方法/参数相同。
抽象工厂(家族工厂,这个工厂可以创造一家子相关操作)
- 工厂模式的基础上,将有关系的内容都放到一个工厂里,一起生产出来。
动机
- 在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。(一系列:Oracle有一系列数据库访问操作,MySql有一系列数据库访问操作,其他数据库又有一系列数据库访问操作)
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合?
定义
- 提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,(如数据库访问的一系列关联操作),无需指定它们具体的类。
代码
1 |
|
结构总结
要点总结
- 如果没有应对“多系列对象构建”的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的工厂完全可以。
- “系列对象”指的是在某一特定系列下的对象之间有相互依赖、或作用的关系。不同系列的对象之间不能相互依赖。
- Abstract Factory模式主要在于应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动
- 也就是 工厂基类(IDBFactory)不能够增添操作(纯虚函数。)。因为我们假定他是稳定的,也就是这个模式就是在利用那个基类的稳定性,如果变了,那么我们该采用其他模式。
工厂方法到抽象工厂
- 工厂模式是抽象工厂的一种特殊情况!(也即,工厂基类里的方法只有一个而非多个,只负责创建一个对象而非一系列相互依赖的对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//数据库访问有关的基类 以及相应基类工厂
// 如果不用抽象工厂,那么,三个相关基类,三个相关工厂。
class IDBConnection{
};
class IDBConnectionFactory{
public:
virtual IDBConnection* CreateDBConnection()=0;
};
class IDBCommand{
};
class IDBCommandFactory{
public:
virtual IDBCommand* CreateDBCommand()=0;
};
class IDataReader{
};
class IDataReaderFactory{
public:
virtual IDataReader* CreateDataReader()=0;
};
//支持SQL Server
class SqlConnection: public IDBConnection{
};
class SqlConnectionFactory:public IDBConnectionFactory{
};
class SqlCommand: public IDBCommand{
};
class SqlCommandFactory:public IDBCommandFactory{
};
class SqlDataReader: public IDataReader{
};
class SqlDataReaderFactory:public IDataReaderFactory{
};
//支持Oracle
class OracleConnection: public IDBConnection{};
class OracleFactory:public OralceFactory{
};
class OracleCommand: public IDBCommand{};
OracleCommand工厂
class OracleDataReader: public IDataReader{};
OracleDataReader工厂
class EmployeeDAO{
// 三个基类指针 指向 工厂
IDBConnectionFactory* dbConnectionFactory;
IDBCommandFactory* dbCommandFactory;
IDataReaderFactory* dataReaderFactory;
// 可以看出这三个工厂所要创作的三个对象相互依赖相互关联。是一系列一系列的操作。因此,可以把他们放入同一工厂。由同一工厂生产。
public:
vector<EmployeeDO> GetEmployees(){
IDBConnection* connection =
dbConnectionFactory->CreateDBConnection();
connection->ConnectionString("...");
IDBCommand* command =
dbCommandFactory->CreateDBCommand();
command->CommandText("...");
command->SetConnection(connection); //关联性
IDBDataReader* reader = command->ExecuteReader(); //关联性
while (reader->Read()){
}
}
};
最终代码(施磊)
- 子类必须实现父类的纯虚函数,不然无法new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98// 系列产品1
class Car
{
public:
Car(string name) :_name(name) {}
virtual void show() = 0;
virtual ~Car(){} // 基类的虚构函数写成虚的!
protected:
string _name;
};
class Bmw : public Car
{
public:
Bmw(string name) :Car(name) {}
void show()
{
cout << "获取了一辆宝马汽车:" << _name<<endl;
}
};
class Audi : public Car
{
public:
Audi(string name) :Car(name) {}
void show()
{
cout << "获取了一辆奥迪汽车:" << _name<<endl;
}
};
// 系列产品2
class Light
{
public:
virtual void show() = 0;
};
class BmwLight : public Light
{
public:
void show() { cout << "BMW light!" << endl; }
};
class AudiLight : public Light
{
public:
void show() { cout << "Audi light!" << endl; }
};
// 工厂方法 => 抽象工厂(对有一组关联关系的产品簇提供产品对象的统一创建)
class AbstractFactory
{
public:
virtual Car* createCar(string name) = 0; // 工厂方法 创建汽车
virtual Light* createCarLight() = 0; // 工厂方法 创建汽车关联的产品,车灯
};
// 宝马工厂
class BMWFactory : public AbstractFactory
{
public:
Car* createCar(string name)
{
return new Bmw(name);
}
Light* createCarLight()
{
return new BmwLight();
}
};
// 奥迪工厂
class AudiFactory : public AbstractFactory
{
public:
Car* createCar(string name)
{
return new Audi(name);
}
Light* createCarLight()
{
return new AudiLight();
}
};
int main()
{
// 现在考虑产品 一类产品(有关联关系的系列产品)
unique_ptr<AbstractFactory> bmwfty(new BMWFactory());
unique_ptr<AbstractFactory> audifty(new AudiFactory());
unique_ptr<Car> p1(bmwfty->createCar("X6"));
unique_ptr<Car> p2(audifty->createCar("A8"));
unique_ptr<Light> l1(bmwfty->createCarLight());
unique_ptr<Light> l2(audifty->createCarLight());
p1->show();
l1->show();
p2->show();
l2->show();
return 0;
}
- 讲给h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28工厂模式:简要来说就是:一个抽象类(字母),以及诸多具体类(a,b,c,d)。+ 一个工厂基类,以及诸多具体工厂类(对应生产a,b,c,d)。当需要a对象时,在相应代码段处,通过工厂基类指针,使用a工厂来创建a对象,用抽象基类(字母类型来接收)
宗旨:避免依赖具体的类(尽量依赖抽象类),将本要使用具体类的代码段和具体类分离开(通过使用抽象类和工厂)。
情景:假设现在要在一个类的成员函数FUNC()内使用对象a/b/c/d。但是,由于需求的变化,我们所要创建的对象的具体类型经常变化,所以,我们要找一个机制来避开常规的new语句。
因为,如果正常的new,就意味着我们必须在这个函数的代码段内明确说出要创建的对象的类型,那么就会造成紧耦合(如 A *p = new A(),指明了=号左侧的A和右侧的A类型)。
所以,对于a,b,c,d类,我们需要有一个他们的抽象基类:字母类,来接收具体的a、b、c对象。(因为抽象,所以稳定)。
如 字母 *p = new A()。那么,=号的左边就解决了。使用抽象类来接收具体的对象。
但是,在=号右边。我们还是要指明具体的A类型,也就是说还是依赖一个具体的类。
所以,为了不依赖它,我们得把new A()这个语句从本函数拿出来。
所以,我们新建一个类,把这个new的任务交给那个新类去处理,那个新类就叫做a工厂类,称为AFactory,内有create方法。
那么,现在的语句就变成 字母* p = pointer_to_AFactory->create();
但是,此时我们还是需要指出,这个工厂的名字叫做AFactory。那么对于b,c,d来说,我们还是需要指出使用BFactory,CFactory...这还是需要依赖具体的类,所以还需要继续改进。
所以,我们设置一个抽象基类 BaseFactory 里面有纯虚函数 create(),BaseFactory的指针/引用,可以用来接收A/B/CFactory。
那么,这条语句就变成 字母 *p = pointer_to_BaseFactory -> create();
create()函数的调用利用了多态的机制,将本应该编译时绑定的函数延迟到运行时进行绑定。
(这也就是通过面向对象的手法,将所要创建的具体对象延迟到子类中去进行。(延迟到子类中的C++中的virtual函数,Java中子类实现父类的函数),从而实现一种扩展(而非更改)的策略,较好地解决了原本的紧耦合关系)
这个pointer_to_BaseFactory,就是调用本语句所在的函数时传入的参数。这样,在FUNC()内,就不回依赖于具体的类(易变化),只依赖于抽象类(稳定)。
将对具体类的依赖从本成员函数内转移到其他代码处。
之前讲得工厂模式 是 一个工厂类里只负责一个对象的创建。
抽象工厂,就是一个工厂类负责多个对象的创建。其中,这多个对象有相互依赖、相互影响的关系。
如,我们要做访问数据库,对于每种数据库都会有三个操作,connection连接,read读取以及command发出命令。而connection又会影响read和command。
但是,每种数据库之间的具体操作又不同,如Oracle和mySQL之间的connection,read,command方式不会相同。
所以,我们可以看出,在要访问数据库时,我们需要生成负责connection的对象,负责read的对象和负责command的对象。
而这些对象之间又相互依赖相互影响。也即,我们实际需要connection、read、command对象是一系列一系列的。(也可以说是一组一组的)
所以,对于访问mySQL数据库,我们需要一个mySQLFactory{},里面负责生成mySQL数据库系列的connection、read、command对象。
对于Oralce数据库,同理,需要一个OracleFactory{},里面负责生成Oracle数据库系列的connection、read、command对象。
剩下的还是和工厂模式一样,一个数据库抽象基类,一个工厂的抽象基类,就是抽象工厂类负责一系列相关对象
单例模式
动机
- 特殊的类,必须保证他们在系统中只存在一个实例,才能保证他们的逻辑正确性、以及良好的效率。
定义
- 保证一个类仅有一个实例,并提供一个该实例的全局访问点
懒汉 推演
1 | class Singleton{ |
- 单线程用这个即可。线程不安全
1
2
3
4
5
6
7//线程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
} - 多线程最初:线程安全但性能差
1
2
3
4
5
6
7
8//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock; // 当对象已经创建出来、多个线程都只需要读时,会付出不必要的代价来等待锁
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
} - 多线程改进:双检查锁。看似正确,很有可能出reorder问题。不能用!!
- 锁前检查:避免当两个线程都是读取操作时,发生不必要的上锁解锁以及等待来提高效率
- 锁后检查:避免重复new对象。
1
2
3
4
5
6
7
8
9
10
11//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
- 漏洞:
- 正常应该是
- 先malloc 再 构造 再返回地址给 m_instance,
- 但是,编译器出于优化,可能会发生这样:
- 先malloc,然后返回地址,之后再构造
- 就会造成,假设有2个线程,一个进入
m_instance = new Singleton
。但是编译器优化,返回了地址却没调用构造函数,就会出现m_instance!=nullptr
但是m_instance
指向的对象还没被构造,而另一个线程在锁前检查发现不是nullptr
,就直接返回m_instance
,也即返回了一个不能使用的对象
- 正常应该是
- volatile
- volatile(vaoleitou) 易变的;无定性的;不稳定的
- volatile是一个特征修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。也即,编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。编译器会逐一地进行编译并产生相应的机器代码。
- volatile
volatile在多线程的作用。也是因为这个让编译器从内存读取变量的值而非寄存器/缓存
1
2volatile作用在变量上。不把变量存储在寄存器中,每次使用该变量都从内存中读取。这样做的目的是告诉编译器该变量的值可能会随时改变,因此需要从内存中而非寄存器中读取以确保变量的值准确。
在一些多线程的程序里一些全局变量可能会被其他线程修改,这时候要用volatile确保这个变量的值正确。(但是光用volatile也不够,但这就是另一个topic了)我总结:volatile的作用就是防止编译器对指令进行优化
- 从而防止编译器从寄存器而非内存中读值(避免多线程情况下发生错误)
- 从而防止单例模式中new对象时改变指令顺序返回空对象
好博客:博客
懒汉最终:懒汉模式线程安全+有效率+无reorder
1
1 | class Singleton |
2(通过static局部变量)
- 对于static静态局部变量的初始化,编译器会自动对它的初始化进行加锁lock和解锁unlock控制,使静态局部变量的初始化成为线程安全的操作,不用担心多个线程都会初始化静态局部变量,因此如下的懒汉单例模式是线程安全的单例模式
- shilei
- static instance:C++11在static局部变量初始化时自动进行加锁和解锁控制
- static 函数:为了在没有对象时就可调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 懒汉式单例模式另一种写法
class Singleton
{
public:
static Singleton* getSingleton()
{
// static局部变量
// static:生命周期为全局
// static + 局部 :汇编层面已经自动添加线程互斥指令
static Singleton instance;
return &instance;
}
private:
Singleton(){}
Singleton(const Singleton&) = delete;
Single& operator=(Singleton&) = delete;
};
饿汉 线程安全
- 饿汉单例模式中,单例对象定义成了一个static静态对象,它是在程序启动时,main函数运行之前就初始化好的,因此不存在线程安全问题,可以放心的在多线程环境中使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Singleton
{
public:
static Singleton* getSingleton()
{
return &m_instance;
}
private:
static Singleton m_instance;
Singleton(){}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Singleton Singleton:: m_instance;
步骤
- 构造函数私有化
- 2/3. 定义一个唯一的类的实例对象
- 3/2. 获取类的唯一实例对象的接口方法
可重入:可以在多线程下运行,且不会发生竞态条件。
原型模式 Prototype
一个小模式,不常用,是factory的变体
- 为了获取相同状态的对象。
- 当对象的创建复杂繁琐时。
动机
结构
要点总结
代码
1 |
|