1. 设计模式简介
设计模式(Design Pattern)是软件开发过程中解决的一般问题的方法经验总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使编码真正工程化。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。
1.1 设计模式类型
设计模式大约有二十多种,主要可分为三大类:创建型设计模式、结构型设计模式、行为型设计模式。
下面将简要介绍各设计模式。
1.2 面向对象设计六大原则
- 单一职责原则(The Single Responsibility Principle:SRP)
- 开放封闭原则(The Open Closed Princeple:OCP)
- 里氏替换原则(The Liskov Substitution Principle:LSP)
- 迪米特法则(The Law of Demeter:LD)
- 接口隔离原则(The Interface Segregation Principle:ISP)
- 依赖倒置原则(The Dependency Inversion Principle:DIP)
也称为SOLID原则。
开闭原则要求我们写好的类不要去修改,如果需要增加功能,请扩展它。单一职责原则要求我们的一个类只做一件事情。里氏替换原则要求子类必须兼容父类。迪米特法则要求我们尽可能少的依赖其他的类。接口隔离原则要求我们定义接口的时候尽可能简单一些。依赖倒置原则要求我们不能依赖实现类,而应该依赖接口编程。
1.2.1 单一职责原则
一个类只做一件事,不要做与这个类主要职责无关的事。
1.2.2 开放封闭原则
对扩展开放,对修改封闭。类或接口定义好后,若无bug,若不是为了改进内部实现,则不应修改这个类。开闭原则是为了保持类或接口后续版本的兼容性。
1.2.3 里氏替换原则
所有父类出现的地方,都可以用子类替换。
1.2.4 迪米特法则
迪米特法则又叫最少知识原则,一个类应该对他所依赖的类知道的越少越好。高内聚,低耦合。
1.2.5 接口隔离原则
使用多个小的接口比使用一个臃肿的接口要更好。细的接口也有利于遵守最少知识原则。
1.2.6 依赖倒置原则
不要依赖具体实现,而应该依赖抽象。应该面向接口编程,而不是面向实现类编程。
2. 创建型
创建型设计模式提供了一种灵活的创建对象方式,同时隐藏了创建的逻辑。创建型设计模式包括5种:
- 简单工厂模式:创建某一类对象
- 工厂方法模式:创建工厂,工厂创建产品
- 抽象工厂模式:创建多个种类的对象,创建系列产品
- 单例模式:维护全局唯一对象
- 建造者模式:拼接各个元素组装成一个对象
- 原型模式:从现有原型对象拷贝产生对象
2.1 简单工厂模式
工厂模式中工厂类提供一个统一的创建的对象方法。根据输入参数创建不同子类型对象。
在开发中经常需要将日志输出,日志可以输出到控制台、文本文件、数据库、消息队列或者通过接口输出到其他系统。采用工厂模式设计一个日志输出工厂。
// 日志抽象类
public abstract class AbstractLogger
{
public abstract void write(string log);
}
// 控制台日志记录
class ConsoleLogger : AbstractLogger
{
public override void write(string log)
{
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {log}");
}
}
// 文件日志记录
class TXTLogger : AbstractLogger
{
public override void write(string log)
{
File.WriteAllText("log.txt", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {log}");
}
}
// 日志工厂
public class LoggerFactory
{
public static AbstractLogger getLogger(string name)
{
if (name.Equals("console"))
{
return new ConsoleLogger();
}
else
if (name.Equals("txt"))
{
return new TXTLogger();
}
return null;
}
}
// 测试
static void Main(string[] args)
{
var logger= LoggerFactory.getLogger("console");
logger.write("this is Factory Pattern test");
logger = LoggerFactory.getLogger("txt");
logger.write("this is Factory Pattern test");
}
2.2 工厂方法模式
在简单工厂的基础上,抽象工厂,每个工厂可生成一种产品。通过创建不同的工厂对象生产产品。
2.3 抽象工厂模式
抽象工厂通过一个超级工厂创建普通工厂,由普通工厂创建系列产品。
在工厂模式中实现了一个日志输出工厂,但是这个日志输出工厂功能太单一,我们需要更丰富的功能。现在来通过抽象工厂模式改造日志输出工厂,我们需要能够输出调试信息、提示信息、错误信息,而且需要能够输出到控制台、文本文件、数据库、消息队列或者其他系统。
2.4 单例模式
单例模式可能是最最常用的模式,在项目中经常用到。
单例模式用于创建程序中只允许存在一个实例的类的对象。单例模式确保可以在程序的任意地方都访问同一个对象。
在多线程中需考虑单例类对象创建时的竞态条件。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件。
2.5 建造者模式*
提供创建一组对象的统一实现。
例如,一个手机套餐包括:流量、通话时间、短信。
2.6 原型模式*
原型模式通过复制的方式来创建对象,在创建大量重复对象时非常快速。要注意浅拷贝和深拷贝。
3. 结构型
结构型关注类和对象的组合。
- 适配器模式
- 桥接模式
- 过滤器模式
- 组合模式
- 装饰器模式
- 外观模式
- 享元模式
- 代理模式
3.1 适配器模式
适配器是作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能。将一个接口转换成客户希望的另外一个接口。
在工作中,我们公司的产品有标准的sdk,通常我们都是给客户提供标准sdk;有一次碰到一个客户,他们之前已经对接了其他厂家的sdk,他们比我们牛x,所以只能修改我们的sdk去适配他们的接口。通过在我们的sdk上在增加一层适配转换成客户需要的接口。
3.2 桥接模式*
在设计类的继承体系时,如果遇到了类型变化的方向沿两个或两个以上的维度变化的情况,如果采用继承来解决问题,就会导致一个大问题——类型数量会飞速膨胀。这个时候应采用组合的方法来解决问题,也就是组合优于继承。桥接模式就是贯彻这一理念诞生的又一个设计模式。
3.3 过滤器模式
最常见的过滤器模式,就是mybatis自动生成的条件查询对象,通过过滤器模式生成sql语句,返回满足条件的数据。
3.4 组合模式*
很多时候,对象与对象之间会产生一个树形结构。组合模式通过给这棵树的每个节点都抽象出一个相似的接口,屏蔽整体与部分的差别,极大的方便了对这棵树的操作。不管是操作一个节点,还是操作一棵树或一棵子树,操作都是统一的。
3.5 装饰器模式
允许向一个现有的对象添加新的功能,同时又不改变其结构。包装现有类,在保持类方法签名完整性的前提下,提供了额外的功能。
创建一个新类,包装现有类,且不改变现有类的方法签名。
c++ 中的shared_ptr有点这种意思。
3.6 外观模式
隐藏系统复杂性,向外部提供一个可以访问的接口。sdk写了很多接口,但是有些并不是客户关心和需要的,可以通过外观模式,把需要的接口暴露出来。
摄像头信息采集sdk中,涉及调焦、灯光控制、曝光控制等,一般的客户都不了解,虽然我们提供了接口,但是客户不需要,也不想客户误操作,只好通过外观模式把这些接口都隐藏起来。
3.7 享元模式
用于减少创建对象的数量,减少内存占用和提高性能。例如一幅图像,每个像素点都是一个对象,可能有百分之八十的像素点都是白色,则只需要创建一个白色像素对象,其他都是引用。这样就减少了内存占用。
享元:共享元素
原型模式是通过内存拷贝创建对象,可提高创建相同对象的速度。
3.8 代理模式
一个类代表另一个类的功能。
提供一种代理以控制这个对象的访问。
例如在C中,申请空间都是使用malloc,释放内存空间都是使用free,现在我需要一个统计程序内存使用量的功能。我们要如何做?创建两个新的new_malloc、new_free函数代理malloc、free函数,并在函数内添加内存使用量的计数功能。为了使new_malloc创建的内存不被free释放,并且记录每个内存申请的大小,需适当修改申请的内存的存储布局,即new_malloc返回的不是申请的内存空间的头地址,而是在申请时多申请4byte,前4个byte存放内存区大小,返回第5个byte的地址。(参考redis的内存分配)
代理模式和装饰器模式有相似之处,从外观上很难区分,之间的边界也比较模糊,但是我们可以从概念来区分:
- 装饰器模式用于增强自身功能,能力更强了
- 代理模式是让代理类做一些与被代理类本身业务无关的职责,代理模式是为了实现对对象的控制,不对本身功能进行增强
4. 行为型
行为型关注对象之间的通信(调用)。
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 空对象模式
- 策略模式
- 模板模式
- 访问者模式
4.1 责任链模式
为请求创建了一个接收者对象的链。例如tcp/ip中数据封包的生成和解包的过程:
- 封包生成:原始数据 => (传输层)添加发送端口信息等信息头 => (网络层)添加发送ip等信息头 => (数据链路层)添加mac地址等信息头 => 封包
- 解析封包:封包生成的逆过程
对某个对象进行类似“审批流程”的处理操作。职责链模式通过将一系列的“处理器对象”连接成一个链来处理消息,保持消息处理过程对消息发送者透明,实现发送者与处理者解耦。
4.2 命令模式
命令模式是一种数据驱动的设计模式。请求以命令的形式包裹在对象中,并传递给调用对象。调用对象寻找可以处理该命令的合适对象,并把该命令传递给相应的对象执行该命令。
// 老板写了一封信给财务,财务收到后执行老板的命令。
#include <vector>
#include <iostream>
using namespace std;
class command
{
public:
virtual void execute() = 0;
};
class fagongzi : public command
{
public:
void execute()
{
cout << "老板发工资啦" << endl;
}
};
class daobi : public command
{
public:
void execute()
{
cout << "公司倒闭,老板跑路了" << endl;
}
};
class caiwu
{
private:
vector<command *> _cmds;
public:
void recvCmd(command *cmd)
{
_cmds.push_back(cmd);
}
void execCmd()
{
for (auto cmd : _cmds)
{
cmd->execute();
}
_cmds.clear();
}
};
int main()
{
command *cmd1 = new daobi;
caiwu _caiwu;
_caiwu.recvCmd(cmd1);
_caiwu.execCmd();
cout << "老板抓回来了" << endl;
command *cmd2 = new fagongzi;
_caiwu.recvCmd(cmd2);
_caiwu.execCmd();
}
4.3 解释器模式
对给定的语言的文法创建一种表示(建立模型)并定义对应的解释器,用这个解释器去解释语句。解释器模式常用于实现表达式语言或文本处理。
例如:计算器输入计算表达式,计算结果;sql语句。
4.4 迭代器模式
迭代器模式用于顺序访问集合对象的元素,不需要指定集合对象的底层表示。
4.5 中介者模式
中介者模式是迎来降低多个对象与类之间的通信复杂性。这种模式提供了一个中介者类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
例如聊天室,A和B之间互相发送信息,聊天室就是中介者,负责转发A与B之间的通信。
路由器也是一个中介者,通过MAC地址查找消息发送的目标。
4.6 备忘录模式
备忘录模式保存了一个对象的某个状态,以便在适当的时候恢复对象。在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,并且能在适当的时候恢复对象。
例如:windows的ctrl+z回退;数据库的事务管理
4.7 观察者模式
当对象被修改时,自动通知对此对象感兴趣的目标对象。
例如:各种事件机制
4.8 状态模式
类的行为是基于它的状态的改变。状态模式通过按类的状态这个维度进行拆分,分离各个不同状态的逻辑。当一个对象具有很多不同的状态,且不同状态下对操作的响应不同,或者说执行某操作的判断情况的条件分支相关代码过于复杂时,状态模式会是解决这一问题的非常好的方案。
例如:项目申报中,在不同项目状态下,提交项目将执行不同的操作。
通过创建不同的状态类,来执行各个状态时的操作。
4.9 空对象模式
用空对象取代NULL对象实例的检查。空对象可以在数据不可用时提供默认的行为。
在有些地方直接使用NULL,额外的判断,若使用空对象,则不需要判断,且可以执行NULL时的默认行为。
4.10 策略模式
一个类的行为或其算法可以在运行时更改。通过改变策略对象,来改变对象的行为。
例如:超市收银,对不同的商品和不同的客户可以采取不同的计价策略。例如普通客户和VIP客户,商品满减、两件五折等,都可以通过策略模式调整计价。
4.11 模板模式/模板方法模式
一个抽象公开定义了执行它的方法的方法模板,它的子类可以按需要重写方法实现,但是调用将以抽象类中定义的方式进行。
抽象类定义好骨架,具体每个步骤的实现由子类自己定义。
4.12 访问者模式
对数据结构的操作封装,实现对数据结构的操作与数据结构本身分离。可以看作一个增强型的策略模式。
例如:一个文件目录数据结构,可以有文件查看访问者,文件搜索访问者;也可以增加其他的访问者,都与文件目录数据结构无关。