设计模式是在软件开发过程中经常遇到的问题的通用解决方案。它们是经过无数的验证和经验积累的最佳实践。
首先,设计模式是一些前人经验的一些总结,所以,当遇到相似的问题的时候,我们可以直接借鉴好的设计模式来实现,这这样可以大大降低我们的试错成本和迭代成本。可以大大提升我们的开发速度。
而且,设计模式都是遵守了很多设计原则的,这些原则可以帮助我们大大提升代码的可重用性、可维护性和可扩展
性等。
设计七大原则
设计模式 | 解释 |
---|---|
开闭原则 | 对扩展开放,对修改关闭 |
依赖倒置原则 | 通过抽象使各个类或者模块不互相影响,实现松耦合 |
单一职责原则 | 一个类、接口、方法只做一件事 |
接口隔离原则 | 尽量保证接口的纯洁性、客户端不需要依赖多余的接口 |
迪米特原则 | 最少知道原则,一个类对于所依赖的类知道的越少越好 |
里氏替换原则 | 子类可以扩展父类的功能但不能改变父类原有的功能 |
合成复用原则 | 尽量使用对象组合、聚合的方式,而不是使用继承实现代码复用的目的 |
设计模式分类
- 创建型:在创建对象的同时隐藏创建逻辑,不使用new直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括工厂/抽象工厂/单例/建造者/原型模式。
- 结构型:通过类和接口间的继承和引用实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。
- 行为型:通过类之间不同通信方式实现不同行为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式
创建型模式
工厂模式
简单工厂
简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数
UML类图如下:
适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。
- Calendar 抽象类的getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。
- Spring 中的 BeanFactory 使用简单工厂模式,根据传入一个唯一的标识来获得 Bean 对象。
工厂方法
和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂。
最上层的接口工厂只是定义了规范,创建逻辑由实现类去做,不同的工厂创建不同的产品、
类图:
适用场景
- 创建对象需要大量重复的代码
- 应用层不依赖于产品类实例如何被创建、实现等细节
- 一个类通过其子类来指定创建哪个对象、
优点:
- 用户只关心所属产品对应的工厂,无须关心创建细节
- 加入新产品符合开闭原则,提高了系统的可拓展性
缺点:
- 类的个数容易过多,增加了代码结构的复杂度
- 增加了系统的抽象性和理解难度
抽象工厂
简单工厂模式和工厂方法模式都是针对一类产品,如果要生成另一种产品那就比较难办了
抽象工厂模式通过在 AbstarctFactory 中增加创建产品的接口,并在具体子工厂中实现新产品的创建,当然前提是子工厂支持生产该产品。否则继承的这个接口可以什么也不干。
示例:定义了工厂能创建什么产品,具体的生产产品的逻辑由对应的品牌来实现,同时支持了不同品牌来实现
适用场景
- 应用层不依赖于产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一个产品族),一起使用创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现
优点
- 具体产品在应用层代码隔离,无须关系创建细节
- 将一个系列的产品族统一到一起创建
缺点
- 规定了所有可能被创建的产品集合,产品族中拓展新的产品困难,需要修改抽象工厂的接口
- 在增加了系统的抽象性和理解难度
实际应用:
Spring 的抽象工厂:AbstarctBeanFactory
单例模式
一个单例类在任何情况下都只存在一个实例, 构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。
优点是内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用。缺点是没有抽象层,难以扩展,与单一职责原则冲突。
饿汉式-线程安全
饿汉式单例模式,顾名思义,类一加载就创建对象,这种方式比较常用,但容易产生垃圾对象,浪费内存空间
优点:线程安全,没有加锁,执行效率较高
缺点:不是懒加载,类加载时就初始化,浪费内存空间
饿汉式单例是如何保证线程安全的呢?它是基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。
public class Singleton {// 1.私有化构造器private Singleton() {}// 2.静态变量创建对象实例private static Singleton singleton = new Singleton();// 3.对外提供一个公共获取实例的方法public Singleton getSingleton(){return singleton;}
}
这种方式可以通过反射来破坏单例。
懒汉式,线程不安全
等到使用的时候再去创建实例
public class Singleton {// 1.私有化构造器private Singleton() {}// 2.静态变量创建对象实例private static Singleton singleton;// 3.对外提供一个公共获取实例的方法public Singleton getSingleton(){// 判断为 null 的时候再创建对象if (null == singleton) {singleton = new Singleton();}return singleton;}
}
懒汉式,线程安全
在 getSingleton() 上加 sychronized 锁,保证线程安全
public class Singleton {// 1.私有化构造器private Singleton() {}// 2.静态变量创建对象实例private static Singleton singleton;// 3.对外提供一个公共获取实例的方法public synchronized Singleton getSingleton(){// 判断为 null 的时候再创建对象if (null == singleton) {singleton = new Singleton();}return singleton;}
}
双重检查锁(DEL,即double-checked locking)
因为存在指令重排序的问题,导致加锁的方式也会获取到非 null 的实例
创建对象原本步骤:
- 在堆内存开辟内存空间
- 调用构造方法,初始化对象
- 引用变量指向堆内存空间
重排序后可能得步骤:
- 在堆内存开辟内存空间
- 引用变量指向堆内存空间
- 调用构造方法,初始化对象
这时引用变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进入了 getInstance 的第一个 if(instance == null) 判断不为 nulll,导致错误使用了没有初始化的非 null 实例,这样的话就会出现异常,
public class Singleton {// 1.私有化构造器private Singleton() {}// 2.静态变量创建对象实例private volatile static Singleton singleton;// 3.对外提供一个公共获取实例的方法public Singleton getSingleton(){// 判断为 null 的时候再创建对象if (null == singleton) {// 加锁synchronized (Singleton.class) {// 第二重检查是否为 nullif (null == singleton) {singleton = new Singleton();}}}return singleton;}
}
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序,从源码到最终执行指令会经历如下流程:
- 源码编译器优化
- 重排序指令级并行重排序
- 内存系统重排序
- 最终执行指令序列
静态内部类,线程安全
public class Singleton {// 1.私有化构造器private Singleton() {}// 2.静态变量创建对象实例private static Singleton singleton;// 3.对外提供一个公共获取实例的方法public Singleton getSingleton(){return InnerClass.INSTANCE;}// 定义静态内部类private static class InnerClass{private final static Singleton INSTANCE = new Singleton();}
}
虚拟机有且仅有 5 种情况必须对类进行初始化,称为主动引用:
被动引用:当 getInstance() 方法被调用时,InnerClass 才在 Singleton 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance() 方法返回出去,这点同饿汉模式。
静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
枚举单例,线程安全
可避免被反射破坏
public enum Singleton { INSTANCE; public void whateverMethod() { }
}
通过字节码可以看到 INSTANCE被 static fina l修饰,所以可以通过类名直接调用,并且创建对象的实例是在静态代码块中创建的。
因为 static 类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程安全的,所以创建一个 enum 类型是线程安全的。
结构性模式
适配器模式
定义:
- 适配器模式,是指将一个类的接口转换成客户期望的另一个接口,使原本不兼容的接口可以一起工作
- 在不改变之前代码的情况下,增加适配器,用来适配新的规则或者标准
- 结构性设计模式
场景:
- 已经存在类,它的方法和需求不匹配(方法结果相同或相似)的情况
- 适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品,不同厂家造成功能相似而接口不同的情况下的解决方案
优点:
- 能够提高类的透明性和复用性,现有类复用,但是不需要改变
- 目标类和适配器类解耦,提高程序的拓展性
- 在很多业务场景中符合开闭原则
缺点:
- 适配器模式在编写时需要全面考虑,增加系统的复杂性
- 增加代码的阅读难度,降低代码的可读性,过多的使用适配器模式被使系统变得凌乱
装饰者模式
定义:
- 是指在不改变原有的对象的基础上,将功能附加到对象上,提供了比继承更有弹性的替代方案(拓展原有对象的功能)
- 装饰者和被装饰者都实现同一接口
- 结构性模式
场景:
- 用于拓展一个类的功能或给一个类添加附加职责
- 动态地给一个对象添加功能,这些功能还可以动态地撤销
优点:
- 装饰者是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象拓展功能,即插即用
- 通过不同的装饰类和装饰类的不同排列组合,可以实现不同的效果
- 装饰者完全遵守开闭原则
缺点:
- 会出现更多的代码,更多的类,增加程序的复杂性
- 动态装饰时,多层装饰时会更复杂
装饰者模式和适配器模式对比
装饰器模式 | 适配器模式 | |
---|---|---|
形式 | 是一种非常特别的适配器模式 | 没有层级关系,装饰器模式有层级关系 |
定义 | 装饰者和被装饰者都实现同一个接口,主要是为了拓展之后依旧保留OOP关系 | 适配器和被适配者没有必然的联系,通常是采用继承和代理的形式进行包装 |
关系 | 满足is-a(类的父子继承关系)的关系 | 满足has-a(表示组合,包含)的关系 |
功能 | 注重覆盖、拓展 | 注意兼容、转换 |
设计 | 前置考虑 | 后置考虑 |
代理模式
定义:
- 代理模式是为其它对象提供一种代理,以控制对这个对象的访问
- 代理对象在客户端和目标对象之间起着中介的角色
- 结构性设计模式
场景:
- 保护目标对象
- 增强目标对象
方式:
- 静态代理:
- 显示声明被代理对象
- 动态代理:
- 动态配置和替换代理对象
享元模式
享元模式是一种通过尽可能多地共享数据来最小化内存使用和对象数量,从而提高性能的设计模式。在享元模式中,如果需要相同数据的多个对象,则共享这些对象而不是创建新的对象,从而提高系统的效率。
其实有很多应用场景,我们日常经常能接触到,但是很多人并不知道这其实是享元模式,如:
字符串池:在Java中,String对象使用了享元模式,通过字符串池的方式共享相同的字符串对象,避免了重复创建
其实,很多池化技术,如数据库连接池、线程池等,背后都是采用了享元模式来共享对象的。
在服务器端开发中,享元模式也经常被使用,可以用来管理网络连接,避免资源的浪费
行为性模式
观察者模式
定义:
- 定义对象之间一对多的依赖,让多个观察者对象同时监听一个主体对象,当主体对象发生变化时,它的所有依赖着(观察者)都会受到更新的通知
- 行为性模式
- 发布订阅模式
场景:
- 关联行为之间建立一套触发机制的场景
优点:
- 观察者和被观察者之间建立了一个抽象的耦合
- 耦合度较低,两者之间的关联仅仅在于消息的通知
- 被观察者无需关心他的观察者
- 观察者模式支持广播通信
缺点:
- 观察者之间有过多的细节依赖,提高时间消耗和程序的复杂度
- 使用要得当,避免循坏调用
责任链模式
定义:
- 一个请求沿着一条“链”传递,直到该“链”上的某个处理者处理它为止。(switch-case)
- 一个请求可以被多个处理者处理或处理者未明确指定时。
场景:
- 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。该模式能将多个处理者连接成一条链。接收到请求后,它会“询问”每个处理者是否能够对其进行处理。这样所有处理者都有机会来处理请求。
- 当必须按顺序执行多个处理者时,可以使用该模式。无论你以何种顺序将处理者连接成一条链,所有请求都会严格按照顺序通过链上的处理者。
实际应用场景:
- 过滤器链:在Web开发中,可以通过责任链模式来实现过滤器链,例如Spring框架中的FilterChain就是一条责任链,每个过滤器都有机会对请求进行处理,直到最后一个过滤器处理完毕。
- 日志记录器:在日志系统中,可以使用责任链模式来将日志记录器组成一条链,从而实现多种日志记录方式的灵活组合。
- 异常处理器:在应用程序中,可以使用责任链模式来实现异常处理器的链式调用,从而灵活地处理各种异常情况。
- 授权认证:在系统中,可以使用责任链模式来实现授权认证的链式调用,从而灵活地控制不同用户对系统的访问权限。
策略模式(strategy)
定义:
- 策略莫斯,是指定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的用户
场景:
- 假如系统中有很多类,而他们的区别仅仅在于它们的行为不同
- 一个系统需要动态地在集中算法中选择一种
- if else,switch
策略模式在源码中的体现:
- jdk中:
- Comparator接口,compare方法,比较大小
- Arrays ,sort方法有用到compare
- TreeeMap构造方法中的比较器有用到compare
- spring中:
- Resource 接口,策略
- 类初始化(InstantiationStrategy),cglib,jdk,单例,原型
优点:
- 策略模式符合开闭原则
- 避免使用多重条件转移语句,如if…else…语句、swutch语句
- 使用策略模式可以提高算法的保密性和安全性
缺点:
- 客户端必须知道所有策略,并且自行决定使用哪一个策略类
- 代码中会产生非常多的策略类,增加维护难度
模版模式
定义:
- 模板模式也叫做模板方法模式,是指定义一个算法的骨架,并允许子类为一个或者多个步骤实现
作用:
- 模板方法使得子类在不改变算法结构的情况下,重新定义算法的某些步骤
场景:
- 一次性实现算法不变的部分,再把可变的行为留给子类去实现
- 各自类中的公共行为被提取出来并集中到一个公共的父类中,从而避免代码的重复
优点:
- 提高代码的复用性
- 提高代码的拓展性
- 符合开闭原则
缺点:
- 类数目的增加
- 间接的增加了系统实现的复杂度
- 继承关系自身的缺点,如果父类增加新的抽象方法,所有子类都要改一遍
定义好公共的部分,预留钩子方法给子类重写来干预流程
例如:JDBC的连接,预留执行sql的钩子方法,用来执行不同的sql语句
状态模式
状态模式允许一个对象在其内部状态发生改变时改变它的行为,使其看起来像是修改了其类。它通过将对象的行为包装在不同状态对象中,实现了在运行时更改对象的状态,从而影响其行为。
状态模式也有很多实际的应用场景,如:
- 订单状态管理:订单状态有很多种,如未付款、已付款、已发货、已签收等。不同状态下,订单的行为也不同。
- 音视频播放器:音视频播放器的状态有很多种,如播放、暂停、停止、快进、快退等。不同状态下,播放器的行为也不同。
在实际应用中,状态模式通常需要和其他设计模式结合使用,例如工厂模式、单例模式、策略模式等,以实现更灵活和高效的代码设计。
Spring 中使用了哪些设计模式
- 工厂设计模式 : Spring 使用工厂模式通过
BeanFactory
、ApplicationContext
创建 bean 对象。 - 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中
jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。
享元模式
在Java中,Strinq对象使用了享元模式,即在内存中共享相同的字符串常量,当创建一个新的字符串对象时,会先在字符串池中查找是否已经存在相同的字符串常量,如果存在,则直接返回该常量的引用;如果不存在,则创建-个新的字符串常量,并将其加入到字符串池中,以便以后的重复使用。
这种共享字符串常量的机制可以大大减少内存的使用,因为同一个字符串常量在内存中只会存在一份拷贝,而不同的字符串对象可以共享同一个字符串常量,避免重复创建相同的字符串对象。
不可变模式
String对象还使用了不可变模式,即一旦创建了一个字符串对象,就不能再修改其内容。这是通过将String类中的字符数组定义为private final的方式实现的,即该字符数组一旦被初始化,就不能再修改其内容,保证了字符串对象的不可变性,
这种不可变模式带来了一些好处,如线程安全、安全性、可靠性等。因为不可变的对象在多线程环境下是线程安全的,可以被多个线程共享,不需要进行额外的同步操作。同时,不可变的对象在安全性和可靠性方面也有优势,因为一旦对象创建完成,就不会再被修改,避免了意外修改导致的问题。