设计模式
本文章主要是记录个人学习设计原则和设计模式的一些笔记,希望对其他朋友也有参考价值。
一、七大设计原则
目的:稳定的软件架构所需要遵循的原则。
学习方法:从设计模式来理解设计原则在实际上的应用。
SOLID原则:
S:单一职责原则
O:开闭原则
L:里氏替换原则
I:接口隔离原则
D:依赖倒置原则
其他:迪米特法则、复合聚合原则
1、单一职责
- 单一职责:一个类只负责一个功能。如果有2个功能,一个功能的改变会影响其他的。
主要是为了解耦。如qt 界面绘制类,只控制界面的逻辑,获取/保存数据,由另一个类实现。
1)优点:a.降低类的复杂度,一个类只负责一项职责
b.提高类的可读性,可维护性
c.降低变更引起的风险
按照功能划分颗粒度,划分太细会导致接口/类变多,可读性下降。
2、开闭原则
- 开闭原则:对扩展开放,对修改封闭(意味着类一旦设计完成,就可以独立完成其工作,而不要对已有代码进行任何修改)。
1)实现方法:核心思想就是面对抽象编程,而不是面对具体编程,因为抽象相对稳定。
让类依赖于固定的抽象,所以对修改是封闭的;而通过面向对象的继承和多态机制,可以实现新的扩展方法,所以对于扩展就是开放的。
3、 里氏代换
- 里氏代换:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
1)作用:a.克服了继承中重写父类造成的可复用性变差的缺点。
b.确保类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
2)其他解释
在一个软件系统中,子类应该能够完全替换任何父类能够出现的地方,并且经过替换后,不会让调用父类的客户程序从行为上有任何改变。
4、接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
5、依赖倒置:
1、高层模块不应该依赖低层模块,二者都应该依赖其抽象。
2、抽象不应该依赖细节,细节应该依赖抽象。
依赖倒转的中心思想是面向接口编程。
依赖倒转的设计理念为:相对于细节的多变性,抽象的东西要稳定的多。以抽象的基础搭建的架构比以细节为基础的架构要稳定的多。
抽象和细节的理解:
接口是对实现的抽象,类是对接口的抽象,对接口进行设计。----高内聚(重复逻辑代码少),低耦合(进行模块设计、分层)。
把不同的细节组合成元件,把元件组合成更大的元件,一层层封装,思路会变得更加清晰,因为细节被屏蔽了。
6、迪米特法则:
一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。迪米特法则是对软件实体之间通信的限制,它对软件实体之间通信的宽度和深度做出了要求。
7、复合/聚合原则
尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的;
1)继承的缺点:限制了系统的灵活性,使类与类之间的耦合度增加,父类的变化可能会影响到所有的子类。
2)复合/聚合的优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
二、UML类图
UML是统一建模语言,是一种辅助团队成员交流的工具。
常见类关系:
- 依赖关系:某个类以局部变量出现另一个类中
1)虚线,箭头,箭头指向被依赖者,use a - 关联关系:某个类以成员变量的形式出现另一个类中 has a
1)实线,箭头
2)分类
a. 聚合关系,空心菱形,菱形表示整体。是一种弱相互关系,整体和部分可以独立存在,不同生命周期 has a
b. 组合关系,实心菱形,菱形表示整体。强相互关系,整体和部分不可以分割,相同生命周期 contains a
聚合和组合的区别在于:聚合关系是“has-a”关系,组合关系是“contains-a”关系;聚合关系表示整体与部分的关系比较弱,而组合比较强;聚合关系中代表部分事物的对象与代表聚合事物的对象的生存期无关,一旦删除了聚合对象不一定就删除了代表部分事物的对象。组合中一旦删除了组合对象,同时也就 删除了代表部分事物的对象。
- 泛化关系is a
1)类继承关系,实线,三角形表示父类
2)接口继承关系,虚线,三角形表示接口
强弱:泛化=实现>组合>聚合>关联>依赖
- 使用技巧
1.有关系,先用依赖,后续修改
2、关联:只使用关联,分不清组合和聚合时。 聚合:添加管理职责, 上下级,从属关系。
三、时序图
看图方式:柱子表示激活状态,完成某项目任务
实线箭头表示调用,虚线箭头表示调用后返回
画图软件:visio
四、设计模式前言
设计模式优点:
- 易拓展,开闭原则
- 易维护,封装函数、类调用
- 易复用,!=复制,使用封装,可以直接去调用。
设计原则比模式更重要,模式是在特定环境下,人们为了解决某类重复出现问题,而总结归纳出来的有效解决方案。而原则可以帮助我们构建新的模式。
设计模式需要关注四点,学习其他东西时也可以参考这四点,这样才能学的全面。
名称:这个名称说明了什么
问题:解决了什么问题
实现方案:是怎么解决的
优点:有什么优点
总共23种,三大类型。
—创建型模式 与对象创建有关,涉及到对象实例化方式,共5种。
—结构型模式 关注类和对象的组合,共7种
—行为型模式 关注类或对象如何相互协作以完成某项任务或行为。侧重于对象之间的交互和通信。 共11种
一、创建型模式
有单例模式,原型模式,建造者模式,工厂模式,抽象工厂模式,共5种。
与对象创建有关,涉及到对象实例化方式。
1、单例模式
确保一个类只有一个实例被建立。
要求:
- 构造函数应该声明为非公有,从而禁止外界创建实例。静态成员初始化可以调用new,不影响,其他地方不可调用。
- 拷贝操作和移动操作也应该禁止。
- 通过getInstance()获取。static *p; static *p getInstance(); 静态函数可以访问私有的构造函数。
懒汉模式:用的时候初始化. 双重判断+ 锁,线程安全
饿汉模式:一开始就创建,静态变量初始化的时候赋值。 线程安全
2、原型模式
从A的实例得到一份与A内容相同。本质是拷贝构造函数,实际上是实现了一个clone接口。
- 用new新建对象不能获取当前对象运行时的状态。
- 需要用基类指针获取到派生类对象。
类图:
作用:
- 减少创建对象的开销,简化创建过程。当new一个对象,构造参数比较多时或者类初始化需要消化非常多的资源,这个资源包括数据、硬件资源。就可以使用原型模式来创建一个新的对象,不必去理会创建的过程。
3、建造者模式
建造者模式,是将一个复杂的对象的构建(build)与它的表示(product)分离,使得同样的构建过程(director)可以创建不同的表示(产品)。
一个复杂对象的创建,由固定的步骤组成。
build负责创建各个组件,director负责把各个组件的组装步骤。Man是生成出来的人,具有各个属性。
类图:
4、工厂模式
1、定义:在创建对象时提供了一种封装机制,将实际创建对象的代码与使用代码分离。
简单工厂模式:一个工厂,生产多个产品。
创建一个类,实现getInstance(int i),通过传入参数确定生成的产品。
不足:新增子类时,需要修改源代码。
工厂方法模式: 一个工厂生产一个产品
抽象工厂(Abstract Factory):定义了创建产品对象的接口。
具体工厂(Concrete Factory):实现了抽象工厂接口,负责创建具体的产品对象。
抽象产品(Abstract Product):定义了产品的基本属性和行为。
具体产品(Concrete Product):实现了抽象产品接口,是具体的产品对象。
优点:拓展性:当系统需要新增产品时,可以方便地添加新的工厂方法或子类,而不需要修改现有代码。
5、抽象工厂模式
1、使用场景,新增产品类型(不继承product)时,使用该模式可以减少工厂数量。
一个工厂,生产一系列产品。
二、结构型模式
共7种,关注类和对象的组合。
对对象添加职责:适配器、代理、桥接模式
操作的抽象:组合、外观
加减:装饰、享元
1、适配器模式
转换器:将一个类的接口转换成客户希望的另外一个接口这样使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
比如你在网上买了一个手机,但是买家给你发回来了一个3接头的充电器,但是恰好你又没有3接头的插槽,只有2个接口的插槽,于是你很直然地便会想到去找你个3接口转两接口的转换器。简单的分析下这个转换器便是我们这里的适配器Adapter。三相插头便是我们要适配的Adaptee。
分为类适配器和对象适配器。
类适配器:使用继承
对象适配器:使用组合
对象适配器相比类适配器来说更加灵活,他可以选择性适配自己想适配的对象。(复合/聚合原则)。
体现的设计原则:依赖倒置原则:Adapter需要继承Target,而不是直接使用Adapter。面向抽象编程。
2、代理模式
给对象增加额外功能。在调用这个方法之前做前置处理,调用这个方法之后做后置处理。
体现的设计原则:依赖倒置,面对抽象编程。继承于subject,而不是直接使用代理类。
类图
分类
- 远程代理:为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。
- 虚代理:延迟加载,先加载轻量级的代理对象,真正需要再加载真实对象。在需要的时候才初始化主题对象。
- 保护代理,用来控制真实对象的访问权限,一般用于对象有不同的访问权限的时候。也被称为动态代理。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。如智能指针shared_ptr和unique_ptr。
3、桥接模式
1、定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。如下面的例子是shape是抽象,颜色是实现。
2、体现的设计原则:复合聚合原则。
3、适用场景:当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
4、组合模式
对操作的抽象。组合模式使得用户对单个对象和组合对象的使用具有一致性,可统一调用接口。
使用场景:需求中体现部分于整体层次的结构时,用户可以忽略组合对象与单个对象的不同,统一使用相同接口访问。
使用举例:
目录和文件,目录和文件都可以删除或者打开。打开目录其实就是打开目录的全部子目录和该目录下的文件。删除文件就是删除一个文件,删除目录是删除全部子目录和全部文件。
5、外观模式
1、定义:外观模式又称为门面模式(Facade Pattern),提供一个统一接口 , 用于访问子系统中的一群接口。
与多个子系统有交互。
2、体现的设计原则:迪米特法则。降低子系统与调用者的耦合度,调用方便。
3、类图
6、装饰模式
1、定义:动态地给一个对象添加一些额外的职责。
2、体现的设计原则:开闭原则和依赖倒置原则。
原因:
有时我们希望给某个对象而不是整个类添加一些功能。比如有一个手机,为手机添加特性,比如增加挂件、屏幕贴膜等。
使用继承机制不够灵活,用户不能控制对组件添加功能的方式和时机。
一种较为灵活的方式是将组件嵌入另一个对象(装饰类)中,由这个对象添加功能,我们称这个嵌入的对象为装饰。
类图:
7、享元模式
1、定义:主要用于减少在应用程序中使用的对象的数量。
在享元模式中,有两种主要的对象:
- 享元对象(Flyweight):这是被共享的对象,通常包含内部状态(不会随外部环境改变而改变的状态)和可能包含一些外部状态(随环境改变而改变的状态,这些状态通常由客户端保存和管理)。
- 享元工厂(Flyweight Factory):负责创建和管理享元对象。它确保如果请求一个已经存在的享元对象,就返回该对象,而不是创建一个新的对象。
在享元模式中可以共享的相同内容称为内部状态,而那些需要外部环境来设置的不能共享的内容称为外部状态。
通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的(对象不再重复创建)。
也就是说,享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。
示例代码:
#include <iostream>
#include <unordered_map>
#include <string>
// 享元接口
class Flyweight {
public:
virtual ~Flyweight() {}
virtual void operation(std::string extrinsicState) = 0;
};
// 具体享元类
class ConcreteFlyweight : public Flyweight {
private:
std::string intrinsicState;
public:
ConcreteFlyweight(std::string state) : intrinsicState(state) {}
void operation(std::string extrinsicState) override {
std::cout << "Intrinsic State: " << intrinsicState
<< ", Extrinsic State: " << extrinsicState << std::endl;
}
};
// 享元工厂
class FlyweightFactory {
private:
std::unordered_map<std::string, Flyweight*> flyweights;
public:
Flyweight* getFlyweight(std::string key) {
if (flyweights.find(key) == flyweights.end()) {
flyweights[key] = new ConcreteFlyweight(key);
}
return flyweights[key];
}
};
// 客户端使用
int main() {
FlyweightFactory factory;
Flyweight* f1 = factory.getFlyweight("Red");
Flyweight* f2 = factory.getFlyweight("Blue");
Flyweight* f3 = factory.getFlyweight("Red");
f1->operation("X");
f2->operation("Y");
f3->operation("Z");
// 注意 f1 和 f3 指向同一个对象
return 0;
}
三、行为型模式
类之间互相交互和分配职责。
1、封装变化:职责链、状态、策略、模板方法、
2、管理:命令、备忘录、观察者、中介者
3、访问:访问者、迭代器
4、其他:解释器。
1、职责链模式
1、定义:把请求发送者和接收者解耦,请求按照链子处理。
2、体现的设计原则:开闭原则。
事例:考虑员工要求加薪。公司的管理者一共有三级,总经理、总监、经理,如果一个员工要求加薪,应该向主管的经理申请,如果加薪的数量在经理的职权内,那么经理可以直接批准,否则将申请上交给总监。总监的处理方式也一样,总经理可以处理所有请求。
类图
2、状态模式
1、定义:一个对象有多种状态,在每一个状态下,都会有不同的行为。
2、体现的设计原则:开闭原则。分离出容易变化的。
将对象的状态封装成一个独立的类,并将动作行为委托到代表当前状态的对象,行为会随着内部的状态而改变。
优点:消除了分支语句,就像工厂模式消除了简单工厂模式的分支语句一样,将状态处理分散到各个状态子类中去,每个子类集中处理一种状态,这样就使得状态的处理和转换清晰明确。
类图
3、策略模式
1、定义:实现和状态模式类似,区别在于是对算法的封装。
类图
传递方法1:指针
//Cache需要用到替换算法
class Cache
{
private:
ReplaceAlgorithm *m_ra;
public:
Cache(ReplaceAlgorithm *ra) { m_ra = ra; }
~Cache() { delete m_ra; }
void Replace() { m_ra->Replace(); }
};
缺点:暴露了太多的细节。
传递方法2:结合简单工厂
//Cache需要用到替换算法
enum RA {LRU, FIFO, RANDOM}; //标签
class Cache
{
private:
ReplaceAlgorithm *m_ra;
public:
Cache(enum RA ra)
{
if(ra == LRU)
m_ra = new LRU_ReplaceAlgorithm();
else if(ra == FIFO)
m_ra = new FIFO_ReplaceAlgorithm();
else if(ra == RANDOM)
m_ra = new Random_ReplaceAlgorithm();
else
m_ra = NULL;
}
~Cache() { delete m_ra; }
void Replace() { m_ra->Replace(); }
};
只要知道算法的相应标签即可,而不需要知道算法的具体定义。
传递方法3:利用模板实现
//Cache需要用到替换算法
template <class RA>
class Cache
{
private:
RA m_ra;
public:
Cache() { }
~Cache() { }
void Replace() { m_ra.Replace(); }
};
原文链接:https://blog.csdn.net/wuzhekai1985/article/details/6665197
4、模板方法
1、定义:定义操作中的算法骨架,将一些步骤延迟到子类中去实现。
2、体现的设计原则:开闭原则。把变化的部分抽象出来。
3、实现:由基类来实现基本固定的逻辑,而把不同的部分封装在子类里,实现代码的复用。
类图
模板模式和策略模式区别
策略模式:定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。关键点在于每个算法都是过程完整且独立的。
模板方法模式:模板则是将骨架定义好,例如执行的步骤或先后顺序。骨架中的部分在父类中进行实现,而子类的个性化行为则由子类继承再加以实现。
区别的本质就是策略模式是替换了整个流程。而模板模式替换的是固定流程中的一些特定的内容。
5、命令模式
1、定义:请求和执行者解耦,将一个请求封装为一个对象,便于对命令进行管理。
场景:
客户端有多个请求协议的情况下,可以将协议当做命令。对请求排队或者记录请求日志,以及支持可撤销的操作。
实现:
命令模式的关键角色
- 命令(Command):这是一个抽象类或接口,它声明了执行操作的接口。具体的命令类会实现这个接口,并绑定到一个接收者对象上。当命令对象被调用时,它会调用接收者的相应操作。
- 具体命令(ConcreteCommand):这是命令接口的具体实现类,它持有一个接收者对象,并实现了命令接口中的执行方法。当执行方法被调用时,它会调用接收者的相应方法来完成请求的操作。
- 请求者(Invoker):请求者负责调用命令对象的执行方法。它不需要知道具体的命令对象类型,只需要知道它是一个命令对象即可。请求者类可以存储多个命令对象,并按照需要调用它们的执行方法。
- 接收者(Receiver):接收者对象知道如何执行请求的操作。它具体实现了请求的功能。
示例代码:
#include <iostream>
#include <vector>
#include <memory>
// 接收者接口
class Receiver {
public:
virtual ~Receiver() {}
virtual void action() = 0;
};
// 具体接收者
class ConcreteReceiver1 : public Receiver {
public:
void action() override {
std::cout << "Executing ConcreteReceiver1 action" << std::endl;
}
};
class ConcreteReceiver2 : public Receiver {
public:
void action() override {
std::cout << "Executing ConcreteReceiver2 action" << std::endl;
}
};
// 命令接口
class Command {
protected:
Receiver* receiver;
public:
Command(Receiver* receiver) : receiver(receiver) {}
virtual ~Command() {}
virtual void execute() = 0;
};
// 具体命令
class ConcreteCommand1 : public Command {
public:
ConcreteCommand1(Receiver* receiver) : Command(receiver) {}
void execute() override {
receiver->action();
}
};
class ConcreteCommand2 : public Command {
public:
ConcreteCommand2(Receiver* receiver) : Command(receiver) {}
void execute() override {
receiver->action();
}
};
// 请求者
class Invoker {
private:
std::vector<std::unique_ptr<Command>> commands;
public:
void addCommand(std::unique_ptr<Command> command) {
commands.push_back(std::move(command));
}
void executeAllCommands() {
for (const auto& command : commands) {
command->execute();
}
}
};
// 客户端代码
int main() {
// 创建接收者
std::unique_ptr<Receiver> receiver1 = std::make_unique<ConcreteReceiver1>();
std::unique_ptr<Receiver> receiver2 = std::make_unique<ConcreteReceiver2>();
// 创建命令并绑定接收者
std::unique_ptr<Command> command1 = std::make_unique<ConcreteCommand1>(receiver1.get());
std::unique_ptr<Command> command2 = std::make_unique<ConcreteCommand2>(receiver2.get());
// 创建请求者并添加命令
Invoker invoker;
invoker.addCommand(std::move(command1));
invoker.addCommand(std::move(command2));
// 执行所有命令
invoker.executeAllCommands();
return 0;
}
命令模式优点:
1.设计命令队列
2.设置命令日志
3.接受者可以根据请求判断是否执行
4.可以实现撤销和重做
6、备忘录模式
1、定义:快照模式。在不破坏封装的情况下,捕获一个对象的内部状态,并在该对象之外保存这个状态。便于回退。
类图
参考链接:https://www.cnblogs.com/jing99/p/12617294.html
7、观察者模式
1、定义:多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式。
类图:
8、中介者模式
1、定义:关系简化,将多对多的交互简化为一对多的交互。
类图
参考链接:
https://zhuanlan.zhihu.com/p/447208807
9、访问者模式
1、定义:允许在不修改对象结构的情况下向一组对象添加新的操作。
2、使用场景:把处理从数据结构中分离出来,适用于数据结构相对稳定的情况。
3、实现
每个具体元素类都需要实现 accept 方法,这可能会导致代码的复杂性增加。
示例代码
#include <iostream>
#include <vector>
// 访问者接口
class Visitor {
public:
virtual ~Visitor() {}
virtual void visit(Element* element) = 0;
};
// 具体访问者
class ConcreteVisitor : public Visitor {
public:
void visit(Element* element) override {
if (dynamic_cast<ConcreteElementA*>(element)) {
std::cout << "Visiting ConcreteElementA" << std::endl;
} else if (dynamic_cast<ConcreteElementB*>(element)) {
std::cout << "Visiting ConcreteElementB" << std::endl;
}
}
};
// 元素接口
class Element {
public:
virtual ~Element() {}
virtual void accept(Visitor* visitor) = 0;
};
// 具体元素A
class ConcreteElementA : public Element {
public:
void accept(Visitor* visitor) override {
visitor->visit(this);
}
};
// 具体元素B
class ConcreteElementB : public Element {
public:
void accept(Visitor* visitor) override {
visitor->visit(this);
}
};
// 对象结构
class ObjectStructure {
private:
std::vector<Element*> elements;
public:
void addElement(Element* element) {
elements.push_back(element);
}
void accept(Visitor* visitor) {
for (Element* element : elements) {
element->accept(visitor);
}
}
};
// 客户端
int main() {
ObjectStructure os;
os.addElement(new ConcreteElementA());
os.addElement(new ConcreteElementB());
Visitor* v = new ConcreteVisitor();
os.accept(v);
delete v;
// 清理动态分配的内存(这里省略了,实际应用中需要适当管理内存)
return 0;
}
10、迭代器模式
1、定义:提供一种方法顺序访问一个聚合对象中各个元素,而又无需暴露该对象的内部表示。
类图
11、解释器模式
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
使用场景:
通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
原文链接:https://blog.csdn.net/xiqingnian/article/details/42222369
五、工作使用心得
一、高内聚,低耦合
接口复用:是复用代码的内聚
接口放在哪个类中:是类功能的内聚
如:复杂界面的更新,需要很多参数,根据不同的参数执行不同的刷新逻辑,此时可以把参数提取到model类,界面类只负责根据model类的刷新逻辑,model类负责数据的管理。
低耦合:不同模块依赖关系少,这样改动一个模块对另一个模块影响较少,提高系统的灵活性和可拓展性。
如何实现:
1.模块化,模块只暴露必要的接口,封装细节。
2.模块间通过接口通信
3.设计原则和设计模式
实际使用心得:
现有代码的实现逻辑不一定是最佳的,需要评估后再决定是否直接按照该思路添加新逻辑。
二、如何提高函数命名能力(提高代码可读性)
1、保持一致性:项目中一致性>个人风格一致性
2、使用具体描述功能的词汇
3、使用前缀或后缀区分类似功能、从颗粒度上区分