软件设计模式是前辈们代码设计经验的总结,可以反复使用。设计模式共分为3大类,创建者模式(6种)、结构型模式(7种)、行为型模式(11种),一共24种设计模式,软件设计一般需要满足7大基本原则。
目录
面试题总结复盘篇
1.1、介绍以下软件开发原则?(设计模式7大原则)
1.2、设计模式如何分类?
1.3、单例模式的优缺点及使用场景?
1.4、代理模式的优缺点及使用场景?
1.5、策略模式的优缺点及使用场景?
1.6、简单工厂模式、工厂方法模式以及抽象工厂模式的优缺点?
1.7、举例说明你在做项目中如何使用软件设计模式的原则?
1.8、你使用过哪些设计模式,在哪些情况下会使用设计模式?
1.9、单例模式的用哪几种?手写实现一下?
1.10、举例说明设计模式在框架源码中的应用?
1.11、介绍一下代理模式,同时介绍以下动态代理与静态代理的区别?
1.12、简述Spring Bean中是如何实现单例模式的?
1.13、举例建造者模式在框架源码中的应用?
1.14、JDK 类库常用的设计模式有哪些?
1.15、观察者模式的优缺点及使用场景?
1.16、适配器模式的优缺点及使用场景?
1.17、装饰者模式的优缺点及使用场景?
1.18、模板方法模式优缺点及使用场景?
1.19、装饰者模式与代理模式的区别?
1.20、什么是设计模式,为什么要学习设计模式?
结合这篇开发规约进行学习,会让你的代码写的更规范:【Java】阿里巴巴Java开发手册-CSDN博客
设计模式的7大原则,具体如下:
1.开闭原则:软件实体 (类、模块、函数等等) 应该是可以被扩展的,但是不可被修改。
2.里式代换原则:继承的时候尽量不要重写父类的方法,如果重写要保证不破坏原方法的功能。
3.依赖倒转原则:抽象不应该依赖于细节,细节应该依赖于抽象,依赖接口,面向接口编程。
4.接口隔离原则:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
5.迪米特法则:最少知道原则,它表示一个对象应该对其它对象保持最少的了解。通俗来说就是,只与直接的朋友通信。
6.合成复用原则:通过组合或者聚合的方式替代继承,增强封装性与提升复用的灵活性。
7.单一职责:表示一个模块的组成元素之间的功能相关性,即一个类只负责一项职责。
设计模式共分为3大类,创建者模式(6种)、结构型模式(7种)、行为型模式(11种),一共24种设计模式。
创建型模式(6种):本质上就是将对象的创建与使用分离,就是描述怎样去创建对象。
包括:单例、原型、简单工厂、工厂方法、抽象工厂、建造者模式。
单例模式:单例模式是指确保一个类在任何情况下都只有一个实例,并且提供一个访问该单例的全局访问点。单例模式分为饿汉模式与懒汉模式两种。饿汉式在类加载时候创建实例,是线程安全的,但是类加载的时候就创建实例对象,如果后期不使用,会造成内存的浪费,懒汉模式是在使用的使用创建实例,是线程不安全的,需要使用双重检测琐的形式保证线程安全。
原型模式:用一个已经创建的实例作为原型,通过复制该原型对象创建一个和该原型对象相同的对象。通过深克隆和浅克隆方式实现原型模式,浅克隆:java通过实现实现Cloneable接口重写clone()方法;深克隆:java通过实现序列化的反序列化的方式实现。
简单工厂模式:简单工厂模式可以理解为一种编程习惯,通过定义一个工厂类,让客户端和工厂类进行交互,而不是直接和具体产品类进行交互,通过通过类去和具体产品类进行交互。
工厂方法模式:工厂方法模式可以避免简单工厂模式违反开闭原则与依赖倒转原则的问题,定义一个用于创建对象的接口(工厂),让子类对象决定实例化哪个产品对象,使一个产品的实例化延迟到其工厂的子类。
抽象工厂模式:工厂方法模式是用于生产同一等级的多种产品,对于不同等级的产品,使用抽象工厂生产。抽象工厂模式:生产多等级的产品,将一个具体工厂生产的不同等级的产品的一组产品成为产品族。
建造者模式:将复杂对象的构造与装配分离,使得同样的装配组件的过程可以生产出不同的整体。
结构型模式(7种):本质上是将类或者对象按照某种布局组成更大的结构。
包括:代理、适配器、桥接、装饰者、外观、享元、组合模式。
代理模式:Java中按照代理类的生成时期不同分为静态代理与动态代理,静态代理类是在变编译时候生成的,动态代理类是在Java运行的时候生成的,动态代理又分为JDK动态代理与CGlib动态代理。动态代理与静态代理相比,最大的好处在于接口/类中 的方法都被集中到一个invoke()方法中进行处理,另外动态代理的代码耦合度低,动态生成代理类的,不是手动编写代理类。整体来说JDK动态代理的效率的高于CGlib,所以有接口使用JDK动态代理,没有接口使用CGlib动态代理。
适配器模式:将一个接口转换成希望的另外一个接口,使原来因为接口不兼容而不能在 一起工作的接口可以在一起工作。
桥接模式:将抽象与实现分离,使得它们可以独立变化,利用组合关系代替继承关系来实现,降低抽象与实现这两个维度的耦合度。
装饰者模式:在不改变现有对象接口的情况下,动态地给对象增加一些额外的功能。装饰者是增强目标对象,静态代理主要是为了保护和隐藏目标对象。获取目标对象构建的地方也不同,装饰者是由外界传递的,静态代理是在代理类内部创建的。
外观模式:外观模式又称为门面模式,是通过为多个复杂的子系统提供一致的接口,使得这些子系统更容易被访问。外部应用程序不需要关系内部子系统的内部实现细节。
享元模式:运用共享技术来支持大量细粒度对象的复用,通过共享已经存在的对象来大幅度减少需要创建对象的数量,避免大量相似对象的开销,从而提高系统资源利用率。
组合模式:又称为部分整体模式,用于把一组相似的对象当作一个单一的对象,组合模式依据树形结构来组合对象,用来表示部分和整体的层次。
行为模式(11种):本质是描述类与对象协助完成单个对象无法完成的任务,以及怎么分配职责。
包括:模板方法、策略、命令、责任链、状态、观察者、中介者模式、迭代器、访问者、备忘录、解释器。
模板方法模式:定义一个算法骨架,将算法的一些步骤延迟到其子类中,使得子类不改变算法结构的情况下,重新定义该算法的特定步骤。
策略模式:策略模式定义了一系列算法,并把个每个算法封装起来,并把使用算法的责任与算法的实现进行分割,并委派不同的对象对算法进行管理。本质上就是同一接口不同的实现类。
命令模式:将一个请求封装成一个对象,将发出请求与执行请求分割开,这样可以使得两者通过命令对象进行沟通。
责任链模式:责任链模式又名职责链模式,就是将请求的处理者拼接成一条链,当请求发生时候,可以沿着链进行传递,直至找到处理的对象。
状态模式:对于有状态的对象,把复杂的判断逻辑提取到不同的状态对象中去,允许在对象内部状态发生变化时改变其行为。
观察者模式:观察者模式又称为发布订阅模式,定义一对多的依赖关系,让多个观察者同时监听一个主题对象,主题对象变化,观察者可以更好地更新自己。
中介者模式:中介者模式又称为调停模式,定义一个中介角色来封装一系列对象的交互操作,使得原有对象之间耦合度减小。
迭代器模式:提供一个对象来访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
访问者模式:封装一些作用于某种数据结构的各种操作,在不改变数据结构的前提下定义作用于这些元素的新操作。
备忘录模式:备忘录模式又称为快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在外部保存这个对象的内部状态,当需要使用到的时候可以快速恢复到保存的状态。
解释器模式:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统运行效率。
缺点:因为系统中只有一个实例,导致了单例类的职责过重,违背了“单一职责原则”,同时不利于扩展。
使用场景:
优点:
松耦合,代理模式可以将目标对象与代理对象解耦,使得它们可以独立地进行修改和扩展。
保护目标对象,代理对象可以保护目标对象,隐藏其真实实现细节,提高系统的安全性。
控制访问,代理对象可以控制对目标对象的访问,可以在调用目标方法前后添加额外的逻辑,如权限验证、日志记录等。
AOP支持,代理模式是实现面向切面编程(AOP)的一种重要方式,在Spring框架中广泛应用于事务管理、日志记录、性能监控等方面
缺点:复杂性增加,代理模式会增加代码的复杂性,特别是在使用动态代理时,需要理解和处理动态生成的代理类。
使用场景: 业务中的功能不得不写,但是写了又会影响代码的耦合性 这样的问题适合使用代理模式
事务管理
Spring的事务管理功能通常使用代理模式来实现。通过在业务方法前后添加事务管理的逻辑,代理对象可以控制事务的开始、提交或回滚,并提供了对事务的管理和控制。
AOP(面向切面编程)
Spring的AOP功能也是基于代理模式实现的。通过定义切点和切面,代理对象可以在目标对象的方法执行前后插入额外的横切逻辑,如日志记录、性能监控、安全验证等。
优点:
开闭原则 : 策略模式 提供了 对 开闭原则 的支持 , 可以在不修改原有系统的基础上 , 选择不同的行为 , 也可以 额外扩展其它行为 ;
避免代码冗余 : 可以 避免使用多重条件判定语句 ; 可以避免出现大量的 if … else … 语句 , switch 语句等 ;
安全保密 : 策略模式可以 提高算法的 保密性 和 安全性 ; 在终端使用策略时 , 只需要知道策略的作用即可 , 不需要知道策略时如何实现的 ;
缺点:
策略类选择 : 客户端 必须 知道所有的 策略类 , 并且自行决定 使用哪个策略类 ;
增加复杂性 : 如果系统很复杂 , 会 产生很多策略类 ;
使用场景:
行为切换 : 系统有 很多类 , 这些类的区别仅仅在于它们的 行为不同 ; 使用策略模式 , 可以 动态地 让 用户对象 在这些行为中, 选择一个行为 ;
将对象的 不同的行为 , 封装到 不同的类 中 , 每个行为对应一种策略 ;
算法选择 : 系统中需要 动态地 在 几种算法 中 选择一种 ;
算法 就是 策略 , 其中封装了一系列的业务逻辑及计算方式 ;
如 : 计算方式 , 给定两个数字 ; 使用加法策略 , 将两个数相加 ; 使用乘法策略 , 将两个数相乘 ;
再如购物网站优惠策略等。
简单工厂模式:它的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。优点:简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象,通过客户与工厂类进行交互,降低客户端与产品类的直接耦合性。缺点:不满足开闭原则,难以扩展。
工厂方法模式:工厂方法是克服了简单工厂不满足开闭原则的情况,通过依赖于抽象工厂而不是依赖于具体工厂。优点:满足开闭原则,扩展工厂类更方便。缺点:产品类多的时候,会导致工厂类增多,维护困难。
抽象工厂模式:抽象工厂模式在于创建同族的一系列产品,优点:方便管理同族产品,模块间的耦合度降低。缺点:扩展新的种类的产品比较麻烦。
1.单一职责:一般一个方法里面用于实现模块的某一功能,尽量减少一个方法一大堆功能。
2.开闭原则:定义接口或者抽象类,通过面向抽象编程,实现接口或者继承抽象类的方式扩展模块功能而不是直接在某一个类中间直接修改内容。
3.依赖倒转:面向接口编程,依赖抽象而不是依赖具体的类。
4.接口隔离:对类的依赖建立在最小的接口上,即每个类实现接口要重写并使用接口的所有方法。
5.里氏代换:子类继承父类尽量不要重写父类方法或者说不要破坏父类方法的功能。
6.迪米特法则:模块之间的耦合度不要过高,必须直接交互的模型才进行通信。
7.合成复用:通过聚合的方式代替继承减少模块之间的耦合度。
单例模式:懒汉与饿汉,使用饿汉模式,数据库连接池,创建一个单例对象,用于提供对数据库连接方法,还有在一些配值类中,声明一个配置读取单例用于配值类的读取。
原型模式:创建复杂对象时,如果每次都是从头初始化,可能会带来性能开销或者逻辑过于复杂,通过实现Cloneable()接口与实现Searializiable接口实现浅克隆与深克隆。
工厂方法模式:定义工厂接口,然后根据具体工厂类进行产品的管理,客户端直接与具体的工厂进行交互。
建造者模式:创建带参数的构造器对象的时候可以使用建造者模式,可以动态改变参数传递顺序,代码可读性也会更好。
代理模式:面向切面编程,日志处理,事务管理这些都可以用代理模式,有接口用jdk动态代理,无接口用cglib动态代理。
策略模式:在Java开发中,策略模式是一种行为设计模式,它使你能在运行时改变对象的行为。通过定义一系列可互换的算法(策略),并在上下文中使用这些策略来解决特定问题,就是定义抽象的策略接口,定义不同接口实现类实现不同的策略,比如:电商网站购卡的不同优惠策略。
观察者模式:用于实现对象之间的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都会得到通知并自动更新。比如:消息队列就是典型的观察者模式。
备忘录模式:用于在不违反封装性原则的前提下,捕获并存储对象的内部状态,以便将来可以恢复到之前的状态,用于在一些回滚的场景,用于还原状态。
装饰者模式:java.io` 包中的输入/输出流类如 `BufferedReader`、`BufferedWriter`、`DataInputStream` 和 `DataOutputStream` 等,这些类作为装饰器添加额外功能到基础的 `InputStream` 和 `OutputStream` 流上。
饿汉模式:线程安全,类加载时候创建实例,但是不使用会导致内存空间的浪费
/**
* @author nuist__NJUPT
* @ClassName Singleton
* @description: 单例模式-饿汉
* @date 2024年02月07日
*/
public class Singleton {
private static Singleton instance = new Singleton() ;
public Singleton(){}
public static Singleton getInstance(){
return instance ;
}
}
懒汉模式:线程不安全,双重检查锁保证线程安全,判空后加琐,避免琐的抢占浪费资源,加琐后再判空,是保障创建一个实例。
/**
* @author nuist__NJUPT
* @ClassName Singleton
* @description: 单例模式-懒汉
* @date 2024年02月07日
*/
public class Singleton {
// volatile保证可见性与有序性,禁止指令重排,防止jvm实例化对象指令重排导致的空指针
private static volatile Singleton instance ;
public Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton() ;
}
}
}
return instance ;
}
}
补充:volatile关键字的作用及底层原理
volatile
关键字在Java编程语言中主要用于声明变量的特殊规则,这些规则确保了多线程环境下的可见性和有序性。具体作用和底层原理如下:
可见性:当一个线程修改了一个被声明为 volatile
的共享变量时,其他线程能够立即看到这个变化,而不是等待缓存失效或刷新。这意味着对 volatile 变量的写操作会强制刷入主内存,并且读操作会从主内存中读取最新值。
禁止指令重排序:编译器和CPU不会对 volatile
变量进行指令重排序优化,保证了程序执行的顺序与源代码中的顺序一致(即 happens-before 规则)。这对于依赖于特定顺序执行的操作特别重要。
内存屏障:Java内存模型(JMM)通过插入内存屏障来实现 volatile
的语义。内存屏障是一种硬件层面的技术,它可以确保一些内存操作按照指定顺序执行。对于 volatile
写操作,会在其后插入“写屏障”,这样能确保所有之前的操作都完成并刷新到主内存;对于 volatile
读操作,则在其前插入“读屏障”,确保在此之后的所有操作都能看到此变量的最新值。
缓存一致性协议:现代多核处理器使用缓存一致性协议(如Intel的MESI协议等)来维持不同CPU核心之间的缓存数据同步。当一个CPU核心修改了 volatile
变量时,该协议会确保其他核心上的缓存副本失效,从而需要从主内存重新加载最新的值。
总结来说,volatile
关键字是Java中用于简化并发编程的一种机制,它确保了多线程环境下的简单同步问题,但不能替代原子操作或解决复杂的互斥问题。在实际应用中,volatile
常用于状态标志、计数器等简单的并发场景,而更复杂的并发控制通常需要借助于 synchronized
或 java.util.concurrent
包中的工具类。
代理模式:属于结构型模式的一种,它为一个目标对象提供一个代理,并由这个代理控制对原对象的访问。在Java中,代理对象可以在调用真实对象的方法前后添加额外的操作,如权限检查、日志记录、性能统计,事务管理等。
静态代理:在编译期间就已经确定了代理类和被代理类的关系。具体实现方式是创建一个代理类,该类实现与目标对象相同的接口或者继承相同的目标类,然后通过代理类方法调用目标方法。代理类通常包含对目标方法的前置处理、后置处理或异常处理逻辑。
动态代理:运行时候动态生成的代理类,灵活性与扩展性更强,包括jdk与cglib动态代理。
Spring框架中的Bean默认是通过单例模式实现的。在Spring IoC容器中,每个定义的Bean默认都会被配置为单例(Singleton)作用域,这意味着无论何时请求该类型的Bean,Spring容器都会返回同一个共享实例。
具体来说,Spring容器如何实现单例模式:
1. 缓存管理:
- 当Spring容器启动或初始化时,它会读取配置信息(XML、注解或Java配置类),根据这些信息创建Bean的定义。
- 对于每个单例Bean,在首次请求时,Spring容器会调用相关的构造器或者工厂方法来创建其实例,并将这个实例缓存起来。
- 当后续有对该Bean的请求时,Spring容器会直接从内部缓存中取出已创建好的单例对象,而不是重新创建一个新的实例。
2. 线程安全保证:
- Spring确保了在多线程环境下的并发安全性,当多个线程同时请求一个还未初始化完成的单例Bean时,Spring容器会对初始化过程进行同步处理,以确保只有一个线程能够初始化该Bean,避免产生多个实例。
3. 生命周期管理:
- Spring对单例Bean的整个生命周期进行全面管理,包括初始化回调(如`@PostConstruct`)、销毁回调(如`@PreDestroy`)等,这样可以确保在整个应用程序上下文中,该Bean的行为一致且资源得到合理分配和释放。
简单地说,Spring通过维护一个内部的Bean定义注册表以及对应的实例缓存,实现了对单例Bean的高效管理和控制,从而确保了对于某个类型的所有请求都返回同一实例这一单例模式的核心特性。
Spring WebFlux 中的 WebClient
类采用了建造者模式来构造复杂的HTTP客户端配置。用户可以通过链式调用方法逐步配置请求的各种属性(如超时、重试策略、过滤器等),最后通过 build()
方法生成一个完整的 WebClient
实例。
Jackson 库用于处理 JSON 数据转换,在创建 ObjectMapper
实例时也使用了建造者模式。通过 ObjectMapper.builder()
可以进行一系列可选配置,最终构建出适合特定需求的 ObjectMapper
对象。
Apache HttpClient 提供了一个自定义 Http 客户端的方式,其中就利用了建造者模式。用户可以按照需要定制客户端的行为,比如设置连接管理器、协议处理器等。
JDK 类库中广泛使用了多种设计模式,以下是一些在 Java 标准库(JDK)中常见的设计模式示例:
1. 工厂模式:
- `java.util.Calendar`:通过 `Calendar.getInstance()` 方法返回一个具体的日历实例,可以根据系统默认的区域设置创建不同的日历实现。
2. 单例模式:
- `java.lang.Runtime` 类和 `java.util.ResourceBundle` 类都是单例设计,保证全局只有一个实例存在。
- 在多线程环境中,`java.util.concurrent.atomic.AtomicInteger` 等原子类也可以看作是某种意义上的单例模式应用,因为它们提供了一种安全且高效的共享状态管理方式。
3. 观察者模式:
- `java.util.Observable` 和 `java.util.Observer` 接口构成了基本的观察者模式框架,用于事件驱动编程和通知机制。虽然在Java Swing/AWT UI框架中广泛应用,但在标准库的核心部分并不常见直接使用这两个接口的地方,更多的是第三方组件或用户自定义场景下的应用。
4. 装饰器模式:
- `java.io` 包中的输入/输出流类如 `BufferedReader`、`BufferedWriter`、`DataInputStream` 和 `DataOutputStream` 等,这些类作为装饰器添加额外功能到基础的 `InputStream` 和 `OutputStream` 流上。
5. 策略模式:
- `java.text.DecimalFormat` 使用不同格式化策略来格式化数字。
- `java.util.Comparator` 接口允许你传递不同的排序策略给 `Collections.sort()` 或 `Arrays.sort()` 方法。
观察者模式(Observer Pattern)是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
优点:
1. 解耦:观察者模式使得观察者和被观察者之间松耦合。观察者只知道事件发生,并不需要了解具体的触发机制,而被观察者也不需要知道有哪些观察者正在监听自己。
2. 扩展性:新的观察者可以很容易地添加到系统中,只需要实现观察者接口即可,不影响已有的观察者或被观察者。
3. 灵活性:观察者可以自由选择是否订阅或取消订阅某个主题,这增加了系统的灵活性和可配置性。
4. 广播通信:被观察者状态变化时,能够高效地将更新信息传播给多个观察者,适合于发布/订阅场景。
缺点:
1. 开销:如果观察者数量过多或者通知过程较为复杂,可能会带来额外的性能开销,尤其是在大量观察者需要同步更新时。
2. 循环依赖与顺序问题:在复杂的系统中,观察者之间的相互依赖可能导致循环调用或者其他难以预料的行为。另外,如果观察者之间的更新顺序有严格要求,维护这种顺序会变得复杂。
3. 内存泄漏:若不正确管理观察者引用,可能会导致内存泄露,特别是在长生命周期的观察者和短生命周期的被观察者结合使用的情况下。
使用场景:
1. 事件驱动系统:如GUI编程中,按钮点击等事件可以通过观察者模式发送给处理程序。
2. 数据绑定:在MVC(Model-View-Controller)架构中,模型层的数据变化时,视图层通过观察者模式立即得到更新。
3. 消息队列:发布/订阅消息传递系统中,发布者发布消息,多个订阅者接收到消息后执行相应的操作。
4. 实时监控系统:如气象监测站,当气象数据发生变化时,需要实时通知所有关注天气变化的应用程序。
5. 数据库连接池:当连接可用时,可以通知等待获取连接的客户端。
适配器模式(Adapter Pattern)是一种结构型设计模式,它的目的是将一个接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法一起工作的类可以协同工作。
优点:
1. 复用现有类:适配器模式能够使现有的、已有的或不可修改的类与新的系统进行交互,无需修改原有代码,提高了代码复用性。
2. 增加灵活性:通过引入适配器类,可以在运行时动态决定使用哪个适配器,使得系统更加灵活,更容易适应变化。
3. 松耦合:适配器模式解除了客户端和目标接口之间的直接耦合关系,降低了它们之间的依赖性。
4. 扩展性:在不修改原接口的情况下,可以通过添加新的适配器来支持更多不同类型的对象。
缺点:
1. 过多的类层级:如果过度使用适配器模式,可能会导致系统的类层级变深,维护成本提高。
2.性能开销:适配器可能引入额外的方法调用,对性能有一定影响,尤其是在需要高性能场景下需要注意优化。
3. 复杂性增:对于复杂的适配情况,适配器的设计和实现可能变得相当复杂,增加了理解和调试难度。
使用场景:
1. 接口不兼容:当两个系统间有接口差异,但又需要进行数据交换或功能调用时,可以创建适配器以协调两者间的通信。
2. 遗留系统整合:在系统升级或集成中,新系统和遗留系统之间可能存在接口不匹配问题,适配器模式可用于连接这些系统。
3. 多态性设计:在面向对象设计中,适配器模式可以用来处理多个子类具有相似行为但接口不同的情况,提供统一的访问方式。
4. 第三方库接入:当项目需要使用某个第三方库,但是该库提供的API与项目现有代码不兼容时,可以通过适配器模式将其转换为项目内部可以使用的接口。
5. 硬件设备驱动开:在编写硬件设备驱动程序时,经常需要将物理设备的操作接口转换为操作系统或应用层可以识别的接口。
装饰者模式(Decorator Pattern)是一种结构型设计模式,它允许在运行时动态地向一个对象添加新的行为和责任,同时保持类的封装性和继承结构的灵活性。
优点:
1. 扩展性:装饰者模式提供了一种比继承更灵活的方式来扩展对象的功能。可以在不修改原有类代码的基础上,通过组合的方式为对象添加新的功能。
2.避免子类爆炸:如果使用传统的继承方式来实现功能扩展,随着需求变化可能会导致子类数量急剧增加,形成“类爆炸”现象。而装饰者模式通过组合而非继承的方式扩展功能,可以有效地减少子类的数量。
3. 透明性:客户端无须知道是原始对象还是装饰过的对象,它们之间具有良好的一致性,对于调用者来说,组件的变化对它们是透明的。
4. 开放封闭原则支持:装饰者模式遵循“开闭原则”,即对扩展开放、对修改封闭。新功能可以通过添加新的装饰者类来实现,无需修改现有代码。
缺点:
1. 过度使用会增加复杂性:如果过度使用装饰者模式,或者在不合适的地方应用,会导致系统变得难以理解和维护,因为需要跟踪每个对象可能具有的装饰层次。
2. 装饰者与被装饰者耦合度较高:装饰者通常需要持有被装饰者的引用,并在其方法中调用被装饰者的方法,这增加了两者之间的耦合度。
使用场景:
1. 需要给已有类动态添加职责或功能:当希望在不影响其他对象的情况下,在运行时为单个对象添加额外的责任或行为时,可以使用装饰者模式。
2. 接口相同但行为不同的多个类需要统一处理:例如在图形用户界面(GUI)编程中,按钮、文本框等组件都有基本的显示功能,但是还可以添加边框、字体样式、背景色等装饰功能。
3. 避免创建大量具有相似职责的子类:比如咖啡店菜单中的各种咖啡饮品,每一种咖啡都可以通过添加不同的调料(如糖、奶泡、香草等)来生成新的饮品,而不是为每一种组合都创建一个新的咖啡类。
4. 网络通信框架:在网络请求、数据流处理等领域,可以通过装饰者模式动态地添加加密、压缩、日志记录等功能。
模板方法模式(Template Method Pattern)是一种行为设计模式,它定义了一个操作中的算法骨架,并允许子类在不改变结构的情况下重写某些步骤。该模式包含一个抽象类,这个抽象类中定义了算法的框架,并将一些具体步骤推迟到子类中实现。
优点:
1. 代码复用:模板方法封装了算法的通用部分,确保算法的整体结构不变,而将可变的细节留给子类去实现,提高了代码复用性。
2. 扩展性良好:通过继承的方式,子类可以专注于改变或添加特定步骤,而不是整个算法流程,从而支持开闭原则(Open/Closed Principle),即对扩展开放、对修改封闭。
3. 控制结构共享:父类可以强制规定算法的执行顺序,即使子类有不同的实现,整体流程依然保持一致。
缺点:
1. 继承层级过深:每增加一种新的变化,可能就需要创建一个新的子类,这可能导致类的数量过多,而且加深了类的继承层次,影响系统结构清晰度和维护性。
2. 灵活性受限:一旦某个步骤被声明为抽象方法,所有子类都必须提供其实现,对于那些不需要自定义此步骤的子类来说,可能会造成不必要的约束。
3. 违反单一职责原则:模板方法模式下,抽象类负责定义整个算法的框架,同时也包含了具体的操作步骤,这在一定程度上违背了单一职责原则,使得父类既要关注整体逻辑又要关注个别步骤的实现。
使用场景:
1. 算法框架固定,部分步骤可变:当一个类中有一系列步骤组成的方法,而其中一部分步骤的实现需要由子类来完成时,适合使用模板方法模式,例如游戏框架中的角色行动流程、数据库访问过程中的连接-操作-关闭等步骤。
2. 多个子类有公共方法:若多个子类之间存在重复代码,这些重复代码可以通过提取到父类中以模板方法的形式集中管理。
3. 框架设计:在软件开发框架中,模板方法模式常用于定义基本的框架流程,让开发者通过扩展子类来填充具体实现,如Servlet API中的doGet/doPost等方法就应用了模板方法模式的思想。
装饰者模式(Decorator Pattern)和代理模式(Proxy Pattern)在设计模式中都涉及到对原始对象功能的扩展或控制,但它们的目的、使用场景和实现方式有所不同:
装饰者模式:
1. 目的:装饰者模式用于在运行时动态地给一个对象添加新的行为或责任,而不会影响其他同类对象。它是通过创建一个新的装饰类,这个装饰类包装了原始对象,并提供了与原始对象一致的接口,同时可以在此基础上增加额外的功能。
2. 实现:装饰者通常会持有被装饰对象的引用,并在其方法中调用被装饰对象的方法,同时可能在前后添加额外的操作。
3. 特点:装饰者模式支持透明的对象增强,即客户端可以继续使用原始接口与装饰后的对象交互,无需知道具体的装饰细节。装饰者可以在不修改原有类的情况下,为对象添加多种装饰,实现功能的叠加。
4. 应用举例:Java IO库中的InputStream/OutputStream系列,通过装饰者BufferedInputStream,对基本输入输出流进行功能增强。
代理模式:
1. 目的:代理模式主要用于控制对目标对象的访问,它为另一个对象提供一个替身或者占位符以控制对这个对象的访问。代理可以在访问请求前后添加额外的业务逻辑,比如权限控制、日志记录、延迟加载、缓存、计算资源消耗等。
2. 实现:代理类同样也实现了与目标类相同的接口,但它并不单纯是为了增强功能,而是为了在调用真实对象前或后执行一些额外操作,或者是由于某些原因无法直接访问目标对象时作为中间层。
3. 特点:代理模式的核心是控制访问而非单纯的增强,它可以用来保护目标对象,限制直接访问,或者添加额外职责如事务管理、远程代理(RMI)等。
4. 应用举例:Spring AOP(面向切面编程)中,代理模式被广泛用于实现诸如事务处理、日志记录、性能监控等功能,代理类会在方法调用前后插入横切关注点的代码。
总结起来,装饰者模式关注的是扩展对象的行为或属性,保持接口的一致性,使功能像洋葱一样可层层包裹;而代理模式关注的是如何控制对对象的访问,在访问过程中附加额外的功能或控制流程。
软件设计模式是前辈们代码设计经验的总结,本质上是对面向对象设计原则的实际应用,是对类的封装、继承、多态以及类的关联关系与组合关系的充分理解,可以反复使用。设计模式共分为3大类,创建者模式(5种)、结构型模式(7种)、行为型模式(11种),一共23种设计模式,软件设计一般需要满足7大基本原则。
使用设计模式的代码可重用性与可维护性高、可读性强、可以减少软件开发周期,提升开发效率。