设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案(设计思想、设计经验)。
一、六大原则
1、单一职责原则(Single Responsibility Principle)
类的职责应该单一,一个方法只做一件事。
(1)使用建议
两个完全不⼀样的功能不应该放一个类中,一个类中应该是一组相关性很高的函数、数据的封装。
(2)用例
网络聊天:网络络通信 & 聊天,应该分割成为网络通信类 & 聊天类。
2、开闭原则(Open Closed Principle)
对扩展开放,对修改封闭。
(1)使用建议
对软件实体的改动,最好用扩展而非修改的方式。
(2)用例
超时卖货:商品价格 —— 不是修改商品的原来价格,而是新增促销价格。
3、里氏替换原则(Liskov Substitution Principle)
就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常。
在继承类时,一定要重写父类中所有的方法,尤其需要注意父类的 protected 方法,子类尽量不要暴露自己的 public 方法供外界调用。
(1)使用建议
子类必须完全实现父类的方法,子类可以有自己的个性。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小(为了父类替换成子类时不会出错)
(2)用例
跑步运动员类 —— 会跑步,子类长跑运动员 —— 会跑步且擅长长跑,子类短跑运动员 —— 会跑步且擅长短跑
4、依赖倒置原则(Dependence Inversion Principle)
高层模块不应该依赖低层模块,两者都应该依赖其抽象。不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。
模块间依赖通过抽象(接口)发生,具体类之间不直接依赖。
(1)使用建议
- 每个类都尽量有抽象类,任何类都不应该从具体类派生。
- 尽量不要重写基类的方法(结合里氏替换原则使用)。
(2)用例
奔驰车司机类 —— 只能开奔驰; 司机类 —— 给什么车,就开什么车; 开车的人:司机 —— 依赖于抽象
5、迪米特法则(Law of Demeter)
又叫 “最少知道法则”。
尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:只和直接的朋友交流, 朋友之间也是有距离的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就将其放置在本类中)
(1)用例
老师让班长点名 —— 老师给班长一个名单,班长完成点名勾选,返回结果,而不是班长点名,老师勾选。
6、接口隔离原则(Interface Segregation Principle)
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。
(1)使用建议
接口设计尽量精简单一,但是不要对外暴露没有实际意义的接口。
(2)用例
修改密码,不应该提供修改用户信息接口,而就是单一的最小修改密码接口,更不要暴露数据库操作。
二、单例模式
什么是单例设计模式?
单例模式是一种创建型设计模式, 它的核心思想是一个类只能创建一个对象,该设计模式可以保证系统中该类只有一个实例,并提供一个全局访问点来访问这个实例,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例设计模式的基本要求:
- 私有的构造函数:防止外部代码直接创建类的实例
- 私有的静态实例变量:保存该类的唯一实例
- 公有的静态方法:通过公有的静态方法来获取类的实例
为什么要使用单例设计模式呢?
单例设计模式有以下几个优点:
- 全局控制:保证只有一个实例,这样就可以严格的控制客户如何访问它以及何时访问它,简单的说就是对唯一实例的受控访问(引用自《大话设计模式》第21章)
- 节省资源:也正是因为只有一个实例存在,就避免多次创建了相同的对象,从而节省了系统资源,而且多个模块还可以通过单例实例共享数据。
- 懒加载:单例模式可以实现懒加载,只有在需要时才进行实例化,这无疑会提高程序的性能。
在什么场景下考虑使用单例设计模式呢?
可以结合单例设计模式的优点来看:
- 资源共享。多个模块共享某个资源的时候,可以使用单例模式,比如说应用程序需要一个全局的配置管理器来存储和管理配置信息、亦或是使用单例模式管理数据库连接池。
- 只有一个实例。当系统中某个类只需要一个实例来协调行为的时候,可以考虑使用单例模式, 比如说管理应用程序中的缓存,确保只有一个缓存实例,避免重复的缓存创建和管理,或者使用单例模式来创建和管理线程池。
- 懒加载。如果对象创建本身就比较消耗资源,而且可能在整个程序中都不一定会使用,可以使用单例模式实现懒加载。
1、饿汉模式(以空间换时间)
饿汉模式指的是在类加载时就已经完成了实例的创建,不管后面创建的实例是否使用,先创建再说,所以被称为 “饿汉模式”。
在多线程环境下,由于饿汉模式在程序启动阶段就完成了实例的初始化,因此不存在多个线程同时尝试初始化实例的问题。程序启动时就会创建一个唯一的实例对象。因为单例对象已经确定, 所以比较适用于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争,提高性能 / 响应速度。
静态成员变量只能在类域外进行定义初始化,所以在 main 函数之前就将单例对象定义初始化,此时该单例对象创建在静态区上,而且仅有一个,后面就无法再创建。
想要获取该单例对象只能通过静态成员函数 getInstance() 来获取,静态成员函数可以直接访问静态成员变量 _data。
静态对象是在静态区的,它的生命周期是随着整个程序的,它的初始化构造是在程序初始化阶段完成的。
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
(1)优点
- 保证全局(整个进程)只有唯一实例对象。
- 饿汉模式一开始就创建对象,特别简单。
(2)缺点
- 假设有多个单例对象 A、B、C,要求它们之间有依赖关系,如果依次创建就无法达到,无法保证顺序可能会导致进程启动速度很慢。
- 实例在类加载时就被创建,这种方式的实现相对简单,但是实例有可能没有使用而造成资源浪费。
2、懒汉模式
懒汉模式指的是只有在请求实例时才会创建,如果在首次请求时还没有创建,就创建一个新的实例,如果已经创建,就返回已有的实例,意思就是需要使用了再创建,所以被称为 “懒汉模式”。
第一次使用要使用单例对象的时候创建实例对象。如果单例对象构造特别耗费时间或者资源(加载插件、加载网络资源等),可以选择懒汉模式,在第一次使用的时候才创建对象。
在多线程环境下,懒汉模式中会出现多个线程同时访问 getInstance() 方法,并且在同一时刻检测到实例没有被创建,就可能会同时创建实例,从而导致多个实例被创建,这种情况下可以采用一些同步机制,例如使用互斥锁来确保在任何时刻只有一个线程能够执行实例的创建。
举例:张三和李四都发现家里没米了,在他们没有相互通知的情况下,都会去超市买一袋米,这样就重复购买了,违背了单例模式。
如何保证在使用单例对象时才进行实例化呢?不是直接实例化对象,而是定义一个对象的指针(静态资源指针),然后再通过访问接口时发现它为空指针,接着再进行 new 对象。但是这样做会出现一个有关线程安全的问题(当多线程时,进行判断为 nullptr,这时还没有调用 new,当前的线程就被切走了。下一个线程来了还是 nullptr 就又进去 new 了一个对象,然后恢复第一个线程的上下文后又 new 了一个对象,第二个 new 的就将第一个的给覆盖了,所以就出现了错误)。这里可以使用互斥锁(因为加锁需要加在一个锁上才有用,所以也要将锁设为静态的,然后在类外进行初始化,这样就可以保证只实例化出一个对象)。但是加锁之后又造成了锁冲突,导致串行化进行(当 new 出一个对象之后,在进行调用 getInstance 的时候,还会不停的加锁解锁),效率降低(加锁解锁是有性能消耗的),所以有了双检测加锁(Double-Check 检测)。但是又涉及到了代码指令顺序的问题,所以得加上一个 volatile 关键字来修饰。以上需要关注的因素很多,下面不采取这种方式。了解更多可参考:【C++】特殊类设计-CSDN博客
- 《Effective C++》的作者 Scott Meyers 提出的一种优雅简便的单例模式 Meyers' Singleton in C++。
- C++11 Static local variables 特性以确保 C++11 起,静态变量将能够在满足 thread-safe 的前提下唯一地被构造和析构。
翻译:
如果多个线程试图同时初始化同一个静态局部变量,则初始化只会发生一次(使用std::call_once可以为任意函数获得类似的行为)。
此功能的通常实现使用双重检查锁定模式的变体,这将已经初始化的局部静态的运行时开销减少到单个非原子布尔比较。
(1)优点
- 第一次使用实例对象时创建对象。
- 进程启动无负载。
- 多个单例实例启动顺序(通过代码顺序)自由控制。
(2)缺点
- 复杂。
- 如果不加锁是会出现线程安全的问题。但是加锁是会十分影响性能的,所以引入了双检查。那么既要保证线程安全 + 又要保证效率的问题。(原先做法)
为什么称之为懒汉模式呢?
懒汉模式又叫做延时加载模式。如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢,所以这种情况使用懒汉模式(延迟加载)更好。
对应题目链接:【设计模式专题之单例模式】1.小明的购物车 (kamacoder.com)
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
class Shopping
{
public:
static Shopping& getInstance()
{
static Shopping _eton;
return _eton;
}
void add(string item, int num)
{
order.push_back(item);
shopcar[item] += num;
}
void show()
{
for(auto e : order)
{
cout << e << ' ' << shopcar.at(e) << endl;
}
}
Shopping(const Shopping&) = delete;
~Shopping() {}
private:
Shopping()
{}
private:
unordered_map<string, int> shopcar;
vector<string> order;
};
int main()
{
Shopping& eton = Shopping::getInstance();
string item;
int num;
while(cin >> item >> num)
{
eton.add(item, num);
}
eton.show();
return 0;
}
三、工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建 —— 使用的分离。
1、简单工厂模式
简单工厂模式是一种创建型设计模式,但并不属于 23 种设计模式之一,更多的是一种编程习惯。简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。
简单工厂模式的核心思想是将产品的创建过程封装在一个工厂类中,把创建对象的流程集中在这个工厂类里面。简单工厂模式包括三个主要角色,工厂类、抽象产品、具体产品。
- 抽象产品:描述产品的通用行为。
- 具体产品:实现抽象产品接口或继承抽象产品类,具体产品通过简单工厂类的 if-else 逻辑来实例化。
- 工厂类:负责创建产品,根据传递的不同参数创建不同的产品示例。
假设有一个工厂能生产出水果,当客户需要产品的时候就明确告知工厂要生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。 通过参数控制可以生产任何产品。
这个模式的结构和管理产品对象的方式十分简单,简化了客户端操作,客户端可以调用工厂方法来获取具体产品,而无需直接与具体产品类交互,降低了耦合,但是有一个很大的问题,就是它的扩展性非常差,不够灵活,如果需要新增产品,就需要去修改工厂类新增一个类型的产品创建逻辑,违背了开闭原则。
在继承中要构成多态还有 2 个条件:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
virtual void name() = 0; 是一个纯虚函数的声明,表示这个函数在基类中没有具体实现。基类 Fruit 包含了纯虚函数,它不能被实例化,因此它是一个抽象类。name() 方法在 Fruit 类中没有实现,任何继承自 Fruit 的类都必须提供 name() 的具体实现。
(1)优点
- 简单粗暴,易于理解。使用一个工厂生产同一等级结构下的任意产品。
(2)缺点
- 所有东西生产在一起,产品太多的话会导致代码量庞大。
- 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须要修改工厂方法。
2、工厂方法模式
工厂方法模式也是一种创建型设计模式,引入了抽象工厂和具体工厂的概念,每个具体工厂只负责创建一个具体产品,添加新的产品只需要添加新的工厂类而无需修改原来的代码,这样就使得产品的生产更加灵活,支持扩展,符合开闭原则。
在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。
工厂方法模式分为以下几个角色:
- 抽象工厂:一个接口,包含一个抽象的工厂方法(用于创建产品对象)。
- 具体工厂:实现抽象工厂接口,创建具体的产品。
- 抽象产品:定义产品的接口。
- 具体产品:实现抽象产品接口,是工厂创建的对象。
假设现在有 A、B 两种产品,则开两个工厂,工厂 A 负责生产产品 A,工厂 B 负责生产产品 B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。实际上工厂方法模式也很好理解,举例:手机是一个抽象产品,小米手机、华为手机、苹果手机是具体的产品实现,而不同品牌的手机在各自的生产厂家生产。
定义一个创建对象的接口,但是由子类来决定创建哪种对象,使用多个工厂分别生产指定的固定产品。
工厂方法模式每次增加一个产品时,都需要增加一个具体产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加了系统的耦合度。
应用场景:工厂方法模式使得每个工厂类的职责单一,每个工厂只负责创建一种产品,当创建对象涉及一系列复杂的初始化逻辑,而这些逻辑在不同的子类中可能有所不同时,可以使用工厂方法模式将这些初始化逻辑封装在子类的工厂中。
(1)优点
- 减轻了工厂类的负担,将某类产品的生产交给指定的工厂来进行。
- 开闭原则遵循较好,添加新产品只需要新增产品的工厂即可,不需要修改原先的工厂类。
(2)缺点
- 对于某种可以形成一组产品族的情况处理较为复杂,需要创建大量的工厂类。
对应题目链接: 【设计模式专题之工厂方法模式】2.积木工厂 (kamacoder.com)
#include <iostream>
#include <memory>
using namespace std;
class Block
{
public:
virtual void produce() = 0;
};
class Circle : public Block
{
public:
void produce() override
{
cout << "Circle Block" << endl;
}
};
class Square : public Block
{
public:
void produce() override
{
cout << "Square Block" << endl;
}
};
class BlockFactory
{
public:
virtual shared_ptr<Block> create() = 0;
};
class CircleBlockFactory : public BlockFactory
{
public:
shared_ptr<Block> create() override
{
return make_shared<Circle>();
}
};
class SquareBlockFactory : public BlockFactory
{
public:
shared_ptr<Block> create() override
{
return make_shared<Square>();
}
};
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i++)
{
string type;
int num;
cin >> type >> num;
if(type == "Circle")
{
while(num--)
{
std::shared_ptr<BlockFactory> bf(new CircleBlockFactory());
std::shared_ptr<Block> b = bf->create();
b->produce();
}
}
else if(type == "Square")
{
while(num--)
{
std::shared_ptr<BlockFactory> bf(new SquareBlockFactory());
std::shared_ptr<Block> b = bf->create();
b->produce();
}
}
}
return 0;
}
3、抽象工厂模式
抽象工厂模式也是一种创建型设计模式,提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类(引用自大话设计模式第 15 章)。
为什么还有要抽象工厂模式呢?
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时可以考虑将⼀些相关的产品组成⼀个产品族(位于不同的产品等级结构中,功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
这就涉及到创建 “多类” 对象了,在工厂方法模式中,每个具体工厂只负责创建单一的产品。但是如果有多类产品呢,比如:“手机”,一个品牌的手机有高端机、中低端机之分,这些具体的产品都需要建立一个单独的工厂类,但是它们都是相互关联的,都共同属于同一个品牌,这就可以使用到抽象工厂模式。
抽象工厂模式可以确保一系列相关的产品被一起创建,这些产品能够相互配合使用。举例:有一些家具,比如沙发、茶几、椅子,都具有古典风格的和现代风格的,抽象工厂模式可以将生产现代风格的家具放在一个工厂类中,将生产古典风格的家具放在另一个工厂类中,这样每个工厂类就可以生产一系列的家具。
围绕一个超级工厂创建其他工厂。每个生成的工厂按照工厂模式提供对象。
基本结构:
抽象工厂模式包含多个抽象产品接口,多个具体产品类,一个抽象工厂接口和多个具体工厂,每个具体工厂负责创建一组相关的产品。
- 抽象产品接口:定义产品的接口,可以定义多个抽象产品接口,比如说沙发、椅子、茶几都是抽象产品。
- 具体产品类:实现抽象产品接口,产品的具体实现,古典风格和沙发和现代风格的沙发都是具体产品。
- 抽象工厂接口:声明一组用于创建产品的方法,每个方法对应一个产品。
- 具体工厂类:实现抽象工厂接口,负责创建一组具体产品的对象,在本例中,生产古典风格的工厂和生产现代风格的工厂都是具体实例。
基本实现:
- 定义抽象产品接口(可以有多个),接口中声明产品的公共方法。
- 实现具体产品类,在类中实现抽象产品接口中的方法。
- 定义抽象工厂接口,声明一组用于创建产品的方法。
- 实现具体工厂类,分别实现抽象工厂接口中的方法,每个方法负责创建一组相关的产品。
- 在客户端中使用抽象工厂和抽象产品,而不直接使用具体产品的类名。
应用场景:抽象工厂模式能够保证一系列相关的产品一起使用,并且在不修改客户端代码的情况下,可以方便地替换整个产品系列。但是当需要增加新的产品类时,除了要增加新的具体产品类,还需要修改抽象工厂接口及其所有的具体工厂类,扩展性相对较差。因此抽象工厂模式特别适用于一系列相关或相互依赖的产品被一起创建的情况,典型的应用场景是使用抽象工厂模式来创建与不同数据库的连接对象。
(1)思想
将工厂抽象成两层:抽象工厂 & 具体工厂子类,在工厂子类种生产不同类型的子产品。
抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了 “开闭原则”。
简单工厂、工厂方法、抽象工厂的区别
- 简单工厂模式:一个工厂方法创建所有具体产品。
- 工厂方法模式:一个工厂方法创建一个具体产品。
- 抽象工厂模式:一个工厂方法可以创建一类具体产品。
对应题目链接:【设计模式专题之抽象工厂模式】3. 家具工厂 (kamacoder.com)
#include <iostream>
#include <memory>
using namespace std;
class Modern
{
public:
virtual void show() = 0;
};
class ModernChair : public Modern
{
public:
void show() override
{
cout << "modern chair" << endl;
}
};
class ModernSofa : public Modern
{
public:
void show() override
{
cout << "modern sofa" << endl;
}
};
class Classical
{
public:
virtual void show() = 0;
};
class ClassicalChair : public Classical
{
public:
void show() override
{
cout << "classical chair" << endl;
}
};
class ClassicalSofa : public Classical
{
public:
void show() override
{
cout << "classical sofa" << endl;
}
};
class Factory
{
public:
virtual shared_ptr<Modern> getModern(const string& name) = 0;
virtual shared_ptr<Classical> getClassical(const string& name) = 0;
};
class ModernFactory : public Factory
{
public:
shared_ptr<Modern> getModern(const std::string& name) override
{
if(name == "chair")
{
return make_shared<ModernChair>();
}
else
{
return make_shared<ModernSofa>();
}
}
shared_ptr<Classical> getClassical(const string& name) override
{
return nullptr;
}
};
class ClassicalFactory : public Factory
{
public:
shared_ptr<Modern> getModern(const std::string& name) override
{
return nullptr;
}
shared_ptr<Classical> getClassical(const string& name) override
{
if(name == "sofa")
{
return make_shared<ClassicalSofa>();
}
else
{
return make_shared<ClassicalChair>();
}
}
};
class FactoryProducer
{
public:
static shared_ptr<Factory> create(const std::string& name) //直接引用静态成员接口,无需实例化对象即可完成工厂的创建
{
if(name == "modern")
{
return make_shared<ModernFactory>();
}
else
{
return make_shared<ClassicalFactory>();
}
}
};
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i++)
{
string type;
cin >> type;
shared_ptr<Factory> f = FactoryProducer::create(type);
if(type == "modern")
{
shared_ptr<Modern> m = f->getModern("chair");
m->show();
m = f->getModern("sofa");
m->show();
}
else
{
shared_ptr<Classical> m = f->getClassical("chair");
m->show();
m = f->getClassical("sofa");
m->show();
}
}
return 0;
}
四、建造者模式
建造者模式(也被成为生成器模式),是一种创建型设计模式,软件开发过程中有的时候需要创建很复杂的对象,而建造者模式的主要思想是:将对象的构建过程分为多个步骤,并为每个步骤定义一个抽象的接口。具体的构建过程由实现了这些接口的具体建造者类来完成。同时有一个指导者类负责协调建造者的工作,按照一定的顺序或逻辑来执行构建步骤,最终生成产品。
举例:假如要创建一个计算机对象,计算机由很多组件组成,例如 CPU、内存、硬盘、显卡等。每个组件可能有不同的型号、配置和制造,这个时候计算机就可以被视为一个复杂对象,构建过程相对复杂,而我们使用建造者模式将计算机的构建过程封装在一个具体的建造者类中,而指导者类则负责指导构建的步骤和顺序。每个具体的建造者类可以负责构建不同型号或配置的计算机,客户端代码可以通过选择不同的建造者来创建不同类型的计算机,这样就可以根据需要构建不同表示的复杂对象,更加灵活。
使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。
基本结构:
- 抽象产品类:被构建的复杂对象,包含多个组成部分。
- 具体产品类:⼀个具体的产品对象类。
- 抽象建造者 Builder 类:创建一个产品对象所需的各个部件的抽象接口。(定义构建产品各个部分的抽象接口和一个返回复杂产品的方法)
- 具体产品的建造者 Builder 类:实现抽象接口,构建和组装各个部件。(实现抽象建造者接口,构建产品的各个组成部分,并提供一个方法返回最终的产品)
- 指挥者 Director 类:统一组建过程,提供给调用者使用,通过指挥者来构造产品。(调用具体建造者的方法,按照一定的顺序或逻辑来构建产品)
使用建造者模式有下面几处优点:
-
使用建造者模式可以将一个复杂对象的构建与其表示分离,通过将构建复杂对象的过程抽象出来,可以使客户端代码与具体的构建过程解耦。
-
同样的构建过程可以创建不同的表示,可以有多个具体的建造者(相互独立),可以更加灵活地创建不同组合的对象。
对应的,建造者模式适用于复杂对象的创建,当对象构建过程相对复杂时可以考虑使用建造者模式,但是当产品的构建过程发生变化时,可能需要同时修改指导类和建造者类,这就使得重构变得相对困难。
对应题目链接:【设计模式专题之建造者模式】4. 自行车加工 (kamacoder.com)
#include <iostream>
#include <memory>
using namespace std;
class Bike
{
public:
Bike() {}
virtual void setFrame() = 0;
virtual void setTires() = 0;
};
class MountainBike : public Bike
{
public:
void setFrame() override
{
cout << "Aluminum Frame" << ' ';
}
void setTires() override
{
cout << "Knobby Tires" << endl;
}
};
class RoadBike : public Bike
{
public:
void setFrame() override
{
cout << "Carbon Frame" << ' ';
}
void setTires() override
{
cout << "Slim Tires" << endl;
}
};
class Builder
{
public:
virtual void buildFrame() = 0;
virtual void buildTires() = 0;
};
class MountainBikeBuilder : public Builder
{
public:
MountainBikeBuilder()
:_bike(new MountainBike())
{}
void buildFrame() override
{
_bike->setFrame();
}
void buildTires() override
{
_bike->setTires();
}
private:
std::shared_ptr<Bike> _bike;
};
class RoadBikeBuilder : public Builder
{
public:
RoadBikeBuilder()
:_bike(new RoadBike())
{}
void buildFrame() override
{
_bike->setFrame();
}
void buildTires() override
{
_bike->setTires();
}
private:
std::shared_ptr<Bike> _bike;
};
class Director
{
public:
Director(Builder* builder)
:_builder(builder)
{}
void construct()
{
_builder->buildFrame();
_builder->buildTires();
}
private:
unique_ptr<Builder> _builder;
};
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i++)
{
string type;
cin >> type;
if(type == "mountain")
{
Builder* builder = new MountainBikeBuilder();
unique_ptr<Director> director(new Director(builder));
director->construct();
}
else
{
Builder* builder = new RoadBikeBuilder();
unique_ptr<Director> director(new Director(builder));
director->construct();
}
}
return 0;
}
五、代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,用于代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。
在代理模式中,允许一个对象(代理)充当另一个对象(真实对象)的接口,以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用,这样可以在访问对象时添加额外的控制逻辑,比如限制访问权限,延迟加载。代理模式的结构包括:一个是真正你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。
比如说有一个文件加载的场景,为了避免直接访问 “文件” 对象,可以新增一个代理对象,代理对象中有一个对 “文件对象” 的引用,在代理对象的 load 方法中,可以在访问真实的文件对象之前进行一些操作,比如权限检查,然后调用真实文件对象的 load 方法,最后在访问真实对象后进行其他操作,比如记录访问日志。
以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示,带人看房,负责维修,这些工作中有些操作并非房东能完成,因此房东为了省事,将房子委托给中介进行租赁。
基本结构:
- Subject(抽象主题): 抽象类,通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- RealSubject(真实主题):定义了 Proxy 所代表的真实对象,是客户端最终要访问的对象。
- Proxy(代理):包含一个引用,该引用可以是 RealSubject 的实例,控制对 RealSubject 的访问,并可能负责创建和删除 RealSubject 的实例。
使用场景:代理模式可以控制客户端对真实对象的访问,从而限制某些客户端的访问权限,此外代理模式还常用在访问真实对象之前或之后执行一些额外的操作(比如记录日志),对功能进行扩展。
以上特性决定了代理模式在以下几个场景中有着广泛的应用:
- 虚拟代理:当一个对象的创建和初始化比较昂贵时,可以使用虚拟代理,虚拟代理可以延迟对象的实际创建和初始化,只有在需要时才真正创建并初始化对象。
- 安全代理:安全代理可以根据访问者的权限决定是否允许访问真实对象的方法。
但是代理模式涉及到多个对象之间的交互,引入代理模式会增加系统的复杂性,在需要频繁访问真实对象时,还可能会有一些性能问题。
对应题目链接:【设计模式专题之代理模式】7-小明买房子 (kamacoder.com)
#include <iostream>
using namespace std;
class RentHouse
{
public:
virtual void rentHouse(int area) = 0;
};
class Landlord : public RentHouse
{
public:
void rentHouse(int area) override
{
cout << "YES" << endl;
}
};
class Proxy : public RentHouse
{
public:
void rentHouse(int area) override
{
if(area > 100)
{
_landlord.rentHouse(area);
}
else
{
cout << "NO" << endl;
}
}
private:
Landlord _landlord;
};
int main()
{
Proxy proxy;
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
int area;
cin >> area;
proxy.rentHouse(area);
}
return 0;
}
1、静态代理
在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
2、动态代理
在运行时才动态生成代理类,并将其与被代理类绑定。这就意味着,在运行时才能确定代理类要代理的是哪个被代理类。