设计模式
- 简介
- 单例模式
- 饿汉模式
- 懒汉模式
- 工厂模式
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
- 建造者模式
- 代理模式
简介
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路它不是语法规定,而是⼀套⽤来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决⽅案。
设计模式的六大原则
-
单⼀职责原则(Single Responsibility Principle)
(1)类的职责应该单一,一个⽅法只做⼀件事。职责划分清晰明了,每次改动到最小单位的方法或类。
(2)使用建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很高的函数、数据的封装。
(3)用例:网络聊天:网络通信 & 聊天,应该分割成为网络通信类 & 聊天类。在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
-
开闭原则(Open Closed Principle)
(1)对扩展开放,对修改封闭,即软件实体应尽量在不修改原有代码的情况下进行扩展。
(2)使用建议:对软件实体的改动,最好用扩展而非修改的⽅式。
(3)用例:超市卖货:商品价格—不是修改商品的原来价格,而是新增促销价格。
任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。因为变化,升级和维护等原因,如果需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试,所以当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现使我们需要的。
抽象化是开闭原则的关键所在,我们可以为系统定义一个比较稳定的抽象层,将不同的实现行为移动至具体的实现层中去进行实现,如果我们此时需要对代码进行修改的话,比如新增某个功能,就不需要对抽象层的代码进行修改,而是将该类的实现添加到行为层就可以了,并不会修改已有的代码来达到实现新功能的需求,这样就达到开闭原则的要求了。
- 里氏替换原则(Liskov Substitution Principle)
(1)通俗点讲,就是只要父类能出现的地⽅,子类就可以出现,⽽且替换为子类也不会产生任何错误或异常;在继承类时,务必重写父类中所有的方法,尤其需要注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用。
(2)使用建议:子类必须完全实现父类的方法,子类可以有自己的个性。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小。
(3)用例:跑步运动员类–会跑步,子类长跑运动员–会跑步且擅长长跑, 子类短跑运动员–会跑步且擅⻓短跑。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
- 依赖倒置原则(Dependence Inversion Principle)
(1)高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。
(2)使用建议:每个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用。
(3)用例:奔驰车司机类–只能开奔驰; 司机类 – 给什么车,就开什么车; 开车的人:司机–依
赖于抽象。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
- 迪米特法则(Law of Demeter)
(1)如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用,其目的是降低类之间的耦合度,提高模块的相对独立性。
(2)迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
(3)用例:老师让班长点名–⽼师给班长⼀个名单,班长完成点名勾选,返回结果,而不是班长点名,老师勾选。
迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
- 接口隔离原则(Interface Segregation Principle)
(1)使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
(2)使用建议:接口设计尽量精简单⼀,但是不要对外暴露没有实际意义的接口。
(3)用例:修改密码,不应该提供修改用户信息接口,而就是单⼀的最小修改密码接口,更不要暴露数据库操作。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
(1)单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
(2)单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体到每⼀条设计原则,则对应⼀条注意事项:
- 单⼀职责原则告诉我们实现类要职责单⼀;
- 里氏替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要⾯向接口编程;
- 接⼝隔离原则告诉我们在设计接口的时候要精简单⼀;
- 迪⽶特法则告诉我们要降低耦合;
- 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
单例模式
单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现方式,分别是饿汉模式和懒汉模式:
饿汉模式
饿汉模式实现原理就是程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以比较适用于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提高性能。
饿汉实现方式如下:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化。
- 提供一个全局访问点获取单例对象。
class SingleTon
{
public:
// 提供一个获取单例对象的接口函数
static SingleTon* GetInstance()
{
return _inst;
}
private:
// 构造函数私有化
SingleTon() {}
// 析构函数私有化
~SingleTon() {}
// 将拷贝构造函数以及运算符重载函数私有化或者delete
SingleTon(const SingleTon&) = delete;
SingleTon& operator=(const SingleTon&) = delete;
static SingleTon* _inst; // 定义一个指向单例对象的static指针
};
// 在进入程序入口之前完成单例对象的初始化工作
SingleTon* SingleTon::_inst = new SingleTon;
线程安全相关问题:
- 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
- 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
- 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。
懒汉模式
懒汉模式在程序运行之前没有进行单例对象的创建,而是等到某个线程需要使用这个单例对象时再进行创建,也就是GetInstance函数第一次被调用时创建单例对象。
因此在调用GetInstance函数获取单例对象时,需要先判断这个static指针是否为空,如果为空则说明这个单例对象还没有创建,此时需要先创建这个单例对象然后再将单例对象返回;GetInstance函数第一次调用时需要对static指针进行写入操作,这个过程不是线程安全的,因为多个线程可能同时调用GetInstance函数,如果不对这个过程进行保护,此时这多个线程就会各自创建出一个对象。
class SingleTon
{
public:
static SingleTon* GetInstance()
{
// 双重判断
if(_inst == nullptr)
{
_mtx.lock();
if(_inst == nullptr)
{
_inst = new SingleTon;
return _inst;
}
_mtx.unlock();
}
}
private:
// 将构造函数私有化
SingleTon() {}
// 将析构函数私有化
~SingleTon() {}
// 将拷贝构造函数以及运算符重载函数私有化或者delete
SingleTon(const SingleTon&) = delete;
SingleTon& operator=(const SingleTon&) = delete;
static SingleTon* _inst; // 定义一个指向单例对象的static指针
static std::mutex _mtx; // 互斥锁
};
双重加锁的意义
通过上述代码我们可以看见,我们进行双重判断,锁也是在第一个if判断以后才进行使用的,原因就在于如果我们在if之前进行使用,当第一次调用GetInstance创建对象成功以后,后面进行调用依然会进入到GetInstance函数内部,依然会进行加锁解锁,这样就会使程序耗费大量的时间在加锁解锁上面,显然这是没有必要的,所以在这儿我们就使用双重判断的方式,避免了频繁地进行加锁解锁操作。
懒汉模式另一种实现方式:
class SingleTon
{
public:
static SingleTon* GetInstance()
{
static SingleTon inst;
return &inst;
}
private:
// 构造函数私有
SingleTon() {}
// 析构函数私有
~SingleTon() {}
// 将拷贝构造函数以及运算符重载函数私有化或者delete
SingleTon(const SingleTon&) = delete;
SingleTon& operator=(const SingleTon&) = delete;
};
上面这种方式也可用于懒汉模式的创建:
- 由于实际只有第一次调用GetInstance函数时才会定义这个静态的单例对象,这也就保证了全局只有这一个唯一实例。
- 并且这里单例对象的定义过程是线程安全的,因为现在的C++标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作。
- 该方法属于懒汉模式,因为局部静态变量不是在程序运行主函数之前初始化的,而是在第一次调用GetInstance函数时初始化的。
这种方式的缺点就在于:
- 单例对象定义在静态区,因此太大的单例对象不适合使用这种方式。
- 单例对象创建在静态区后没办法主动释放。
饿汉模式和懒汉模式对比
- 饿汉模式的优点就是简单,但是它的缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,如果单例类的构造函数中所做的工作比较多(初始化动作多,还会伴随这一些IO行为,如读取配置文件),就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了。
- 此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为我们无法保证这多个单例对象中的哪个对象先创建。
- 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance函数第一次被调用的顺序决定,因此是可控制的。
懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题。
工厂模式
工厂模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用⼀个共同结构来指向新创建的对象,以此实现创建–使用的分离。
简单工厂模式
简单工厂模式实现由⼀个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能⽣产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工行内部去添加新产品的生产方式。
#include <iostream>
#include <memory>
#include <string>
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
void show()
{
std::cout << "我是一个苹果!!!" << std::endl;
}
};
class Banana : public Fruit
{
public:
Banana() {}
void show()
{
std::cout << "我是一个香蕉!!!" << std::endl;
}
};
class FruitFactory
{
public:
FruitFactory() {}
static std::shared_ptr<Fruit> Create(const std::string& name)
{
if (name == "苹果")
{
return std::make_shared<Apple>();
}
else if (name == "香蕉"){
return std::make_shared<Banana>();
}
return std::shared_ptr<Fruit>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = FruitFactory::Create("苹果");
fruit->show();
fruit = FruitFactory::Create("香蕉");
fruit->show();
return 0;
}
简单工厂模式可以通过参数控制可以生产任何产品,他的优点就在于:
- 简单粗暴,直观易懂;
- 使用一个工厂⽣产同⼀等级结构下的任意产品。
缺点:
- 所有东西生产在⼀起,产品太多会导致代码量庞大;
- 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改工厂方法。
工厂方法模式
在简单工厂模式下新增多个工厂,多个产品,每个产品对应⼀个工厂。假设现在有A、B 两种产品,则开两个工厂,工厂A 负责⽣产产品 A,工厂B 负责生产产品 B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
#include <iostream>
#include <memory>
#include <string>
class Fruit
{
public:
Fruit () {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
void show()
{
std::cout << "我是一个苹果!!!" << std::endl;
}
};
class Banana : public Fruit
{
public:
Banana() {}
void show()
{
std::cout << "我是一个香蕉!!!" << std::endl;
}
};
class FruitFactory
{
public:
FruitFactory() {}
virtual std::shared_ptr<Fruit> Create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
AppleFactory() {}
std::shared_ptr<Fruit> Create()
{
return std::make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
BananaFactory() {}
std::shared_ptr<Fruit> Create()
{
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = nullptr;
std::shared_ptr<FruitFactory> factory(new AppleFactory());
fruit = factory->Create();
fruit->show();
factory.reset(new BananaFactory());
fruit = factory->Create();
fruit->show();
return 0;
}
工厂方法模式的优点就在于:
- 减轻了工厂类的负担,将某类产品的⽣产交给指定的工厂来进行;
- 开闭原则遵循较好,添加新产品只需要新增产品的工厂即可,不需要修改原先的工厂类。
缺点:
- 对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建大量的工厂类,在⼀定程度上增加了系统的耦合度。
抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产⼀类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同⼀个工厂来统一生产,这就是抽象工厂模式的基本思想。
#include <iostream>
#include <memory>
#include <string>
class Fruit
{
public:
Fruit() {}
virtual void Show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
void Show()
{
std::cout << "我是一个苹果!!!" << std::endl;
}
};
class Banana : public Fruit
{
public:
Banana() {}
void Show()
{
std::cout << "我是一个香蕉!!!" << std::endl;
}
};
class Animal
{
public:
Animal() {}
virtual void Voice() = 0;
};
class Dog : public Animal
{
public:
Dog() {}
void Voice()
{
std::cout << "汪汪汪!!!" << std::endl;
}
};
class Lamp : public Animal
{
public:
Lamp() {}
void Voice()
{
std::cout << "咩咩咩!!!" << std::endl;
}
};
class Factory
{
public:
Factory() {}
virtual std::shared_ptr<Fruit> GetFruit(const std::string& name) = 0;
virtual std::shared_ptr<Animal> GetAnimal(const std::string& name) = 0;
};
class FruitFactory : public Factory
{
public:
FruitFactory() {}
std::shared_ptr<Animal> GetAnimal(const std::string& name)
{
return std::shared_ptr<Animal>();
}
std::shared_ptr<Fruit> GetFruit(const std::string& name)
{
if (name == "苹果")
{
return std::make_shared<Apple>();
}
else if (name == "香蕉")
{
return std::make_shared<Banana>();
}
return std::shared_ptr<Fruit>();
}
};
class AnimalFactory : public Factory
{
public:
AnimalFactory() {}
std::shared_ptr<Fruit> GetFruit(const std::string& name)
{
return std::shared_ptr<Fruit>();
}
std::shared_ptr<Animal> GetAnimal(const std::string& name)
{
if(name == "小狗")
{
return std::make_shared<Dog>();
}
else if (name == "小羊")
{
return std::make_shared<Lamp>();
}
return std::shared_ptr<Animal>();
}
};
class FactoryProducer
{
public:
FactoryProducer() {}
static std::shared_ptr<Factory> GetFactory(const std::string& name)
{
if (name == "水果")
{
return std::make_shared<FruitFactory>();
}
else if (name == "动物")
{
return std::make_shared<AnimalFactory>();
}
return std::shared_ptr<Factory>();
}
};
int main()
{
std::shared_ptr<Factory> fruit_factory = FactoryProducer::GetFactory("水果");
std::shared_ptr<Fruit> fruit = fruit_factory->GetFruit("苹果");
fruit->Show();
fruit = fruit_factory->GetFruit("香蕉");
fruit->Show();
std::shared_ptr<Factory> animal_factory = FactoryProducer::GetFactory("动物");
std::shared_ptr<Animal> animal = animal_factory->GetAnimal("小狗");
animal->Voice();
animal = animal_factory->GetAnimal("小羊");
animal->Voice();
return 0;
}
抽象工厂模式将产品族的依赖与约束关系放到抽象工厂中,便于管理;他可以实现职责解耦,用户不需要关心一堆自己不关心的细节,由抽象工厂来负责组件的创建,而且切换产品族容易,只需要增加一个具体工厂实现,客户端选择另一个套餐就可以了。
建造者模式
建造者模式是⼀种创建型设计模式, 使用多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表示分离,提供⼀种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核心类进行实现:
- 产品(Product):代表最终构建的复杂对象,包含多个部件和属性。产品类定义了对象的结构和行为,通常包括多个属性和方法。
- 建造者(Builder):定义创建产品的各个部件的抽象接口。建造者接口提供了构建不同部分的方法,允许灵活地创建不同类型的产品。
- 具体建造者(ConcreteBuilder):实现建造者接口,具体构建和装配产品的各个部件。每个具体建造者负责创建特定类型的产品,同时维护构建过程的状态。
- 指挥者(Director):负责管理建造过程,调用建造者的方法以生成产品。指挥者定义构建的顺序,确保产品的构建过程符合预期。
#include <iostream>
#include <memory>
#include <string>
// 创建一个抽象的产品类
class Computer
{
public:
using ptr = std::shared_ptr<Computer>;
Computer() {}
void SetBoard(const std::string& board)
{
_board = board;
}
void SetDisplay(const std::string& display)
{
_display = display;
}
virtual void SetOs() = 0;
std::string ToString()
{
std::string computer = "Computer:{\n";
computer += "\tboard=" + _board + ",\n";
computer += "\tdisplay=" + _display + ",\n";
computer += "\tOs=" + _os + ",\n";
computer += "}\n";
return computer;
}
protected:
std::string _board;
std::string _display;
std::string _os;
};
// 创建一个具体的产品类
class MacBook : public Computer
{
public:
using ptr = std::shared_ptr<MacBook>;
MacBook() {}
void SetOs()
{
_os = "Max Os X12";
}
};
// 创建一个抽象的建造者类
class Builder
{
public:
using ptr = std::shared_ptr<Builder>;
Builder() {}
virtual void BuildBoard(const std::string board) = 0;
virtual void BuildDisplay(const std::string display) = 0;
virtual void BuildOs() = 0;
virtual Computer::ptr Build() = 0;
};
// 创建一个具体的建造者类
class MacBookBuilder : public Builder
{
public:
using ptr = std::shared_ptr<MacBookBuilder>;
MacBookBuilder(): _computer(new MacBook()) {}
void BuildBoard(const std::string board)
{
_computer->SetBoard(board);
}
void BuildDisplay(const std::string display)
{
_computer->SetDisplay(display);
}
void BuildOs()
{
_computer->SetOs();
}
Computer::ptr Build()
{
return _computer;
}
private:
Computer::ptr _computer;
};
// 创建一个指挥者类
class Director
{
public:
using ptr = std::shared_ptr<Director>;
Director(Builder* builder): _builder(builder) {}
void Construct(const std::string& board, const std::string& display)
{
_builder->BuildBoard(board);
_builder->BuildDisplay(display);
_builder->BuildOs();
}
private:
Builder::ptr _builder;
};
int main()
{
Builder* builder = new MacBookBuilder();
std::unique_ptr<Director> dt(new Director(builder));
dt->Construct("英特尔主板", "VOC显示器");
Computer::ptr computer = builder->Build();
std::cout << computer->ToString();
return 0;
}
建造者模式的优缺点
优点:
- 解耦:建造者模式将复杂对象的构建与表示分离,使得构建过程与产品的表示相互独立,降低了模块间的耦合度。
- 灵活性:可以通过更换具体建造者来灵活地构建不同类型的产品,而无需修改指挥者或客户端代码。
- 可读性和可维护性:通过清晰的接口和结构,增加了代码的可读性,便于后续的维护和扩展。
- 支持复杂对象的构建:适合构建复杂对象,特别是那些具有多个部件和装配顺序要求的对象。
- 步骤控制:指挥者可以控制构建的步骤和顺序,确保复杂对象按照预期的方式构建。
缺点:
- 复杂性:对于简单对象的构建,使用建造者模式可能显得过于复杂,增加了不必要的代码和结构。
- 类的数量增加:需要创建多个具体建造者类和产品类,可能导致类的数量增加,增加了系统的复杂性。
- 构建过程固定:一旦定义了指挥者的构建过程,可能不够灵活,难以适应一些动态变化的需求。
代理模式
代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引用。在某些情况下,⼀个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用;代理模式提供了额外的功能,如延迟加载、访问控制、日志记录等。
代理模式的结构包括⼀个是真正的你要访问的对象(目标类)、⼀个是代理对象。目标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问目标对象。代理模式分为静态代理、动态代理:
- 静态代理:在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
- 动态代理:在运行时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。
主要组成成分
- 抽象主题(Subject):定义了真实对象和代理对象的共同接口。
- 真实主题(Real Subject):实现了抽象主题接口,代表实际的业务逻辑。
- 代理(Proxy):持有对真实主题的引用,并实现了抽象主题接口。代理可以在访问真实主题之前或之后添加额外的功能。
以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示, 带人看房,负责维修,这些工作中有些操作并非房东能完成,因此房东为了图省事,将房子委托给中介进行租赁。 代理模式实现:
#include <iostream>
#include <string>
class RentHouse
{
public:
RentHouse() {}
virtual void rentHouse() = 0;
};
// 创建一个房东类,负责将房子租出去
class LandLord : public RentHouse
{
public:
LandLord() {}
void rentHouse()
{
std::cout << "把房子租出去" << std::endl;
}
};
// 创建一个中介类,对租房子的功能进行加强,实现租房以外的其他功能
class Intermediary
{
public:
void rentHouse()
{
std::cout << "发布租房启示" << std::endl;
std::cout << "带人看房" << std::endl;
_landlord.rentHouse();
std::cout << "负责房子的售后服务" << std::endl;
}
private:
LandLord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}
代理模式的优缺点:
优点:
- 控制访问:代理可以控制对真实对象的访问,增加安全性。
- 延迟加载:可以在需要时才创建真实对象,节省资源。
- 增强功能:可以在访问真实对象前后添加额外的功能,如日志记录、缓存等。
缺点:
- 增加复杂性:引入代理可能会增加系统的复杂性,尤其是在管理代理和真实对象时。
- 性能开销:代理可能会引入额外的性能开销,尤其是在处理大量请求时。
代理模式的分类
- 远程代理 (Remote Proxy):用于控制对远程对象的访问,通常用于网络编程中,例如通过网络调用远程服务。
- 虚拟代理 (Virtual Proxy):用于延迟加载资源,避免不必要的开销。只有在真正需要时才创建真实对象,如大文件或复杂对象的加载。
- 保护代理 (Protection Proxy):用于控制对对象的访问权限,增强安全性。可以根据不同用户的权限来决定是否允许访问某些方法或数据。
- 缓存/缓冲代理 (Cache Proxy):用于缓存频繁访问的数据,以减少计算或网络请求的开销。例如,缓存数据库查询结果或API响应。
- 智能引用代理 (Smart Reference Proxy):用于管理对象的生命周期,例如引用计数或智能指针。可以确保对象在不再需要时被正确释放,避免内存泄漏。
- 写时代理 (Write Proxy):用于延迟写入操作,直到需要时才执行。可以用于优化性能,尤其在处理大量数据时。