我所理解极简java的23种经典设计模式(附加举例)
作为java程序员,设计模式是一个无论如何都绕不开的知识点。
话不多bib,直接上干货:
一、什么是设计模式
比较正式的定义是:
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。
简而言之,就是一套约定俗成的针对大多数问题的通用解决方案,在灵活运用设计模式的情况下,可以大大的提升代码的质量(可读性,可维护性)
二、设计模式分为那几大类
根据目的可以分为三大类:
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
三、各个模式的简介与具体应用
编写提示:简单的模式会一笔带过(如原型,建造者),有些较为复杂的模式会深入详解(如简单工厂,工厂方法以及抽象工厂)
创建型模式:
单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点(又可以分为饱汉单例模式以及恶汉单例模式)
举例:jdk 的Runtime类
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
2.工厂模式:
简单工厂模式:适用场景:工厂类负责创建的对象较少,客户端只关心传入工厂类的参数,对于如何创建对象的逻辑不关心
举例:jdk的Calendar,根据不同的地区来创建不同的日历对象,就好比日历这个工厂,生产着世界上各地区的日历,我需要这个地区日历,我只需要传参数告诉工厂即可,不需要知道日历制作过程和实例的过程
工厂方法模式:定义一个创建对象的接口,由子类决定需要实例化哪一个类。工厂方法使得子类的实例化过程推迟
在使用 JDBC 进行数据库开发时,如果数据库由MySQL改为Oracle或其他,则只需要改一下数据库驱动名称就可以,其他都不用修改(前提是使用的都是标准SQL语句)。或者在Hibernate框架中,更换数据库方言也是类似道理。
同样,jdbc也算满足简单工厂模式的定义:根据不同的参数来选择不同的驱动创建不同的数据库连接,就好比jdbc这个工厂,生产着世界上各个的数据库连接,我需要这个数据库连接,我只需要传参数告诉工厂即可,不需要知道数据库连接制作过程和 实例的过程
抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
这个是工厂模式中最难的一个工厂,因为它多了一个产品等级的概念
抽象工厂模式是工厂方法模式的升级版本,它用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。
举例:一样的jdbc
java.sql.Connection
接口java.sql.Connection相当于抽象工厂CourseFactory,定义了一个产品族会生产哪些产品;
抽象方法的返回值java.sql.Statement和java.sql.PreparedStatement相当于定义了2个产品等级结构,对应Video和Article;
java.sql.Statement和java.sql.PreparedStatement也是接口;
com.mysql.jdbc.ConnectionImpl是一个具体工厂,生产产品族MySQL的产品,其中生产的com.mysql.jdbc.StatementImpl和com.mysql.jdbc.ServerPreparedStatement是具体产品,都属于MySQL产品族;
详情可以看这篇博客,这里就不多做讲解了:C03 抽象工厂 JDK源码分析
3.构建器模式:将一个复杂类的表示与其构造相分离,使得相同的构建过程能得到不同的表示(例如lombok以及guava的缓存都有用构建器模式) https://juejin.cn/post/6844903862600466439
CacheBuilder.newBuilder().maximumSize(100).expireAfterAccess(5000, TimeUnit.MILLISECONDS).build();
如图所示:构造了一个最大100个,访问后5000毫秒过期的缓存对象
4.原型模式(Prototype)
用原型实例指定创建对象的类型,并通过copy这个原型来创建新的对象(比如spring的beanutils的copy方法)
这里会有关键字方便记忆
结构型模式
1. 适配器模式(Adapter) :这在业务中用的比较少,不过jdk底层有大量的应用,比如jdk中关于字符流和字节流的转换就是用了适配器模式 https://developer.ibm.com/zh/articles/j-lo-adapter-pattern/
将一个类的接口转换成用户希望得到的另一种接口。
关键字:转换接口
2. 桥接模式:将类的抽象部分和实现部分分离开来,使它们可以独立地变化。也就是把一棵树拆成2棵树,再把两棵树连接起来。也就是继承与拆分
jdk中DriverManager使用过(也就是多种数据库对于jdbc规范的实现)
关键字:抽象与实现相互分离
3. 组合模式:将对象组合成树形结构以表示“整体-部分”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
举例:这个就太多了,比较经典的就是jdk的文件操作系统file类,HashMap arraylist类
关键字:树形目录结构
4. 装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。
例如:jdk的io相关类:inputstream
来吧,展示:先读取文件流,然后套上缓冲功能,最后直接将流进行GZIP解压缩,一气呵成
InputStream inputStream = new GZIPInputStream(new BufferedInputStream(new FileInputStream("D:\\temp\\test.txt")));
详情可以看 从java的InputStream类认识装饰者模式
关键字:动态附加职责,我喜欢称为套娃模式
5. 外观模式:定义一个高层接口,为子系统中的一组接口提供一个一致的外观。
spring mvc 就是对于该模式的良好实现(广义上来说spring的mvc,io,日志框架,各种涉及底层复杂的框架封装,甚至jvm本身,以前的rpc,最新的微服务等都是对于外观模式的良好演示)
关键字:对外统一接口
6. 享元模式:提供支持大量细粒度对象共享的有效方法。
从理念以及业务使用角度来说,数据库的三范式以及各个缓存框架是对于享元模式的高频率使用场景
关键字:汉字编码以及数据库设计的外键关联
7. 代理模式:为其他对象提供一种代理以控制这个对象的访问。
spring的aop就是对于该模式的良好诠释
关键字:代理访问
行为型模式
1. 职责链模式:通过给多个对象处理请求的机会,减少请求的发送者与接收者之间的耦合。将接收对象链接起来,在链中传递请求,直到有一个对象处理这个请求。
在springMVC中,DispatcherServlet这个核心类中使用到了HandlerExecutionChain这个类,他就是责任链模式实行的具体类。HandlerExecutionChain主要负责请求的拦截器的执行和请求的处理,但是他本身不处理请求,只是将请求分配给在链上注册的处理器执行,这是一种责任链的实现方式,减少了责任链本身与处理逻辑之间的耦合的同时,规范了整个处理请求的流程
关键字:传递职责
2. 命令模式:请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
Java中,线程是命令模式的典型例子。Runnable接口相当于命令模式中的Command,而Thread相当于命令的调用者,只不过这个调用动作是Java自动完成,不需要我们手动编写代码,而在上面的例子中,我们需要调用Button类的click方法。线程不关心Runnable如何执行,操作哪些对象,它只简单执行Runnable中定义的run方法,然后把注意力放在线程本身的管理和控制上。这样一来,Thread和Runnable具体操作的对象实现解耦,很容易定义一个不同的命令(Runnable)而无需更改Thread类。
javax.swing.Action也是命令模式的实现。
而spring mvc也是对命令模式的常见实现,通过点击web按钮触发ajax,服务端收到请求,然后解析url参数命令找到对应的处理器进行分发处理。
关键字:线程,可撤销,web请求
3. 解释器模式:给定一种语言,定义它的文法表示,并定义一个解释器,该解释器用来根据文法表示来解释语言中的句子。
有种东西叫做dsl,翻译成中文就是解释自定义语言
dsl比较出名的有 sql,Groovy,el表达式,正则表达式,yaml,
以上dsl都算解释器模式----
才怪。
dsl不算是解释器模式,但是dsl的底层翻译以及语法解析相关类算是解释器模式。
比如java.util.Pattern类,通过compile方法编译正则表达式这一种语言,并与字符串匹配。
关键字:解释器,正则表达式,el表达式
4. 迭代器模式:提供一种方法来顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。
举例:jdk的集合以及hashmap
关键字:数据集遍历
5. 中介者模式:用一个中介对象来封装一系列的交互。它使各对象不需要显式地相互调用,从而达到低耦合,还可以独立地改变对象之间的交互。
JDK中的Timer就是中介者类的实现,而配合使用的TimerTask则是同事类的实现。
如果同事对象多了,交互也复杂了。中介者会庞大,变得复杂难以维护。
也就是将网状结构改为星形结构
从广义上来说各个模块之间通过反射类进行方法调用,apigateway网关进行路由调用也能算是中介者模式。
关键字:不直接引用
6. 备忘录模式:保存一个对象的某个状态,以便在适当的时候恢复对象
举例:jdbc关于事务的操作
关键字:事务,数据库,持久化
7. 观察者模式:定义对象之间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动更新
举例:jdk的Observer接口,用来实现对应事件触发器
前端的mvvm也都有相关实现,比如点击某个元素,就会触发所绑定的事件
发布和订阅
关键字:联动
8. 状态模式:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
状态模式主要适用于当控制一个对象状态转换的条件表达式过于复杂时,把状态判断的逻辑封装到具体状态类中。
更加高内聚低耦合 (代码更加容易抽离)
举例:这个模式在前端框架用的比较多,比如mvvm的数据驱动视图
后端的话就是迭代器了,迭代到第几个元素,是否迭代完毕 统统不关心,只管迭代就行了。
关键字:状态变成类
9. 策略模式:定义一系列算法,把它们一个个封装起来,并且使它们之间可相互替换,从而让算法可以独立于使用它的用户而变化。
例如:商场的满减和 打折都是现实生活中的策略模式
spring框架中的策略模式体现在,注解上面用作类实例化(指定单列,多个等策略)
org.springframework.core.io.Resource类,用作资源加载(指定如何采用何种策略加载资源文件)
关键字:多方案切换 前端换皮肤
10. 模版方法模式
定义一个操作中的算法骨架,而将一些步骤延迟到子类中。
例如:spring jdbctemplate以及AbstractList 就是模板方法模式的体现
关键字:抽象和实现分离,先定义预处理的一系列行为骨架
11. 访问者模式:一种分离对象数据结构与行为的方法。通过这种分离,可达到一个被访问者动态添加新的操作而无需做其他的修改的效果。
适用于数据结构相对稳定,算法易变化的系统。
简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可为一个被访问者动态添加新的操作而无需对数据结构本身进行修改。
广义上来说后端的service层就体现了访问者模式(业务程序和数据实体的代码分离)
关键字:结构与行为分离
以上就是java的23种常用设计模式。当然随着时代的变化,更多使用场景的出现,23种通用解决方案式已经不足以的应对许多新的问题,由此还出现了更多的设计模式,比较出名的几种就是ioc,aop,事务总线,mvc,mvvm(数据驱动视图)
经过了四年多的java学习,小农总结一下自己学习并且理解设计模式的心得
各个设计模式都是这对于某个通用场景的通用解决方案,不能解决所有的场景问题
各个模式之间不是互斥的,反而很大一部分是体现了你中有我,我中有你的的特点,而且很多实现会综合体现出多个设计模式的特点。
设计模式只是解决事务的表象,六大设计原则才是本质,只要符合设计原则的代码都可以被称为设计模式
设计模式是一种思想,是对于现实解决方案的抽象体现,而并非拘泥一种固定的结构,一门具体的语言的禁锢,比如享元模式,现在我们的常用场景是在数据库而很少在业务代码中进行体现。
最后,放上六大设计原则结尾:
1.开闭原则。
一个软件实体,如类,模块和函数应该对外扩展开发,对内修改关闭。
2.单一职责原则。
一个类只允许有一个职责,即只有一个导致该类变更的原因。
3.依赖倒置原则。
依赖抽象而不是依赖实现。抽象不应该依赖细节,细节应该依赖抽象。高层模块不能依赖低层模块,二者都应该依赖抽象。
4.接口分离原则。
多个特定的客户端接口要好于一个通用性的总接口。
5.迪米特法则
一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
6.里氏替换原则。
所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。
采菊东篱下,悠然见南山。山气日夕佳,飞鸟相与还。