1 概览
23种设计模式主要可以分为三种类型:
创建型模式:用来创建对象
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。
结构型模式:是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。(关注对象和类的组成关系)
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式:关注系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责
- 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
1.1 六大原则
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
其他:
- 高内聚:一个模块或一个类被设计成只支持一组相关的功能时,它具有高内聚。反之,被设计成一组不相关的功能时,我们说它具有低内聚。
- 低耦合:每个个模块之间或类的关联性降到可控范围的最低
- 单一职责原则(SRP):一个类或者模块只负责完成一个职责(或者功能)。
2 单例模式*
保证证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
- 主要解决:一个全局使用的类频繁地创建与销毁。
单例模式同时解决了两个问题
- 保证一个类只有一个实例。
- 为什么会想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。它的运作方式是这样的: 如果创建了一个对象, 同时过一会儿后决定再创建一个新对象, 此时会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
为该实例提供一个全局访问节点。 全局变量在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 我们不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
常见的五种单例模式实现方式
- 饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
- 普通懒汉式(线程不安全,调用效率不高。 但是,可以延时加载。)
- 双重检测锁式(加锁的懒汉式,使用互斥锁保证线程安全)
- 静态局部变量的懒汉单例(线程安全,调用效率高。 但是,可以延时加载)
- std::call_once 实现单例(C++11线程安全)(线程安全,调用效率高,不能延时加载)
注意:
- 懒汉式的名称来源是因为系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例;这种方式要考虑线程安全。而饿汉式则是系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题。
- 这里的线程安全是指创建单例对象时的,使用单例对像时的安全要开发者自己保证
何时使用:当您想控制实例数目,节省系统资源的时候。
2.1 饿汉式
饿汉式之所以线程安全是因为其实例是在代码一运行就初始话创建的,其本身就是线程安全的 实现步骤:
- 设置构造、析构、拷贝和赋值函数私有化,禁止外部构造和析构
- 创建静态接口,以获得单例对象,并定义一个类内私有的类静态成员指针
- 类外静态初始化一个单例对象
头文件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Singleton
{
public:
static Singelton* getsingle();
//释放单例,进程退出时调用
static void delteSingle();
private:
Singleton();
Singleton(const Singleton& single);
const Singleton& operator=(const Singleton& single);
~Singleton();
private:
static Singleton *g_singleton;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//装载类时就会初始化创建实例,本身就是线程安全
Singleton* Singleton::g_singleton = new(std::nothrow)Singleton();
Singleton* Singleton::getsingle(){
return g_singleton;
}
void Singleton::delteSingle(){
if(g_singleton){
delete g_singleton;
g_singleton = nullptr;
}
}
Singleton::Singleton(){
cout<<"构造函数"<<endl;
}
...
2.2 加锁懒汉模式(双重检测锁)
懒汉模式因为需要使用时才创建该实例,因此一般对线程来说是不安全的,需要通过加互斥锁来保证线程安全,但加锁的开销还是很大的,因此加锁的懒汉模式用两个if
判断语句来检测是否加锁,也叫双重检测锁式 步骤
- 设置构造、析构、拷贝和赋值函数私有化,禁止外部构造和析构
- 创建静态接口,以获得单例对象,并定义一个类内私有的类静态成员指针并在类外设置为nullptr
- 在静态接口使用双重
if
即双捡锁判断是否加锁来赋值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//头文件
class Singleton{
public:
static Singelton* getsingle();
//释放单例,进程退出时调用
static void delteSingle();
private:
Singleton();
Singleton(const Singleton& single);
const Singleton& operator=(const Singleton& single);
~Singleton();
protected:
static Singleton *g_singleton;
static std::mutex m_Mutex;
};
//源文件
//初始化静态成员变量
Singleton* Singleton:: g_singleton = nullptr;
std::mutex Singleton::m_Mutex;
Singelton* Singleton::getsingle()
{
//使用两个if,双检锁:只有判断指针为空在加锁,每次调用该方法式避免每次都要加锁
//若是空,则进入进行赋值
if(g_singleton==nullptr)
{
std::unique_lock<std::mutex>lock(m_Mutex); //加锁
if(g_singleton==nullptr)
g_singleton=new(std::nothrow)Singleton();
}
return g_singleton;
}
2.3 静态局部变量的懒汉单例
静态局部变量的懒汉单例,顾名思义就是在静态接口初始化一个静态变量(利用了静态变量只初始化一次的特性)
步骤:
- 设置构造、析构、拷贝和赋值函数私有化,禁止外部构造和析构
- 创建静态接口,以获得单例对象
- 在静态接口初始化一个局部静态变量(只初始化一次)
1 | //头文件 |
但是,这种方法也有点问题:在多线程场景下还是有可能会存在线程安全的问题,因为多线程同时调用 gessingle()
方法有可能还是会产生竞争。
解决这个问题的一种做法是:在程序的单线程启动阶段就调用 gessingle()
方法。
2.4 std::call_once 实现单例
std::call_once修饰的代码表示只被执行一次,常与lambda联合使用。 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
class Singleton {
public:
static std::shared_ptr<Singleton> getSingleton();
~Singleton() {
..
}
private:
Singleton() {
...
}
...
};
static std::shared_ptr<Singleton> singleton = nullptr;
static std::once_flag singletonFlag;
std::shared_ptr<Singleton> Singleton::getSingleton() {
std::call_once(singletonFlag, [&] {
singleton = std::shared_ptr<Singleton>(new Singleton());
});
return singleton;
}
2.5 适用场景
如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
如果需要更加严格地控制全局变量, 可以使用单例模式。 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
如要解决一个全局使用的类频繁的创建和销毁的问题,可以考虑单例模式
请注意, 可以随时调整限制并设定生成单例实例的数量, 只需修改获取实例方法。
2. 6 优缺点
- 优点
- 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
- 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
- 缺点
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
2.7 与其他设计模式的关系
- 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 如果能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
- 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
3 工厂模式*
工厂模式实现了创建者和调用者分离,本质上是实例化对象,用工厂方法代替创建对象。 同时,将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。
- 意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
- 主要解决:主要解决接口选择的问题。
工厂模式可以分为三类:
- 简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
- 抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)
注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过定义、new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
3.1 简单工厂模式
简单工厂模式也叫静态工厂模式,就是工厂类一般是使用静态方法, 通过接收的参数的不同来返回不同的对象实例。 其缺陷是对于增加新产品无能为力,不修改代码的话,是无法扩展的。
因此对于简单工厂模式来说客户端只知道传入工厂类的参数,对于如何创建对象,调用什么产品类创建对象不关心,客户端只需要指定工厂类的公共方法即可; 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//基类
class Phone{
public:
virtual void run()=0;
};
//不同品牌手机的实现类
class Huawei:public Phone
{
public:
virtual void run(){
cout<<"HUAWEI"<<endl;
}
};
class iPhone:public Phone
{
public:
virtual void run()
{
cout<<"iPhone"<<endl;
}
}
//工厂类
class phone_Factory(){
public:
static Phone* Create(char Type){
Phone* ret=nullptr;
switch(Type)
{
case'H':
ret=new Huawei();
break;
case'i':
ret=new iPhone();
break;
}
return ret;
}
}
//客户端
int main(int argc,char* argv[]){
Phone* phone_Object=phone_Factory::Create('H');
phone_Object->run();
return 0;
}
优点:工厂类是整个模式的关键.包含了必要的逻辑判断,根据客户端给定的信息,决定究竟应该创建哪个具体类的对象.通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。
- 缺点:
- 由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。
- 当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;
3.2 工厂方法模式
修正了简单工厂模式中不遵守开放-封闭原则。工厂方法模式把选择判断交给客户端去实现,工厂只负责实例化各个实现类;
即工厂方法模式和简单工厂模式最大的不同在于,简单工厂模式只有一个(对于一个项目或者一个独立模块而言)工厂类,内部要逻辑判断实例化哪一个类,而工厂方法模式有一组继承工厂基类的工厂类,各个工厂类各自实现对应的类实例化,其逻辑判断交由客户端去处理。
1 | //工厂基类 |
根据设计理论建议:工厂方法模式。但实际上,我们一般都用简单工厂模式。
3.3 工厂模式的优缺点
- 可以避免创建者和具体产品之间的紧密耦合。工厂模式创建对象时不会对客户端暴露具体产品创建逻辑,并且是通过使用一个共同的接口来创建的新对象。
- 单一职责原则。 可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。
- 开闭原则。 无需更改现有客户端代码, 就可以在程序中引入新的产品类型。
3. 4 工厂模式的应用场景
当在编写代码的过程中,如果无法预知对象确切类别及其依赖关系,可使用工厂方法。
工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
例如, 如果需要向应用中添加一种新产品, 只需要开发新的创建者产品子类, 然后重写其工厂方法即可。
3.5 与其他设计模式的关系
- 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制),随后演化为使用抽象工厂模式、 原型模式或生成器模式(更灵活但更加复杂)。
- 抽象工厂模式通常基于一组工厂方法, 但也可以使用原型模式来生成这些类的方法。
- 可以同时使用工厂方法和迭代器模式来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。
- 原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。
- 工厂方法是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。与其他设计模式的关系
4 抽象工厂模式*
抽象工厂模式是简单工厂和工厂方法的结合。工厂类:
- 抽象工厂模式有多个工厂(即抽象工厂->具体工厂),每个具体工厂又可以生产多种产品;因此抽象工厂模式是多个工厂,每个工厂一对多
- 简单工厂模式是一个工厂,自己一对多;
- 工厂方法模式是多个工厂,每个工厂一对一
注意事项:产品族难扩展,产品等级易扩展。 何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
4.1
且如果说简单工厂和工厂方法是对单个产品系列,那抽象工厂模式针对的是产品族,用来生产不同产品族的全部产品。在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。就比如产品类:
- 对于简单工厂和工厂方法,我们只讨论了华为和苹果手机,这样产品类从
手机->(Huawei/iPhone)
- 而对于抽象工厂模式来说,其产品可对系列来说,比如说多加了一个电脑,这样可以以这样方式设计:
抽象产品类->HUAWEI、iPhone产品工厂->HUAWEI、iPhone手机/电脑
总结来说抽象工厂如下: 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----------------产品-------------------------------------------
//抽象产品类
class product{
public:
product(int _price):prince(_price){}
virtual ~product(){}
int getPrince(){return prince;}
protected:
int price;
};
//抽象产品细分--HUAWEI
class Huawei_product:public product
{
public:
Huawei_product(int price,string color):product(price),m_color(color){}
virtual ~Huawei_product(){}
//获取颜色
string getColor(){return m_color;}
protected:
string m_color;
};
//HUAWEI具体产品类:手机和电脑
class Huawei_phone:public Huawei_product
{
public:
Huawei_phone(int price,string color,int _type):
Huawei_product(price,color),type(_type{}
virtual ~Huawei_phone(){}
int getType(){return type;}
protected:
int type;
};
class Huawei_computer:public Huawei_computer
{
public:
Huawei_computer(int price,string color,string _type):
Huawei_product(price,color),cpu_type(_type{}
virtual ~Huawei_computer(){}
string getType(){return type;}
protected:
int cpu_type;
};
...苹果产品略
-----------------------------------------------------------
工厂:
class Factory{
public:
virtual product* getPhone()=0;
virtual product* getComputer()=0;
};
//具体工厂--HUAWEI工厂
class Huawei_factory:public Factory
{
public:
virtual product* getPhone(){
product* H_phone=new Huawei_phone(4399,"白色",203);
return H_phone;
}
virtual product* getComputer(){
product* H_computer=new Huawei_computer(5699,"银色","i7-12500h");
return H_computer;
}
};
//具体工厂类---Apple
class Apple_factory:public Factory
{
public:
virtual product* getPhone(){
product* A_phone=new Apple_phone(4399,"白色",203);
return A_phone;
}
virtual product* getComputer(){
product* A_computer=new Apple_computer(5699,"银色","i7-12500h");
return A_computer;
}
};
4.2 抽象工厂的优缺点
优点:
- 具体类分离。具体产品类在具体工厂的实现中进行了分离和归类。
- 易于更换产品族。当客户端想要使用哪个整个产品族时,只需要切换具体工厂即可。也避免客户端和具体产品代码的耦合。
- 利于产品一致性。当产品族的各个产品需要在一起执行时,抽象工厂可以确保客户只操作同系列产品,而不会进行跨品牌的组合
- 单一职责原则。 可以将产品生成代码抽取到同一位置, 使得代码易于维护。
4.3 抽象工厂适用场景
- 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 不希望代码基于产品的具体类进行构建, 在这种情况下, 可以使用抽象工厂。
- 抽象工厂提供了一个接口, 可用于创建每个系列产品的对象。 只要代码通过该接口创建对象, 那么就不会生成与应用程序已生成的产品类型不一致的产品。
- 如果有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。
- 在设计良好的程序中, 每个类应该仅负责一件事。 如果在应用场景中一个类与多种类型产品交互, 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。
4.4 与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法模式(较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、原型模式或生成器模式](更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许在获取产品前执行一些额外构造步骤。
- 抽象工厂模式通常基于一组工厂方法, 但也可以使用原型模式来生成这些类的方法。
- 当只需对客户端代码隐藏子系统创建对象的方式时, 可以使用抽象工厂来代替外观模式。
- 可以将抽象工厂和桥接模式搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
- 抽象工厂、 生成器和原型都可以用单例模式来实现。
5 建造者(生成器)模式*
当某个类的创建需要很多的其他类组成时,建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
一个 Builder
类会一步一步构造最终的对象。该 Builder
类是独立于其他对象的。
- 主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
建造者模式的本质:
存在一个复杂对象,其内部有许多子组件(子对象)。生成器模式分离了对象子组件的单独构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况下使用。
由于实现了构建和装配的解耦。不同的构建器builder,相同的装配Director,也可以做出不同的对象; 相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
与工厂模式的区别是:建造者模式更加关注与零件装配的顺序,并最终实现复杂对象,而工厂模式使依据需要生成需要的产品 何时使用:一些基本部件不会变,而其组合经常变化的时候。
5.1 实现
这里以汽车Car为例:完整的汽车由几个组件组成:发动机、中控、轮胎和外观;即复杂对象是汽车
其过程为:
- 首先肯定有一个复杂类Car,其内部由组件对象成员和各种功能方法
- 组件类,单一职责,实现各个组件的功能
Builder
基类作为接口,后续的具体builder类都继承它来实现各个组件的创建,以此达到程序易扩展;同理也有一个Director
基类,使组件聚合在一起最终得到我们的Car对象
1 |
|
5.2 优缺点
优点:
- 可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时, 可以复用相同的制造代码。
- 单一职责原则。 可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点:
- 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。
5.3 适用场景
使用生成器模式可避免 “重叠构造函数 (telescoping constructor)” 的出现。
假设构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。
只有在 C++或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。1
2
3
4
5class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// ...生成器模式可以分步骤生成对象, 而且允许仅使用必须的步骤。 应用该模式后, 再也不需要将几十个参数塞进构造函数里了。
- 使用生成器构造组合树或其他复杂对象。
生成器模式能分步骤构造产品。 可以延迟执行某些步骤而不会影响最终产品。 甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。
当希望使用代码创建不同形式的产品 (例如石头房屋或木头房屋) 时, 可使用生成器模式。
如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。
5.4 与其他模式的关系
在许多设计工作的初期都会使用工厂方法模式(较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式(更灵活但更加复杂)。
生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许在获取产品前执行一些额外构造步骤。
可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。
可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
抽象工厂、 生成器和原型都可以用单例模式来实现。
6 原型模式*
原型模式的设计思想:在软件系统中,创建某一类型的对象,为了简化创建的过程,可以只创建一个对象,然后通过克隆的方式复制出多个相同的对象。
原型模式(Prototype Pattern):是一种对象创建模式,用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
何时使用: 1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
6.1 实现
(Prototype)抽象原型类:抽象原型类是定义具有克隆自己的方法的接口,是所有具体原型类的公共父类。
ConcretePrototype(具体原型类):具体原型类实现具体的克隆方法,在克隆方法中返回自己的一个克隆对象。原型模式说白了就是在类实现克隆操作,提供该接口给客户端调用
Client (客户端):客户端让一个原型克隆自身,从而创建一个新的对象。在客户类中只需要直接实例化或通过工厂方法等创建一个对象,再通过调用该对象的克隆方法复制得到多个相同的对象。
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//原型基类
class prototype{
protected:
string name;
int id;
public:
prototype(string _name,int _id):name(_name),id(_id){}
virtual prototype* clone()=0;
};
//具体原型类
class StudentPrototype:public prototype
{
public:
StudentPrototype(string _name,int _id):prototype(_name,_id){}
StudentPrototype(const StudentPrototype& s);
virtual prototype* clone(){
prototype* new_object=new StudentPrototype(*this);
return new_object;
}
};
//客户端
int main()
{
StudentPrototype ob1("trluper",1);
prototype* ob2=ob1.clone();
}
原生模型与拷贝构造函数的区别: 相同点:原型模式和拷贝构造函数都是要产生对象的复制品。
不同点:原型模式实现的是一个clone接口,注意是接口,也就是基于多态的clone虚函数。也就是说原型模式能够通过基类指针来复制派生类对象。拷贝构造函数完不成这样的任务。
原型模式的核心是克隆,构造函数只是克隆的一个办法而已
6.2 优缺点
优点:
可以克隆对象, 而无需与它们所属的具体类相耦合
可以克隆预生成原型, 避免反复运行初始化代码。
可以更方便地生成复杂对象。
缺点:
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,当克隆包含循环引用的复杂对象可能会非常麻烦
6.3 适用场景
- 资源优化场景:类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。通过原生模型的复制可以绕过这些资源消耗
- 对象的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),抑或是需要从 RPC、网络、数据库、文件系统等非常慢速的IO中读取,这个时候就可以利用原型模式从其他对象直接拷贝,从而减少资源的消耗。
- 当初始化一个对象需要非常繁琐的数据准备或访问权限时,如构造函数的参数很多,而自己又不完全的知道每个参数的意义,可以考虑原生模型
6.4 与其他模式的关系
在许多设计工作的初期都会使用工厂方法模式(较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式(更灵活但更加复杂)。
抽象工厂模式通常基于一组工厂方法, 但也可以使用原型模式来生成这些类的方法。
原型可用于保存命令模式的历史记录。
大量使用组合模式和装饰模式的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承,但是它不需要初始化步骤。
有时候原型可以作为备忘录模式的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。
抽象工厂、 生成器和原型都可以用单例模式来实现。
原型模式通过复制原型(原型)而获得新对象创建的功能,这里原型本身就是"对象工厂"(因为能够生产对象),实际上原型模式和
Builder
模式、AbstractFactory
模式都是通过一个类(对象实例)来专门负责对象的创建工作(工厂对象),它们之间的区别是:Builder
模式重在复杂对象的一步步创建(并不直接返回对象),AbstractFactory
模式重在产生多个相互依赖类的对象,而原型模式重在从自身复制自己创建新类。
7 适配器模式*
适配器模式属于结构型模式,其功能是作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能。这个模式涉及到单一的适配器类,该类负责加入独立的或不兼容的接口功能。如读卡器是作为内存卡和笔记本之间的适配器,将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
作用:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。因此其主要解决在软件系统中,新环境要求的接口是现对象不能满足的,因此需要适配器类来整合适配。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用: 1、系统需要使用现有的类,而此类的接口不符合系统的需要。 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 3、通过接口转换,将一个类插入另一个类系中
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
7.1 实现
- 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
- 需要适配的类(Adaptee):需要适配的类或适配者类,客户端需要调用该类,但是没有接口可以调用,需要被适配。
- 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
现在举例:顾客进一家服装店的动作为目标接口Target
,然后顾客在该店买不同品种的衣服是需要适配的类adaptee
,适配器通过继承与目标接口实现相应的适配,即各个客户是买不同的衣服的: 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//目标接口
class Target_shop{
protected:
int id;
public:
Taeget_shop(int _id):id(_id){}
virtual void print()=0;
};
//需要适配的类
class Customer_shopping{
protected:
string Clothname;
double price;
public:
Customer_shopping(string _cloth,double _price):Clothname(_cloth),price(_price){}
void ShoppingList(){
cout<<"顾客购买了:"<<Clothname<<",价格为:"<<price<<endl;
}
}
//适配器
class Customer_Adaptor:public Target_shop
{
protected:
Customer_shopping* customer;
public:
Customer_Adaptor(int _id,Customer_shopping* cus):Target_shop(_id),customer(cus){}
virtual void print()
{
cout<<"第"<<id<<"位顾客来店!"<<endl;
customer->ShoppingList();
}
}
----------------------------------------------
//客户端
int main()
{
Customer_shopping *cus=new Customer_shopping("衬衫",199.0);
Customer_shopping customer(1,cus);
customer->print();
}
7.2 优缺点
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好
- 符合开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
缺点:
- 代码整体复杂度增加, 因为需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。
7.3 适用场景
当使用某个类时, 但是其接口与其他代码不兼容时, 可以使用适配器类。
- 适配器模式允许创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。
如果需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
- 我们是可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 必须在所有新子类中重复添加这些代码, 这会显得很不方便。可以通过将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。
7.4 与其他模式的关系
桥接模式通常会于开发前期进行设计, 能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
桥接、 状态模式和策略模式(在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 还可以使用它们来和其他开发者讨论模式所解决的问题。
8 桥接模式*
桥接(Bridge)模式属于结构型模式,通过提供抽象化和实现化之间的桥接结构,使得二者可以独立变化。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响
- 主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸的问题,扩展起来不灵活。
桥接模式可以取代多层继承的方案。
多层继承违背了单一职责原则, 复用性较差,类的个数也非常多。桥接模式可以极大的减少子类的个 数,从而降低管理和维护的成本。极大的提高了系统可扩展性,在两个变化维度中任意扩展一 个维度,都不需要修改原有的系统,符合开闭原则。
比如说游戏里面有成长进化的武器、宠物,其都是由基础武器、基础宠物成长而来,这样如果采用继承,则会有很多子类,且随着游戏版本迭代,武器子类只会越来越多,此时使用桥接模式是可采纳的
抽象部分 (也被称为接口) 是一些实体的高阶控制层。** 该层自身不完成任何具体的工作, 它需要将工作委派给实现部分层 (也被称为平台)**
8.1 实现
以游戏装备为例:有两个基类基础打野刀和**进化打野刀,如果按继承我们应该是三个进化打野刀继承与基础打野刀的,现在我们采用桥接的模式:在两个基类当中建立桥接结构:
1 | //进化打野刀基类 |
8.2 优缺点
优点:
- 符合开闭原则。 可以新增抽象部分和实现部分, 且它们之间不会相互影响。
- 符合单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节。
- 实现细节对客户透明。客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息。
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。因此对高内聚的类使用该模式可能会让代码更加复杂。
8.3 适用场景
1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
8.4 与其他模式的关系
桥接模式通常会于开发前期进行设计, 使能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
桥接、 状态模式和策略模式(在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 还可以使用它们来和其他开发者讨论模式所解决的问题。
可以将抽象工厂模式和桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
可以结合使用生成器模式和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
9 装饰模式*
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀
何时使用:在不想增加很多子类的情况下扩展类。
9.1 实现
- 首先有一个基类,其有相应的功能方法;
- 后续的装饰器类
Decirator
继承于他,而具体的装饰类decorator_xx
继承与装饰器类Decorator
,实现添加新功能,即装饰目的 同样,后续的具体被装饰类也会继承基类,实现一些共同方法
- 这里还是以汽车
Car
为例, - 其有一个子类位
BenzCar
,该子类实现一些正常功能; 同样还有还有一个
Decorator
装饰器类继承与Car
,在装饰器类的子类实现该类型车的不同颜色的喷漆操作,这里以白色为例WhiteBenzCarDecorator
1 | //基类 |
9.2 优缺点
优点
- 装饰模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。
- 装饰模式降低系统的耦合度,可以动态的增加或删除对象的职责,并使得需要装饰的具体构建类和具体装饰类可以独立变化,以便增加新的具体构建类和具体装饰类。
缺点:
- 产生很多小对象。大量小对象占据内存,一定程度上影响性能。
9.3 应用场景
- 如果希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。
- 装饰能将业务逻辑组织为层次结构, 可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。
- 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 可以使用该模式。
- 许多编程语言使用
final
最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。
- 许多编程语言使用
9.4 与其他模式的关系
适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
责任链模式和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。
责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
但是, 模式也可以相互合作: 可以使用装饰来扩展组合树中特定对象的行为。
大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 可以通过该模式来复制复杂结构, 而非从零开始重新构造。
装饰可更改对象的外表, 策略模式则能够改变其本质。
装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
装饰模式和桥接模式的区别:两个模式都是为了解决过多子类对象问题。但他们的诱因不一样。桥接模式是对象自身现有机制沿着多个维度变化,是既有部分不稳定。装饰模式是为了增加新的功能。
10 组合模式*
组合模式(Composite Pattern)是结构型模式。组合模式又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
- 主要解决:它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦
10.1 实现
- 抽象构件(Component)角色: 定义了叶子和容器构件的共同点
- 叶子(Leaf)构件角色:无子节点
- 容器(Composite)构件角色: 有容器特征,可以包含子节点
模拟病毒文件和文件夹查杀:
1 | /* |
10.2 优缺点
优点:
可以利用多态和递归机制更方便地使用复杂树结构。
开闭原则。 无需更改现有代码, 就可以在应用中添加新元素, 使其成为对象树的一部分。
缺点:
对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 需要过度一般化组件接口, 使其变得令人难以理解
在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
10.3 适用场景
- 部分、整体场景,如树形菜单,文件、文件夹的管理。
- 如果希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。 组合模式中定义的所有元素共用同一个接口。 在这一接口的帮助下, 客户端不必在意其所使用的对象的具体类。
10.4 与其他模式的关系
桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 还可以使用它们来和其他开发者讨论模式所解决的问题。
可以在创建复杂组合树时使用生成器模式, 因为这可使其构造步骤以递归的方式运行。
责任链模式通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。
可以使用迭代器模式来遍历组合树。
可以使用访问者模式对整个组合树执行操作。
可以使用享元模式实现组合树的共享叶节点以节省内存。
组合和装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
但是, 模式也可以相互合作: 可以使用装饰来扩展组合树中特定对象的行为。
大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 可以通过该模式来复制复杂结构, 而非从零开始重新构造。
11 外观模式*
外观模式为结构性模式,其为子系统提供统一的入口(门面),封装子系统的复杂性,便于客户端调用。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
- 主要解决:降低访问复杂系统的内部子系统时的复杂度,简化客户端之间的接口。
- 何时使用:
- 1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。
- 2、定义系统的入口。
11.1 实现
- 外观模式,最重要的是外观类(这个模式也就只有这一个类),其提供统一入口:
1 | /* |
11.2 优缺点
优点:
- 可以让自己的代码独立于复杂子系统。减少系统相互依赖,提高灵活性,也提高了安全性
缺点:
- 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
11.3 适用场景
如果需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。
子系统通常会随着时间的推进变得越来越复杂。 即便是应用了设计模式, 通常也会创建更多的类。 尽管在多种情形中子系统可能是更灵活或易于复用的, 但其所需的配置和样板代码数量将会增长得更快。 为了解决这个问题, 外观将会提供指向子系统中最常用功能的快捷方式, 能够满足客户端的大部分需求。
如果需要将子系统组织为多层结构, 可以使用外观。
创建外观来定义子系统中各层次的入口。 可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。
11.4 与其他模式的关系
- 外观模式为现有对象定义了一个新接口, 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
- 当只需对客户端代码隐藏子系统创建对象的方式时, 可以使用抽象工厂模式来代替外观。
- 享元模式展示了如何生成大量的小型对象, 外观则展示了如何用一个对象来代表整个子系统。
- 外观和中介者模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
- 外观类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 外观与代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
12 享元模式*
享元模式属于结构性模式,享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。内存属于稀缺资源,不要随便浪费。如果有很多个完全相同或相似的对象,我们可以通过享元模式,节省内存。
享元模式以尝试共享的方式高效地支持大量细粒度对象的重用,如果未找到匹配的对象,则创建新对象。享元对象能做到共享的关键是区分了内部
- 主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建
元对象能做到共享的关键是区分了内部状态和外部状态,必须划分外部状态和内部状态,否则可能会引起线程安全问题。
- 内部状态:可以共享,不会随环境变化而改变
- 外部状态:不可以共享,会随环境变化而改变
12.1 实现
FlyweightFactory
享元工厂类:创建并管理享元对象,享元池一般设计成键值对FlyWeight
抽象享元类:通常是一个接口或抽象类,声明公共方法,这些方法可以向外界提供对象的内部状态,同时可以设置外部状态ConcreteFlyWeight
具体享元类:为内部状态提供成员变量进行存储UnsharedConcreteFlyWeight
非共享享元类:不能被共享的子类可以设计为非共享享元类
实例:这里用多用户访问一个相同网站为例,网站这个对象应该共享,而用户的鉴权的密码应该是共享的 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//抽象享元类
class Flyweight{
public:
virtual void Use()=0;
};
//具体非共享用户信息
class FlyweightUserInfo:public Flyweight
{
private:
string account;
string secretKey;
public:
string Username;
FlyweightUserInfo(string _name,string _secretKey):
acount(_name),secretKey(_secretKey){
//数据库鉴权..省略
Username=sqlname;
cout<<"登录成功"<<endl;
}
virtual void Use(){
cout<<"正在访问共享网站"<<endl;
}
};
//具体共享网站
class FlyweightWebsite:public Flyweight{
private:
string str;
public:
FlyweightUserInfo* uInfo;
FlyweightWebsite(string s,Flyweight* _info):str(s),uInfo(_info){}
virtual void Use(){
cout<<uInfo->Username<<",你好!"<<endl;
}
};
//享元工厂类
class FlyweightFactory{
private:
Map<string,Flyweight*>myMap;
public:
FlyweightFactory(){}
FlyweightWebsite* getFlyshared(string s,Flyweight* _info){
if(map[s]!=myMap.end())
return myMap[s];
else
myMap.insert(pair<string,Flyweight*>(s,new FlyweightWebsite(s,_info)));
return myMap[s];
}
};
----------------------------------------------------
//客户端
int main(){
//非共享鉴权
Flyweight* user=new FlyweightUserInfo("trluper","123456789");
FlyweightFactory* fac =new FlyweightFactory();
Flyweight* web = fac->getFlyshared("设计模式",user);
web->Use();
}
12.2 优缺点
优点:
- 极大减少内存中对象的数量
- 相同或相似对象内存中只存一份,极大的节约资源,提高系统性能
- 外部状态相对独立,不影响内部状态
缺点:
- 模式较复杂,使程序逻辑复杂化
- 为了节省内存,共享了内部状态,分离出外部状态,而读取外部状态使运行时间变长。用时间换取了空间
12.3 适用场景
仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。应用该模式所获的收益大小取决于使用它的方式和情景。 它在下列情况中最有效:
- 程序需要生成数量巨大的相似对象
- 这将耗尽目标设备的所有内存
- 对象中包含可抽取且能在多个对象间共享的重复状态。
还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用较少的共享对象取代多组对象,此时可以考虑使用享元。
12.4 与其他模式的关系
- 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
- 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
- 如果能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同:
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
13 代理模式*
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
意图:为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
13.1 实现
- 抽象角色:定义代理角色和真实角色的公共对外方法
- 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。(关注真正的业务逻辑)
- 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。(将统一的流程控制放到代理角色中处理!)
1 |
|
13.2 优缺点
优点:
- 可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求, 可以对生命周期进行管理。
- 即使服务对象还未准备好或不存在, 代理也可以正常工作。
- 开闭原则。 可以在不对服务或客户端做出修改的情况下创建新代理。
缺点:
- 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂,因为要创建许多新的类。
13.3 应用场景
按职责来划分,通常有以下使用场景:
延迟初始化 (虚拟代理)。 如果有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。
访问控制 (保护代理)。 如果只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。代理可仅在客户端凭据满足要求时将请求传递给服务对象。
本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。
记录日志请求 (日志记录代理)。 适用于当需要保存对于服务对象的请求历史记录时。代理可以在向服务传递请求前进行记录。
缓存请求结果 (缓存代理)。适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。
智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。
代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。
代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。
13.4 与其他模式的关系
适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
外观模式与代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
14 模板方法模式
模版方法定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现。这样,新的子类可以在不改变一个算法结构的前提下重新定义该算法的某些特定步骤。把不变的代码部分都转移到父类中, 将可变的代码用 virtual 留到子类重写
- 主要解决:一些方法通用,却在每一个子类都重新写了这一方法
14.1 实现
1 | //抽象算法骨架 |
14.2 优缺点
优点:
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点:
- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
14.3 适用场景
- 有多个子类共有的方法,且逻辑相同时可考虑
- 重要的、复杂的方法,可以考虑作为模板方法。
15 命令模式*
命令模式属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。。
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式
15.1 实现
Command
抽象命令类ConcreteCommand
具体命令类Invoker
请求的调用者/请求者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联。在程序运行时,将调用命令对象的execute
方法,间接调用接受者的相关操作Receiver
接收者:接收者执行与请求相关的操作,具体实现对请求的业务处理。(实际执行操作内容的对象)Client
客户类,需要创建调用者对象,具体命令类对象,在创建具体命令类对象时指定对应的接受者。发送者和接收者之间没有直接关系,都通过命令对象间接调用
1 |
|
15.2 优缺点
优点:
- 单一职责原则。 可以解耦触发和执行操作的类。
- 开闭原则。 可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 可以实现撤销和恢复功能。
- 可以实现操作的延迟执行。
- 可以将一组简单命令组合成一个复杂命令。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类,代码可能会变得更加复杂。
15.3 适用场景
1.在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
2.如果需要通过操作来参数化对象, 可使用命令模式。 命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
3.如果想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式. 同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 还可以将命令放入队列、 记录命令或者通过网络发送命令。
4.如果想要实现操作回滚功能, 可使用命令模式。 尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。
为了能够回滚操作, 需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 可以使用备忘录模式来在一定程度上解决这个问题。
其次, 备份状态可能会占用大量内存。 因此, 有时需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。
16 迭代器模式
提供一种方法顺序访问一个聚敛对象的各个元素,而又不暴露该对象的 内部表示。
为遍历不同的聚集结构提供如开始,下一个,是否结束,当前一项等统一接口
- 主要解决:不同的方式来遍历整个整合对象。
16.1 实现
- 迭代器抽象类,声明一些迭代器常用的方法,如`begin\end++
1 |
|
16.2 优缺点
优点:
- 单一职责原则。 通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理。
- 开闭原则。 可实现新型的集合和迭代器并将其传递给现有代码, 无需修改现有代码。
- 可以并行遍历同一集合, 因为每个迭代器对象都包含其自身的遍历状态。
- 相似的, 可以暂停遍历并在需要时继续。
缺点:
- 如果程序只与简单的集合进行交互, 应用该模式可能会矫枉过正。
- 对于某些特殊集合, 使用迭代器可能比直接遍历的效率低。
在C++ STL库中已经提供迭代器的实现。本文的实现主要是了解迭代器的大致原理。
16.3 适用场景
当集合背后为复杂的数据结构, 且希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。
迭代器封装了与复杂数据结构进行交互的细节, 为客户端提供多个访问集合元素的简单方法。 这种方式不仅对客户端来说非常方便, 而且能避免客户端在直接与集合交互时执行错误或有害的操作, 从而起到保护集合的作用。
使用该模式可以减少程序中重复的遍历代码。
重要迭代算法的代码往往体积非常庞大。 当这些代码被放置在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降低其可维护性。 因此, 将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。
如果希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。
该模式为集合和迭代器提供了一些通用接口。 如果你在代码中使用了这些接口, 那么将其他实现了这些接口的集合和迭代器传递给它时, 它仍将可以正常运行。
16.4 与其他模式的关系
可以使用迭代器模式来遍历组合模式树。
可以同时使用工厂方法模式和迭代器来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。
可以同时使用备忘录模式和迭代器来获取当前迭代器的状态, 并且在需要的时候进行回滚。
可以同时使用访问者模式和迭代器来遍历复杂数据结构, 并对其中的元素执行所需操作, 即使这些元素所属的类完全不同。
17 观察者模式*
我们可以把多个订阅者、客户称之为观察者; 需要同步给多个订阅者的数据封装到对象中,称之为目标。
意图:观察者模式主要用于1:N的通知。当一个对象(目标对象
Subject
或Objservable
的状态变化时,它需要及时告知一系列对象(观察者对象Observer
),令它们作出响应主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
通知观察者的方式有:
- 推:每次都会把通知以广播方式发送给所有观察者,所有观察者只能被动接收
- 拉:观察者只要知道有情况即可,至于什么时候获取内容,获取什么内容,都可以自主决定
17.1 实现
- 首先是目标类,在目标类内定义一集合保存观察者对象,当目标类发生变化时,调用通知接口
Notify
通知观察者们 - 观察者类(多个),收到通知会根据自身条件响应
1 |
|
17.2 优缺点
优点:
- 1、观察者和被观察者是抽象耦合的。
- 2、建立一套触发机制,A发生变化,与它相关的会收到这种变化通知,然后依据自身条件是否响应。
缺点:
- 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
17.3 适用场景
一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
一个对象必须通知其他对象,而并不知道这些对象是谁。
需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
18 中介者模式*
如果一个系统中对象之间的联系呈现为网状结构,对象之间存在大量多对多关系,将导致关系极其复杂,这些对象被称为同事对象,可以引入一个中介者对象,使各个同事只跟中介者对象打交道,将复杂的网络结构简单化。
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性,解耦多个同事对象之间的交互关系,这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。每个同事对象都持有中介者对象的引用,只跟中介者对象打交道。我们通过中介者对象统一管理这些交互关系。
- 主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理
18.1 实现
- 抽象同事类
Departement
和抽象中介者类Mediator
- 具体实现同事类,内含中介者指针成员变量
- 具体中介类,内部维护一个
map
,管理同事类对象
1 | class Mediator; |
输出: 1
2
3
4
5
6研发需要资金,财务部赶紧拨钱
财务部拨钱
研发部门进行产品开发工作
市场扩展需要资金,财务部赶紧拨钱
财务部拨钱
市场部门进行市场规划制定
18.2 优缺点
优点:
- 单一职责原则。 可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。
- 开闭原则。 无需修改实际组件就能增加新的中介者。
- 可以减轻应用中多个组件间的耦合情况。
- 可以更方便地复用各个组件。
缺点:
- 一段时间后, 中介者可能会演化成为上帝对象。(一个上帝对象(God object)是一个了解过多或者负责过多的对象)
18.3 适用场景
当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。
当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。
应用中介者模式后, 每个组件不再知晓其他组件的情况。尽管这些组件无法直接交流,但它们仍可通过中介者对象进行间接交流。如果希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。
如果为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,可使用中介者模式。
由于所有组件间关系都被包含在中介者中, 因此无需修改组件就能方便地新建中介者类以定义新的组件合作方式。
18.4 与其他模式的关系
责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
外观模式和中介者的职责类似:它们都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
中介者和观察者之间的区别往往很难记住。在大部分情况下,可以使用其中一种模式,而有时可以同时使用。
中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接,使得部分对象可作为其他对象的附属发挥作用。
有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色,其他组件则作为订阅者,可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时,它可能看上去与观察者非常相似。
当你感到疑惑时,记住可以采用其他方式来实现中介者。 例如,可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同,但这仍是一种中介者模式。
假设有一个程序,其所有的组件都变成了发布者,它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象,而只有一些分布式的观察者。
19 备忘录模式*
用于保存某个对象内部状态的拷贝,这样以后就可以将该对象恢复到原先的状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。如我们常说的回滚就可以用备忘录实现
注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式+备忘录模式。
19.1 实现
Originator
源发器类:要做备份的内容,其中有方法负责创建一个备忘录,用以记录当前时刻它的内部状态,并可以使用备忘录恢复到内部状态Memonto
备忘录类,负责存储Originator
对象的内部状态,并可防止Originator
以外的其他对象访问CareTaker
负责人类:负责保存好备忘录Memento
,从Memento
中恢复对象的状态
1 | class Memonto; |
19.2 优缺点
优点:
- 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:
- 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
20 解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
- 意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题
解释器模式可可使用的场景较少
20.1 实现
- 定义一个抽象类 AbstractExpression 和实现了 Expression 的实体类。
- 定义作为上下文中主要解释器的 TerminalExpression 类,终结符表达式,实现与文中的终结符相关联的解释操作。非终结符表达式类,
OrExpression、AndExpression
用于创建组合式表达式. Context
包含解释器之外的一些全局信息
20.2 优缺点
优点:
- 1、可扩展性比较好,灵活。
- 2、增加了新的解释表达式的方式。
- 3、易于实现简单文法。
缺点:
- 1、可利用场景比较少。
- 2、对于复杂的文法比较难维护。
- 3、解释器模式会引起类膨胀。
- 4、解释器模式采用递归调用方法。
21 状态模式*
在状态模式(State Pattern)属于行为型模式,类的行为是基于它的状态改变的。在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
- 意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类
- 主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。因此当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,可考虑用到状态模式。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
21.1 实现
- state
抽象类,后续的状态子类继承于它实现 - Context
是一个带有某个状态的类,依据状态而改变它的行为
下面举例一个机器人收到不同的行为命令处于不同的状态,如行走、停止、蹲下 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/*
*state抽象类
*/
class state{
public:
virtual void actionOfState()=0;
};
/*
*state具体类,行走WalkSate、StopState
*/
class WalkState:public state
{
public:
virtual void actionOfState(){
std::cout<<"收到行走指令,开始行走"<<std::endl;
}
};
class StopState:public state
{
public:
virtual void actionOfState(){
std::cout<<"收到停止指令,停止走动"<<std::endl;
}
};
/*
*受命令约束的机器人,依据状态而改变行为
*/
class Robot
{
private:
state* robotState;
public:
Robot(state* _state):robotState(_state){}
void setStatus(state* _state){this->robotState=_state;}
void doSomething(){
robotState->actionOfState();
}
};
-----------------------------------------------------------
//客户端
int main()
{
state* walk=new WalkState();
state* stop=new StopState();
Robot carRobot(walk);
carRobot.doSomething();
carRobot.setStatus(stop);
carRobot.doSomething();
}
21.2 优缺点
优点:
- 1、封装了转换规则。
- 2、枚举可能的状态,在枚举状态之前需要确定状态种类。
- 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
- 1、状态模式的使用必然会增加系统类和对象的个数。
- 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
21.3 适用场景
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,可考虑用到状态模式。
条件、分支语句的代替者。
21.4 与其他模式的关系
- 通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。
- 状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
- 多个环境对象共享一个状态对象,从而减少系统中对象的个数,此时跟享元模式相似
22 策略模式*
策略模式对应于解决某一个问题的一个算法族,允许用户从该算法族中任选一个算法解决某一问题,同时可以方便的更换算法或者增加新的算法。并且由客户端决定调用哪个算法。
从上面的思想可以看到策略模式和简单工厂很相像,但简单工厂模式只能解决对象创建问题,对于经常动的算法应使用策略模式。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换,算法的变化,不会影响到用户。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
22.1 实现
- 一个策略基类
Strategy
,后续各种不同的策略继承于他实现。 - 继承于
Strategy
的各种策略的具体实现 - 一个聚合类
Context
,作用时提供给客户端调用不同的策略方法,因此内部要有策略基类的指针成员变量、设置采用哪种策略的接口、以及调用接口
等基本方法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
using namspace std;
//策略基类
class Strategy{
protected:
double discount;
public:
Strategy(double _dis):discount(_dis){}
virtual int get_discount()=0;
};
//各种不同的策略算法
class newCustomerFewStrategy:public Strategy
{
public:
newCustomerFewStrategy(double _dis):Strategy(_dis){}
virtual int get_discount(){
cout<<"普通用户小批量,不打折"<<endl;
return this->discount;
}
};
class newCustomerManyStrategy:public Strategy
{
public:
newCustomerManyStrategy(double _dis):Strategy(_dis){}
virtual int get_discount(){
cout<<"普通用户大批量,打"<<discount<<"折"<<endl;
return this->discount;
}
};
class oldCustomerFewStrategy:public Strategy
{
public:
newCustomerManyStrategy(double _dis):Strategy(_dis){}
virtual int get_discount(){
cout<<"老用户小批量,打"<<discount<<"折"<<endl;
return this->discount;
}
};
class oldCustomerManyStrategy:public Strategy
{
public:
newCustomerManyStrategy(double _dis):Strategy(_dis){}
virtual int get_discount(){
cout<<"老用户大批量,打"<<discount<<"折"<<endl;
return this->discount;
}
};
//Context负责和具体的策略类交互,实现具体算法与客户端调用分离
class Context{
protected:
Strategy* strategy; //当前采用的策略算法
public:
Context(Strategy* _sta):strategy(_sta){}
Strategy* getStrategy(){return strategy;}
void setStrategy(Strategy* _sta){
this->strategy=_sta;
}
void print_price(double price){
double ret*=strategy->get_discount/10;
cout<<"最终价格为:"<<ret<<"元"<<endl;
}
};
//客户端
int main(){
Strategy* st=new oldCustomerManyStrategy(8.5);
Context con(st);
con.print_price(700);
}
22.2 优缺点
优点:
- 1、算法可以自由切换。
- 2、避免使用多重条件判断。
- 3、扩展性良好。
缺点:
- 1、策略类会增多。
- 2、所有策略类都需要对外暴露。
22.3 使用场景
1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。
3、如果一个对象有很多的行为,这些行为使用多重的条件选择语句来实现,现在有了策略模式,我们就可以省略它们,由客户端决定使用哪一个行为方法。
23 责任链模式*
责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些客户端的哪些请求,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象;
由于责任链的创建完全在客户端,因此新增新的具体处理者对原有类库没有任何影响,只需添加新的类,然后在客户端调用时添加即可。 符合开闭原则。
因此发出这个请求的客户端可以不知道链上的哪一个对象最终处理这个请求,只需要发送即可,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
23.1 实现
责任链的实现方法有两种:
- 链表方式定义职责链
- 非链表方式实现职责链:通过集合、数组生成职责链更加实用!实际上,很多项目中,每个具体的Handler并不是由开发团队定义的,而是项目上线后由外部单位追加的,所以使用链表方式定义COR链就很困难。
下面代码以管理者为例:首先管理者为基类,其子类又经理、总监和普通员工,其责任链顺序为经理->总监->普通员工
;基类声明了两个必须的接口:
RequestHandler
:在该接口实现经过判断是处理请求,还是将请求传递给下一个,因此要重写,因此为虚函数setSuccessor
:该接口实现当前处理者能将请求传递给下一个处理者,即请求处理继任者,以此实现责任链的形式;不需重写
1 |
|
23.2 优缺点
- 优点
- 可以控制请求处理的顺序。
- 单一职责原则。 可对发起操作和执行操作的类进行解耦。
- 开闭原则。 可以在不更改现有代码的情况下在程序中新增处理者。
- 缺点
- 部分请求可能未被处理
23.3 适用场景
当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
该模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。
当必须按顺序执行多个处理者时, 可以使用该模式。
无论以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。
如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。
如果在处理者类中有对引用成员变量的设定方法, 将能动态地插入和移除处理者, 或者改变其顺序。
23.4 与其他模式的关系
责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
命令在发送者和请求者之间建立单向连接。
中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
观察者允许接收者动态地订阅或取消接收请求。
责任链通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。
责任链的管理者可使用命令模式实现。 在这种情况下, 可以对由请求代表的同一个上下文对象执行许多不同的操作。
还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 可以对由一系列不同上下文连接而成的链执行相同的操作。
责任链和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。
责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
24 访问者模式
对于存储在一个集合中的对象,他们可能具有不同的类型(即使有一个公共的接口),对于该集合中的对象,可以接受一类称为访问者的对象来访问,不同的访问者其访问方式也有所不同。访问者模式表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作
- 意图:主要将数据结构与数据操作分离。
- 主要解决:稳定的数据结构和易变的操作耦合问题。
- 何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
24.1 实现
24.2 优缺点
优点:
- 1、符合单一职责原则。
- 2、优秀的扩展性。
- 3、灵活性。
缺点:
- 1、具体元素对访问者公布细节,违反了迪米特原则。
- 2、具体元素变更比较困难。
- 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
24.3 适用场景
- 当需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,可以使用访问者模式将这些封装到类中。
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
文章来源:本文笔记主要来源腾讯大佬
Sliverming
的笔记和菜鸟总结