设计模式
1、设计模式简介
- 什么是设计模式?
“每一个模式描述了一个在我们周围不断重复发生的问题, 以及该问题的解决方案的核心。这样,你就能一次又一次 地使用该方案而不必做重复劳动”。
- 深入理解面向对象
向下:深入理解三大面向对象机制
• 封装,隐藏内部实现
• 继承,复用现有代码
• 多态,改写对象行为
向上:深刻把握面向对象机制所带来的抽象意义,理解如何使用 这些机制来表达现实世界,掌握什么是“好的面向对象设计”
- 如何解决复杂性?
更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。
由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节, 而去处理泛化和理想化了的对象模型。
软件设计的金科玉律:复用!
2、面向对象设计原则
-
依赖倒置原则(DIP Dependence Inversion Principle) (编译时依赖)
高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
-
开放封闭原则(OCP Open Close Principle)
对扩展开放,对更改封闭。
尽可能不要动源代码
类模块应该是可扩展的,但是不可修改。
-
单一职责原则(SRP Single Responsibility Principle)
一个类应该仅有一个引起它变化的原因。
变化的方向隐含着类的责任。
-
Liskov 替换原则(LSP Liskov Substitution Principle)
子类必须能够替换它们的基类(IS-A)。
继承表达类型抽象。
-
接口隔离原则(ISP Interface Segregation Principle)
不应该强迫客户程序依赖它们不用的方法。
接口应该小而完备。
-
优先使用对象组合,而不是类继承 (CRP Composite Reuse Principle)
类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
继承在某种程度上破坏了封装性,子类父类耦合度高。 • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
-
封装变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层的 一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
-
针对接口编程,而不是针对实现编程
不将变量类型声明为某个特定的具体类,而是声明为某个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
减少系统中各部分的依赖关系,从而实现“高内聚、松耦合” 的类型设计方案。
接口标准化很重要!
设计模式设计原则
- 单⼀职责原则:就⼀个类⽽⾔,应该仅有⼀个引起它变化的原因。
- 开放封闭原则:软件实体可以扩展,但是不可修改。即⾯对需求,对程序的改动可以通过增加代码来完成,但是不 能改动现有的代码。
- 里氏代换原则:⼀个软件实体如果使⽤的是⼀个基类,那么⼀定适⽤于其派⽣类。即在软件中,把基类替换成派⽣ 类,程序的⾏为没有变化。
- 依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接⼝编程,不要对实现编程。
- 迪⽶特原则:如果两个类不直接通信,那么这两个类就不应当发⽣直接的相互作⽤。如果⼀个类需要调⽤另⼀个类 的某个⽅法的话,可以通过第三个类转发这个调⽤。
- 接⼝隔离原则:每个接⼝中不存在派⽣类⽤不到却必须实现的⽅法,如果不然,就要将接⼝拆分,使⽤多个隔离的 接⼝。
三、Template Method模式
-
设计模式分类:
创建型(Creational)模式:将对象的部分创建工作延迟到子 类或者其他对象,从而应对需求变化为对象创建时具体类型实 现引来的冲击。
结构型(Structural)模式:通过类继承或者对象组合获得更灵 活的结构,从而应对需求变化为对象的结构带来的冲击。
行为型(Behavioral)模式:通过类继承或者对象组合来划分 类与对象间的职责,从而应对需求变化为多个交互的对象带来 的冲击。
-
重构获得模式 Refactoring to Patterns
面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指是那些可以
满足 “应对变化,提高复用”
的设计 。现代软件设计的特征是“需求的频繁变化”。设计模式的要点是 “寻找变化点,然后在
变化点处应用设计模式,
从而来更好地应对 需求的变化”.“什么时候、什么地点应用设计模式”比“理解设 计模式结构本身”更为重要。设计模式的应用不宜先入为主,
一上来就使用设计模式是对设计 模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提 倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设 计模式的方法。
-
重构关键方法
静态 -> 动态
早绑定 -> 晚绑定
继承 -> 组合
编译时依赖 -> 运行时依赖
紧耦合 -> 松耦合
-
“组件协作”模式:
现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过
晚期绑定
(一个早的东西调用一个晚的东西),来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
典型模式
•
Template Method
•
Observer / Event
•
Strategy
- 动机
在软件构建过程中,对于某一项任务,它常常有
稳定
的整体操作结构,但各个子步骤却有很多
改变
的需求,或者由于固有的原因 (比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求。
-
模式定义
定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟 (变化)到子类中。Template Method使得子类可以不改变 (复用)一个算法的结构即可重定义(override 重写)该算法的 某些特定步骤。
注意:
-
稳定(相对)的骨架 是这个设计模式的前提,必须要有稳定点。
-
设计模式是在稳定点和变化点之间隔离开,找到平衡点
-
稳定架构中有变化 稳定需要写成non-virtual函数 变化的需要写成virtual函数
-
延迟或者变化操作需要基类添加虚函数让子类去实现 或者override。
总结:
-
Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性) 为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
-
除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用 你”的
反向控制结构是Template Method的典型应用。
-
在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法。
代码对比
//程序库开发人员
class Library{
public:
void Step1(){
//...
}
void Step3(){
//...
}
void Step5(){
//...
}
};
//应用程序开发人员
class Application{
public:
bool Step2(){
//...
}
void Step4(){
//...
}
};
int main()
{
Library lib();
Application app();
lib.Step1();
if (app.Step2()){
lib.Step3();
}
for (int i = 0; i < 4; i++){
app.Step4();
}
lib.Step5();
}
使用Template Method模式后
//程序库开发人员
class Library{
public:
//稳定 template method
void Run(){
Step1();
if (Step2()) { //支持变化 ==> 虚函数的多态调用
Step3();
}
for (int i = 0; i < 4; i++){
Step4(); //支持变化 ==> 虚函数的多态调用
}
Step5();
}
virtual ~Library(){ }
protected:
void Step1() { //稳定
//.....
}
void Step3() {//稳定
//.....
}
void Step5() { //稳定
//.....
}
virtual bool Step2() = 0;//变化
virtual void Step4() =0; //变化
};
//应用程序开发人员 继承
class Application : public Library {
protected:
virtual bool Step2(){
//... 子类重写实现
}
virtual void Step4() {
//... 子类重写实现
}
};
int main()
{
// 多态
Library* pLib=new Application();
lib->Run();
delete pLib;
}
}
四、Strategy策略模式
- 动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂; 而且有时候支持不使用的算法也是一个性能负担。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本 身解耦,从而避免上述问题?
-
模式定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程 序(稳定)而变化(扩展,子类化)。
总结:
-
Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在
运行时
方便地根据需要在各个算法之间进行切换。 -
Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需 要Strategy模式。
-
注意:
代码中不推荐使用
if else switch case
if else
绝对不变情况 可以不使用策略模式 例如: 一周七天
实际业务变化不太可能一成不变的 大多数情况遇到
if...else...
可使用策略模式
-
如果Strategy对象没有实例变量,那么各个上下文可以共享同一个 Strategy对象,从而节省对象开销。
代码对比:
// 枚举
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
FR_Tax //更改
};
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
//...
if (tax == CN_Tax){
//CN***********
}
else if (tax == US_Tax){
//US***********
}
else if (tax == DE_Tax){
//DE***********
}
else if (tax == FR_Tax){ //更改
//...
}
//....
}
};
使用策略模式后:
class TaxStrategy{
public:
virtual double Calculate(const Context& context)=0;
// 作为父类一定要写虚析构函数
virtual ~TaxStrategy(){}
};
class CNTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class USTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class DETax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
//扩展
//*********************************
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
// 这里的代码不用改动
class SalesOrder{
private:
//抽象类不能创建对象,创建指针才有多态性,创建对象没有多态性。
TaxStrategy* strategy;
public:
SalesOrder(StrategyFactory* strategyFactory){
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
public double CalculateTax(){
//...
Context context();
double val =
strategy->Calculate(context); //多态调用
//...
}
};
五、Observer(Event) 观察者模式
-
动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密, 将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
-
模式定义
定义对象间的一种
一对多
(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都 得到通知并自动更新。 -
要点总结
使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
观察者自己决定是否需要订阅通知,目标对象对此一无所知。
Observer模式是基于事件的UI框架中非常常用的设计模式,也是 MVC模式的一个重要组成部分。
注意:
接口就是抽象基类
不推荐使用多继承 可以同时继承基类和接口
代码对比
class FileSplitter
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
}
};
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, progressBar);
splitter.split();
}
};
使用观察者模式后:
// 接口 抽象基类
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};
class FileSplitter
{
string m_filePath;
int m_fileNumber;
List<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者
public:
// 这里没有发生改变 不管添加多少观察者 构造函数不必发生变化
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
onProgress(progressValue);//发送通知
}
}
void addIProgress(IProgress* iprogress){
m_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress){
m_iprogressList.remove(iprogress);
}
protected:
// 虚函数 可以允许子类override
virtual void onProgress(float value){
List<IProgress*>::iterator itor=m_iprogressList.begin();
while (itor != m_iprogressList.end() )
(*itor)->DoProgress(value); //更新进度条
itor++;
}
}
};
// 继承父类和接口
class MainForm : public Form, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath, number);
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};
六、单例模式
单例模式:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。
有两种模式懒汉和饿汉:
饿汉:饿了就饥不择⻝了,所以在单例类定义的时候就进⾏实例化。
懒汉:顾名思义,不到万不得已就不会去实例化类,也就是在第⼀次⽤到的类实例的时候才会去实例化。
饿汉模式(线程安全)
在最开始的时候静态对象就已经创建完成,设计⽅法是类中包含⼀个静态成员指针,该指针指向该类的⼀个对象, 提供⼀个公有的静态成员⽅法,返回该对象指针,为了使得对象唯⼀,构造函数设为私有。
#include <iostream>
#include <algorithm>
using namespace std;
class SingleInstance {
public:
static SingleInstance* GetInstance() {
static SingleInstance ins;
return &ins;
}
~SingleInstance(){};
private:
//涉及到创建对象的函数都设置为private
SingleInstance() { std::cout<<"SingleInstance() 饿汉"<<std::endl; }
SingleInstance(const SingleInstance& other) {};
SingleInstance& operator=(const SingleInstance& other) {return *this;}
};
int main(){
//因为不能创建对象所以通过静态成员函数的⽅法返回静态成员变量
SingleInstance* ins = SingleInstance::GetInstance();
return 0;
}
//输出 SingleInstance() 饿汉
懒汉模式(线程安全需要加锁)
尽可能的晚的创建这个对象的实例,即在单例类第⼀次被引⽤的时候就将自己初始化,C++ 很多地⽅都有类型的思想,⽐如写时拷⻉,晚绑定等
#include <pthread.h>
#include <iostream>
#include <algorithm>
using namespace std;
class SingleInstance {
public:
static SingleInstance* GetInstance() {
if (ins == nullptr) {
pthread_mutex_lock(&mutex);
if (ins == nullptr) {
ins = new SingleInstance();
}
pthread_mutex_unlock(&mutex);
}
return ins;
}
~SingleInstance(){};
//互斥锁
static pthread_mutex_t mutex;
private:
//涉及到创建对象的函数都设置为private
SingleInstance() { std::cout<<"SingleInstance() 懒汉"<<std::endl; }
SingleInstance(const SingleInstance& other) {};
SingleInstance& operator=(const SingleInstance& other) { return *this; }
//静态成员
static SingleInstance* ins;
};
//懒汉式 静态变量需要定义
SingleInstance* SingleInstance::ins = nullptr;
pthread_mutex_t SingleInstance::mutex;
int main(){
//因为不能创建对象所以通过静态成员函数的⽅法返回静态成员变量
SingleInstance* ins = SingleInstance::GetInstance();
delete ins;
return 0;
}
//输出 SingleInstance() 懒汉
单例模式的适⽤场景
:
(1)系统只需要⼀个实例对象,或者考虑到资源消耗的太⼤⽽只允许创建⼀个对象。
(2)客户调⽤类的单个实例只允许使⽤⼀个公共访问点,除了该访问点之外不允许通过其它⽅式访问该实例(就 是共有的静态⽅法)。