本教程尽量使用通俗易懂的话语来讲明白23种设计模式,其中生产型5种,结构型7种,行为型11种。
正式进入教学之前先看六大原则 总则是开闭原则
- 单一职责原则:每个类只实现单一的责任,注意责任不是方法,责任更加抽象,例如班主任有教学、管理、监督等责任,但是不止一种方法。
- 开闭原则:对扩展开发对修改关闭
- 里氏替换原则:在继承类时,务必重写(override)父类中所有的方法,尤其需要注意父类的protected方法(它们往往是让你重写的),子类尽量不要暴露自己的public方法供外界调用
- 迪米特法则:一个对象对其他对象得了解应该是最少得,最少知道法则
- 接口隔离原则:不要对外暴露没有实际意义的接口
- 依赖倒置原则:模块间的依赖通过抽象发生,实现类之间不直接发生依赖关系,其依赖关系是通过接口或抽象类产生的
一、生产者模式
1. 单例模式
饿汉式:类加载就会导致该单例被创建
懒汉式:类加载不会导致单例对象被创建,而是首次创建该对象才会加载
饿汉式1(静态变量方式):
声明静态变量创建,如果对象足够大并且一直未使用就会造成内存浪费
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
饿汉式2(静态代码块):
和静态变量基本一样,同样会内存浪费
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//静态代码块创建对象
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
懒汉式1(线程不安全):
懒加载,多线程的时候出现线程安全问题
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式2(线程安全):
添加synchronized关键字,效率低,初始化才会有线程安全问题
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式3(双重检查锁)
非常好的单例模式,在多线程的情况下,可能会出现空指针问题,出现问 题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关 键字可以保证可见性和有序性。
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//使用volatile解决空指针问题
//private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
懒汉式4(静态内部类):
优秀的单例模式,众多开源项目使用
代码语言:javascript复制public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
饿汉(枚举):
线程安全,只记载一次。最好的单例模式,不可能被破坏
代码语言:javascript复制public enum Singleton {
INSTANCE;
}
破坏单例模式:序列化反序列化(写入文件再读取),暴力反射(反射创建两个对象)(枚举无法破坏)
序列化解决方案:添加readResolve() 方法直接返回单例对象,在反序列化时会判断是否有这个方法,有就直接返回对象,没有就反序列化
反射解决方案:添加代码(构造方法中判断对象不为空则抛出异常),反射时会抛出该异常
JDK源码
Runtime采用标准的饿汉式(静态属性)实现单例
代码语言:javascript复制public class Runtime {
private static final Runtime currentRuntime = new Runtime();
}
2. 工厂方法模式
工厂模式:工厂生产对象,从工厂拿对象,解耦
结构:抽象产品,具体产品,具体工厂
spring大量使用工厂方法,核心类为BeanFactory
简单工厂模式:
一种编程习惯,增加新产品违背开闭原则
代码语言:javascript复制public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
静态工厂:
将简单工厂定义为静态即可,实现懒加载
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。知道具体工厂名称即可获取产品,但是增加了系统复杂度
结构:抽象产品,具体产品,抽象工厂,具体工厂
代码语言:javascript复制//抽象工厂
public interface CoffeeFactory {
Coffee createCoffee();
}
//具体工厂
public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
3. 抽象工厂模式
就是对工厂分类减少类爆炸
使用场景:
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空 调等。
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。 如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。
简单工厂 配置文件解除耦合(简易IOC)
可以通过工厂模式 配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。
代码语言:javascript复制//配置文件bean.properties
american=com.itheima.pattern.factory.config_factory.AmericanCoffee
latte=com.itheima.pattern.factory.config_factory.LatteCoffee
//工厂类
public class CoffeeFactory {
//定义容器对象存储咖啡对象
private static Map<String,Coffee> map = new HashMap();
//加载配置文件
static {
//创建peoperties对象
Properties p = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader()
.getResourceAsStream("bean.properties");
try {
// 反射获取对象
p.load(is);
//遍历Properties集合对象
Set<Object> keys = p.keySet();
for (Object key : keys) {
//根据键获取值(全类名)
String className = p.getProperty((String) key);
//获取字节码对象
Class clazz = Class.forName(className);
//调用无参构造方法
Coffee obj = (Coffee) clazz.newInstance();
//将名称和对象放入容器中
map.put((String)key,obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Coffee createCoffee(String name) {
return map.get(name);
}
}
静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及 创建对象写在静态代码块中,目的就是只需要执行一次。
JDK源码
Collection接口中的iterator方法使用的是工厂模式
DateForamt类中的getInstance()方法使用的是工厂模式
Calendar类中的getInstance()方法使用的是工厂模式
4. 原型模式
按照原型复制出一个对象
角色:抽象原型类(规定具体原型必须实现clone方法),具体原型类(实现clone是可被复制),访问类(使用具体原型的clone方法复制新对象)
使用场景:对象创建的复杂度高,性能和安全要求比较高
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。(浅克隆的引用对象是克隆地址,深克隆是创建新对象)
JDK源码
Java中的Object类中提供了 clone() 方法来实现浅克隆。 Cloneable 接口是抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。
代码语言:javascript复制public class Realizetype implements Cloneable {
public Realizetype() {
System.out.println("具体的原型对象创建完成!");
}
@Override
protected Realizetype clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}
深克隆使用序列化和反序列化克隆(破环单例),注意:具体原型类和引用对象类必须实现Serializable接口,否则会抛 NotSerializableException异常。
5. 建造者模式
当一个类的构造方法含有大量属性的时候传统的构造函数可读性不高,使用建造者可以提高可读性
模式扩展:利用建造者模式进行代码重构,增强可读性,
代码语言:javascript复制// 构造函数需要一个static final修饰的内部类,内部类通过set方法增强可读性
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
private Phone(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder() {}
public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Phone build() {
return new Phone(this);}
}
@Override
public String toString() {
return "Phone{"
"cpu='" cpu '''
", screen='" screen '''
", memory='" memory '''
", mainboard='" mainboard '''
'}';
}
}
public class Client {
public static void main(String[] args) {
//重构前可读性不高 其实这个方法没什么屌用,idea会显示参数含义
//Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
Phone phone = new Phone.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(phone);
}
}
二、结构型模式
1. 代理模式
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接 引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。大白话就是静态代理是new的,动态代理是反射
结构:抽象主题类、真实主题类、代理类
静态代理
代码语言:javascript复制//火车票代售点代理火车站售票,内部调用真实主题类
public class ProxyPoint implements SellTickets {
//声明目标对象
private TrainStation station = new TrainStation();
public void sell() {
System.out.println("代理点收取一些服务费用");
//返回代理对象
station.sell();
}
}
JDK动态代理
Java中提供了一个动态代理类 Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法 (newProxyInstance方法)来获取代理对象。JDK动态代理必须有接口,否则无法代理成功
动态代理往往和工厂模式结合使用,在工厂中通过代理来实现对象的创建
代码语言:javascript复制public class ProxyFactory {
private TrainStation station = new TrainStation();
public SellTickets getProxyObject() {
//使用Proxy(普通对象)获取代理对象
/*
newProxyInstance()方法参数说明:
ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
InvocationHandler h : 代理对象的调用处理程序
*/
SellTickets sellTickets = (SellTickets)Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
// 这里采用匿名内部类实现jdk动态代理
new InvocationHandler() {
/*
InvocationHandler中invoke方法参数说明:
proxy : 代理对象
method : 对应于在代理对象上调用的接口方法的 Method 实例
args : 代理对象调用接口方法时传递的实际参数
*/
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
System.out.println("对普通对象做增强(JDK动态代理方式)");
//执行原始方法,也就是普通对象的方法
Object result = method.invoke(station, args);
return result;
}
});
return sellTickets;
}
}
注意:ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。
代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
代码语言:javascript复制//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
//传递给父类,其实就是我们自己实现的匿名内部类invocationHandler
super(invocationHandler);
}
static {
//m3拿到具体类
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").
getMethod("sell", new Class[0]);
}
public final void sell() {
//invoke就是我们书写的invocationHandler的invok方法
this.h.invoke(this, m3, null);
}
}
//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
this.h = h;
}
}
执行流程:
1、在测试类中通过代理对象调用sell()方法
2、根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
3、代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的 invoke方法
4、invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
CGLIB代理
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提 供了很好的补充。 CGLIB是第三方提供的包,所以需要引入jar包的坐标:
代码语言:javascript复制<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
代码语言:javascript复制//CGLIB代理工厂
public class ProxyFactory implements MethodInterceptor {
private TrainStation target = new TrainStation();
public TrainStation getProxyObject() {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer =new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(target.getClass());
//设置回调函数
enhancer.setCallback(this);
//创建代理对象
TrainStation obj = (TrainStation) enhancer.create();
return obj;
}
/*
intercept方法参数说明:
o : 代理对象
method : 真实对象中的方法的Method实例
args : 实际参数
methodProxy :代理对象中的方法的method实例
*/
public TrainStation intercept(Object o, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
System.out.println("增强实现(CGLIB动态代理方式)");
TrainStation result = (TrainStation) methodProxy.invokeSuper(o,args);
return result;
}
}
三种代理的对比
jdk代理和CGLIB代理:CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在 JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。 在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率 低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以有接口使用JDK动态代理,没有接口使用CGLIB代理。
动态代理和静态代理:动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实 现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
使用场景
远程(Remote)代理: 本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中 可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一 个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
防火墙(Firewall)代理: 当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响 应时,代理服务器再把它转给你的浏览器。
保护(Protect or Access)代理: 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
2. 适配器模式
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能 一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现 有组件库中的相关组件的内部结构,所以应用相对较少些。
类适配器模式
这里只是展示了实现得一种思想,具体如何适配,要看具体场景,
代码语言:javascript复制//定义适配器类(SD兼容TF),实现了目标接口,继承了适配者类
public class SDAdapterTF extends TFCardImpl implements SDCard {
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
对象适配器模式
讲类适配继承得对象作为内部属性使用
代码语言:javascript复制//创建适配器对象(SD兼容TF)
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创 建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
适用场景:以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
JDK源码
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。 InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
代码语言:javascript复制//sd(StreamDecoder类对象),在Sun的JDK实现中,
//实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论: 从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器 模式。
spring中得适配器
springmvc在DispatcherServlet中的doDispatch方法,是将请求分发到具体的controller,因为存在很多不同类型的controller,所以使用了适配器
AOP中得不同通知对外使用统一接口,使用了适配器
3. 装饰者模式
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象 来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭 原则,继承是静态的附加责任,装饰者则是动态的附加责任。 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以 动态扩展一个实现类的功能。
使用场景
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目 呈爆炸性增长;
第二类是因为类定义不能继承(如final类)
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
JDK源码
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream, BufferedReader,BufferedWriter。
代码语言:javascript复制//BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。
public class Demo {
public static void main(String[] args) throws Exception{
//创建BufferedWriter对象
//创建FileWriter对象
FileWriter fw = new FileWriter("C:\Users\Think\Desktop\a.txt");
BufferedWriter bw = new BufferedWriter(fw);
//写数据
bw.write("hello Buffered");
bw.close();
}
}
代理和装饰者的区别
相同点: 都要实现与目标类相同的业务接口 在两个类中都要声明目标对象 都可以在不修改目标类的前提下增强目标方法
不同点: 目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象 获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理 是在代理类内部创建,以此来隐藏目标对象
4. 桥接模式
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实 现这两个可变维度的耦合度。
假设你正在开发一个图形绘制应用,需要绘制不同类型的形状(如圆形、矩形)并选择不同的颜色(如红色、蓝色)。你希望能够在不修改已有代码的情况下,轻松地扩展支持新的形状和颜色组合。
首先,我们定义两个层次结构:形状层次和颜色层次。
形状层次结构:
代码语言:javascript复制 codepublic interface Shape {
void draw();
}
public class Circle implements Shape {
private Color color;
public Circle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Draw a circle ");
color.fill();
}
}
public class Rectangle implements Shape {
private Color color;
public Rectangle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Draw a rectangle ");
color.fill();
}
}
颜色层次结构:
代码语言:javascript复制javaCopy codepublic interface Color {
void fill();
}
public class Red implements Color {
@Override
public void fill() {
System.out.println("with red color.");
}
}
public class Blue implements Color {
@Override
public void fill() {
System.out.println("with blue color.");
}
}
接下来,我们使用桥接模式将形状和颜色连接起来:
代码语言:javascript复制javaCopy codepublic class Main {
public static void main(String[] args) {
Shape redCircle = new Circle(new Red());
redCircle.draw();
Shape blueRectangle = new Rectangle(new Blue());
blueRectangle.draw();
}
}
在这个例子中,Circle
和 Rectangle
是形状类,Red
和 Blue
是颜色类。通过组合,我们在 Circle
和 Rectangle
类的构造函数中将颜色对象传递进去,从而将形状和颜色连接起来。这使得我们可以轻松地扩展形状和颜色的组合,而不需要修改已有代码。
桥接模式允许我们将抽象和实现分离,使系统更加灵活和可扩展。它特别适用于那些需要在两个或多个维度上进行变化的情况,例如本例中的形状和颜色。
使用场景: 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间 建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
5. 外观模式
提供了一个统一的接口,用于访问一个复杂系统中的一组接口。外观模式通过创建一个简化的接口,隐藏了系统内部的复杂性,使得客户端可以更容易地与系统交互。
下面是一个具体的外观模式示例,假设你正在开发一个家庭影院系统,其中包括多个子系统,如投影仪、音响和灯光等。为了让用户更方便地使用这些设备,你可以使用外观模式来提供一个统一的接口,使用户只需与一个接口交互,而不需要直接与各个子系统交互。
首先,我们定义多个子系统的类:
代码语言:javascript复制 codeclass Projector {
void turnOn() {
System.out.println("Projector is on");
}
void turnOff() {
System.out.println("Projector is off");
}
}
class SoundSystem {
void turnOn() {
System.out.println("Sound System is on");
}
void turnOff() {
System.out.println("Sound System is off");
}
}
class Lights {
void dim() {
System.out.println("Lights are dimmed");
}
void brighten() {
System.out.println("Lights are brightened");
}
}
然后,我们创建一个外观类 HomeTheaterFacade
,该类提供了一个简化的接口,用于控制整个家庭影院系统:
codeclass HomeTheaterFacade {
private Projector projector;
private SoundSystem soundSystem;
private Lights lights;
HomeTheaterFacade(Projector projector, SoundSystem soundSystem, Lights lights) {
this.projector = projector;
this.soundSystem = soundSystem;
this.lights = lights;
}
void watchMovie() {
projector.turnOn();
soundSystem.turnOn();
lights.dim();
System.out.println("Get ready to watch the movie!");
}
void endMovie() {
projector.turnOff();
soundSystem.turnOff();
lights.brighten();
System.out.println("Movie is over");
}
}
最后,我们在客户端使用外观类来控制家庭影院系统,而不需要直接操作各个子系统:
代码语言:javascript复制javaCopy codepublic class Main {
public static void main(String[] args) {
Projector projector = new Projector();
SoundSystem soundSystem = new SoundSystem();
Lights lights = new Lights();
HomeTheaterFacade homeTheater = new HomeTheaterFacade(projector, soundSystem, lights);
homeTheater.watchMovie();
System.out.println("-------------------------------");
homeTheater.endMovie();
}
}
在这个示例中,HomeTheaterFacade
提供了简化的接口来控制家庭影院系统的各个子系统。通过使用外观模式,客户端可以更方便地操作整个系统,而不需要了解每个子系统的细节。外观模式帮助简化了客户端与复杂系统之间的交互,同时也提高了系统的可维护性。其实外观就是简单的组合而已。
tomcat源码
使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成 ServletRequest对象,但是大家想想ServletRequest是一个接口,它还有一个 子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象 的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为 RequestFacade的类的对象。
6. 组合模式
假设你正在开发一个文件系统应用,需要管理文件和文件夹。文件夹可以包含文件和其他文件夹,形成一个树状结构。你希望能够以统一的方式处理文件和文件夹,例如计算文件系统中所有元素的总大小。
首先,我们定义一个通用的抽象组件接口 FileSystemElement
,它包括文件和文件夹的共同操作:
javaCopy codeinterface FileSystemElement {
int getSize();
}
然后,我们实现两个具体的类,一个是文件 File
,另一个是文件夹 Folder
:
javaCopy codeclass File implements FileSystemElement {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public int getSize() {
return size;
}
}
class Folder implements FileSystemElement {
private String name;
private List<FileSystemElement> elements = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addElement(FileSystemElement element) {
elements.add(element);
}
public int getSize() {
int totalSize = 0;
for (FileSystemElement element : elements) {
totalSize = element.getSize();
}
return totalSize;
}
}
在这个例子中,File
和 Folder
都实现了 FileSystemElement
接口,使得它们可以在统一的接口下被处理。Folder
可以包含多个子元素,包括其他文件夹和文件。
现在,我们可以使用组合模式来创建一个文件系统并计算总大小:
代码语言:javascript复制javaCopy codepublic class Main {
public static void main(String[] args) {
File file1 = new File("file1.txt", 100);
File file2 = new File("file2.txt", 50);
Folder folder1 = new Folder("folder1");
folder1.addElement(file1);
folder1.addElement(file2);
File file3 = new File("file3.txt", 75);
Folder rootFolder = new Folder("root");
rootFolder.addElement(folder1);
rootFolder.addElement(file3);
int totalSize = rootFolder.getSize();
System.out.println("Total size of the file system: " totalSize);
}
}
透明组合模式:抽象根节点角色中声明了所有用于管理成员对象的方法,这样做的好处是确保所有的构件 类都有相同的接口。透明组合模式也是组合模式的标准形式。 透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能 有下一个层次的对象,即不可能包含成员对象,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应 的错误处理代码)
安全组合模式: 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具 有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端 不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
使用场景:组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显 示,多级目录呈现等树形结构数据的操作。
7. 享元模式
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建 的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两 种状态,并将外部状态外部化。
享元模式通常配合工厂模式使用,在工厂中保存内部状态,查询到已经存在的状态了就直接返回,
JDK源码
Integer类使用了享元模式。给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf() , Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。
三、行为型模式
1. 模板方法模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情 况下重定义该算法的某些特定步骤。
笔者遇见过两种实现
抽象类
在抽象类中将模板写为具体方法,而动态修改的地方设置为空方法,最好是protect级别,具体的实现子类继承即可。
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽 象。 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构, 它提高了代码阅读的难度。
JDK源码
InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,
同样AQS也使用了模板方法,子类可以自己实现一些扩展方法。
代码语言:javascript复制public abstract class InputStream implements Closeable {
//抽象方法,要求子类必须重写
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
for (; i < len ; i ) {
c = read();
if (c == -1) {
break;
}
b[off i] = (byte)c;
}
return i;
}
}
从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的 方法。 在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。
总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节, 并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实 现。
2. 策略模式
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用 算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分 割开来,并委派给不同的对象对这些算法进行管理。
使用场景
一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条 件分支移入它们各自的策略类中以代替这些条件语句。
系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
JDK源码
Comparator 中的策略模式。在Arrays类中有一个 sort() 方法,如下:
代码语言:javascript复制public class Arrays{
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
}
Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。 就比如下面的测试类。
代码语言:javascript复制public class demo {
public static void main(String[] args) {
Integer[] data = {12, 2, 3, 2, 4, 5, 1};
// 实现降序排序
Arrays.sort(data, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
}
}
这里我们在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以 Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类 (Arrays)应该持有抽象策略的引用来调用。那么,Arrays类的sort方法到底有没有使用 Comparator子实现类中的 compare() 方法吗?让我们继续查看TimSort类的 sort() 方法,代 码如下:
代码语言:javascript复制class TimSort<T> {
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo initRunLen, c);
return;
}
...
}
private static <T> int countRunAndMakeAscending(T[] a, int lo, inthi,
Comparator<? super T> c) {
assert lo < hi;
int runHi = lo 1;
if (runHi == hi)
return 1;
// Find end of run, and reverse range if descending
if (c.compare(a[runHi ], a[lo]) < 0) { // Descending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
runHi ;
reverseRange(a, lo, runHi);
} else { // Ascending
while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
runHi ;
}
return runHi - lo;
}
}
上面的代码中最终会跑到 countRunAndMakeAscending() 这个方法中。我们可以看见,只用了 compare方法,所以在调用Arrays.sort方法只传具体compare重写方法的类对象就行,这也是 Comparator接口中必须要子类实现的一个方法。
3. 命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象 进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
结构
抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调 用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收 者,只要它能够实现命令要求实现的相应功能。
调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很 多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用 命令对象的入口。