设计模式
2023/1/31
设计模式为了复用
如何解决复杂问题
- 分解
- 分而治之,大问题分解为小问题,复杂问题分解为多个简单问题。
- 抽象
- 由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化后的对象模型。
如果要存储一个表示多态的对象,应该存储基类的指针类型;需要注意的是,往vec中写数据时,需要存储的是堆对象(new Object),而不是栈对象。因此在析构的时候也要注意释放堆内存。因此base类需要写一个虚的析构函数。多态调研,各负其责。
vector<Shape*> vec; // 正确做法
vector<Shape> vec; // 如果实际存储的是Shape的子类,这种情况会导致对象切割
class Shape{
public:
virtual ~Shape() {} // 虚析构 虚方法在调用的时候根据里面的实际对象类型去调用自身的方法实现
}
面向对象设计原则
- 依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖低层模块(变化),二者都应该依赖于抽象(稳定)。
- 抽象(稳定)不应该依赖实现细节(变化),实现细节应该依赖抽象(稳定)。
- 开闭原则(OCP)
- 对扩展开放,对更改封闭。
- 类模块应该是可扩展的,但是不可修改的。
- 单一职责(SRP)
- 一个类应该仅有一个引起它变化的原因。
- 变化的方向隐含着类的责任。
- Liskov 替换原则(LSP)
- 子类必须能够替换它们的基类。
- 继承表达类型抽象。
- 接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不用的方法。
- 接口应该小而完备。
- 优先使用对象组合,而不是类继承
- 类继承通常为“白箱复用”, 对象组合通常为“黑箱复用”。
- 继承在某种程度上破坏了封装性,子父类耦合度高。
- 对象组合只要求被组合对象具有良好定义的接口,耦合度低。
- 封装变化点
- 使用封装来创建对象之间的分界层,在一侧进行修改,不影响另一侧。
- 针对接口编程,而不是针对实现
- 不将变量类型声明为某个特定的具体类,而是声明为某个接口,比如基类。
- 客户程序无需获知对象的具体类型,只需知道接口。
- 减少系统中各部分的依赖关系,实现“高内聚,低耦合”。
设计模式分类
重构获得模式 Refactoring to Patterns
没有一步到位的设计模式,敏捷开发倡导 Refactoring to Patterns 是目前公认的最好的使用设计模式的方法。
模版方法 Template method
定义一个操作中的算法的骨架(稳定),将一些步骤延迟到子类中,Template method使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。也就是将不确定具体细节的步骤写成虚方法,在子类中实现。
策略模式 Strategy
定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换(变化)。该模式使得算法可独立于使用它的客户程序而变化。
class TaxStrategy {
public:
virtual double Calculate(const Context& ctx) = 0;
virtual ~TaxStrategy() {}
};
class CNTax : public TaxStrategy {
public:
virtual double Calculate(const Context& ctx) {
// ...
}
};
class USTax : public TaxStrategy {
public:
virtual double Calculate(const Context& ctx) {
// ...
}
};
class DETax : public TaxStrategy {
public:
virtual double Calculate(const Context& ctx) {
// ...
}
};
class SalesOrder {
private:
TaxStrategy* strategy; // 这里必须使用指针,才可以多态
public:
SalesOrder(StrategyFactory* strategyFactory) { // 这里是通过工厂模式
this->strategy = strategyFactory->NewStrategy(); // 返回在堆上开辟的内存对象
}
~SalesOrder() {
delete this->strategy;
}
public void CalculateTax() {
Context context();
double val = strategy->Calculate(context); // 多态调用
}
}
观察者模式 Observer
需要为某些对象建立一种“通知依赖关系”,一个对象的状态发生改变,所有的依赖对象(观察者)都将得到通知。
/**
* 观察者模式
* 场景为文件分割工具的进度条显示
*/
// 多继承的建议方法是只有一个主继承类,其他为接口
class MainForm : public Form, public IProgress {
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSpliter spilter(filePath, number);
// 添加多个观察者
spilter.addIProgress(this); // 订阅通知
ConsoleNotifer cn;
spilter.addIProgress(&cn); // 订阅通知
spilter.split();
//...
spilter.removeIProgress(&cn); // 取消订阅
}
virtual void DoProgress(float value) {
progressBar->setValue(value);
}
};
// 进度条
class IProgress {
public:
virtual void DoProgress(float value) = 0;
virtual ~IProgress() {}
};
class ConsoleNotifer : public IProgress {
public:
virtual void DoProgress(float value) {
///
}
}
// 文件分割类
class FileSpliter{
string m_filePath;
int m_fileNumber;
List<IProgress*> m_iprogressList; // 抽象通知机制 支持多个观察者
// ProgressBar* progressBar; 具体通知控件 紧耦合 且 依赖倒置
public:
FileSpliter(const string& filePath, int fileNumber) :
m_filePath(filePath),m_fileNumber(fileNumber){}
void addIProgress(IProgress* iprogress) {
m_iprogressList.add(iprogress);
}
void removeIProgress(IProgress* iprogress) {
m_iprogressList.remove(iprogress);
}
void split() {
// 1. 读取文件
// 2. 分批次写入小文件
for (int i = 0; i < m_fileNumber; ++i) {
float rate = m_fileNumber;
rate = (i+1)/rate;
notify(rate);
}
}
protected:
void notify(float value) {
List<IProgress*>::Iterator itor = m_iprogressList.begin();
while (itor != m_iprogressList.end())
{
(*itor)->DoProgress(value); // 更新进度条
itor++;
}
}
};
装饰模式 Decorator
单一职责 表征明显
定义:动态地(组合而非继承)给一个对象增加一些额外的职责,就增加功能而言,Decorator模式比子类继承更为灵活(消除重复代码&减少子类个数)。
Demo:原始继承关系 类的个数 1+n+n*m!/2
重构后的结构:类的个数1+n+1+m
/**
* 装饰者模式 Decorator
**/
// 基类
class Stream {
public:
virtual char Read(int number) = 0;
virtual void Seek(int pos) = 0;
virtual void Write(char data) = 0;
};
class FileStream : public Stream {
public:
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
class NetworkStream : public Stream {
public:
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
class MemoryStream : public Stream {
public:
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
// 添加功能的装饰类
class DecoratorStream: public Stream {
protected:
Stream* stream;
public:
DecoratorStream(Stream* stm):stream(stm) {}
};
// 添加的功能类
class CryptoStream : public DecoratorStream {
public:
CryptoStream(Stream* stm):DecoratorStream(stm){}
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
// 添加的功能类
class BufferedStream : public DecoratorStream {
public:
BufferedStream(Stream* stm):DecoratorStream(stm){}
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
int main() {
FileStream* fs1 = new FileStream();
NetworkStream* ns1 = new NetworkStream();
CryptoStream* cs1 = new CryptoStream(fs1);
BufferedStream* bs1 = new BufferedStream(fs1);
BufferedStream* bs2 = new BufferedStream(cs1); // 实现功能组合 对文件Stream即加密又缓存
}
或者使用下面这种,DecoratorStream类是为了将功能类的公共部分Stream*提取出来
// 添加的功能类
class CryptoStream1 : public Stream {
protected:
Stream* stream;
public:
CryptoStream1(Stream* stm):stream(stm){}
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
// 添加的功能类
class BufferedStream1 : public Stream {
protected:
Stream* stream;
public:
BufferedStream1(Stream* stm):stream(stm){}
virtual char Read(int number) {}
virtual void Seek(int pos) {}
virtual void Write(char data) {}
};
单例模式
单例模式也称单子模式、单件模式,通过单例模式可以保证系统中只有一个类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。对于系统中的某些类来说,只有一个实例很重要,比如一个打印机可以有多个打印任务,但是只有一个正在工作的任务,一个系统只能有一个窗口管理器或文件系统
单例模式的要点有三个:
- 单例类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供提供这个实例
从具体实现角度来说,就是以下三点:
- 单例模式的类只提供私有的构造函数
- 类定义中含有一个该类的静态私有对象
- 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。
单例模式的优点:
1. 在内存中只有一个对象,节省内存空间
2. 避免频繁的创建销毁对象,可以提高性能
3. 避免对共享资源的多重占用
4. 可以全局访问
单例模式的适用场景:
1. 需要频繁实例化然后销毁的对象
2. 创建对象耗时过多或者耗资源过多,但又经常用到的对象
3. 有状态的工具类对象
4. 频繁访问数据库或文件的对象
5. 以及其他要求只有一个对象的场景
//饿汉模式,不存在线程安全问题
class Singleton
{
protected:
Singleton(){} // 设置为保护便于被继承
private:
Singleton(const Singleton& s) = delete; // 防拷贝(注:delete是c++11中的语法),将该函数禁用
Singleton* operator=(const Singleton& s) = delete; // 防赋值
static Singleton* p;
public:
static Singleton* getInstance(){
return p;
}
};
Singleton* Singleton::p = new Singleton(); // 第一次创建类就给分配资源