- 1、代理模式
- 1.1 静态代理
- 1.2 jdk 动态代理
- 1.3 CGLIB 动态代理
- 1.4 优缺点
- 2、适配器模式
- 2.1 类适配器模式
- 2.2 对象适配器模式
- 2.3 JDK 源码解析
- 3、装饰者模式
- 3.1 JDK 源码解析
- 3.2 代理模式与装饰者模式的区别
- 4、桥接模式
- 5、外观模式
- 6、组合模式
- 6.1 组合模式的分类
- 6.2 优点及使用场景
- 7、享元模式
- 7.1 优缺点和使用场景
- 7.2 JDK 源码分析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
- 代理模式
- 适配器模式
- 装饰者模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
1、代理模式
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK
代理和CGLib
代理两种。
代理(Proxy)模式分为三种角色:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
1.1 静态代理
【例】火车站卖票
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
/**
*
* Author: YZG
* Date: 2023/2/23 19:50
* Description: 抽象主题:定义具体主题和代理类实现的业务功能
*/
public interface SellTickets {
// 卖票
void sell();
}
/**
*
* Author: YZG
* Date: 2023/2/23 19:52
* Description: 代理类【代售点】:代理真实主题
*/
public class ProxyPoint implements SellTickets{
private TranStation tranStation = new TranStation();
@Override
public void sell() {
System.out.println("代售点卖票,并收取一些服务费用");
tranStation.sell();
}
}
/**
*
* Author: YZG
* Date: 2023/2/23 19:51
* Description: 具体主题【火车站】: 实现抽象主题的功能
*/
public class TranStation implements SellTickets{
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
//测试类
public class Client {
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint();
pp.sell();
}
}
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
1.2 jdk 动态代理
接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
/**
*
* Author: YZG
* Date: 2023/2/23 19:50
* Description: 抽象主题:定义具体主题和代理类实现的业务
*/
public interface SellTickets {
// 卖票
void sell();
}
/**
*
* Author: YZG
* Date: 2023/2/23 19:51
* Description: 具体主题【火车站】: 实现抽象主题的功能
*/
public class TranStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
/**
*
* Author: YZG
* Date: 2023/2/23 21:33
* Description:
*/
public class ProxyFactory {
// 被代理的对象:目标对象
private TranStation station = new TranStation();
// 获取代理对象
public SellTickets getObjectInstance(){
/*
* ClassLoader loader, 定义代理类的类加载器
* Class<?>[] interfaces, 实现的代理类的接口列表
* InvocationHandler h 调用代理对象的方法会被分派到此方法中
* */
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
* Object proxy, 调用该方法的代理实例 == sellTickets
* Method method, 代理实例调用的方法就是method,sellTickets调用了sell 方法,那么method就是 sell 方法。
* Object[] args 代理实例调用方法的参数。其实就是 sell 方法的参数
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 此处可以做一些代理对象的增强
System.out.println("代售点收取一些服务费【jdk】");
// 执行被代理对象的目标方法:sell方法
Object obj = method.invoke(station, args);
return obj;
}
}
);
return sellTickets;
}
}
/**
*
* Author: YZG
* Date: 2023/2/23 21:40
* Description: 客户端
*/
public class Client {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory();
SellTickets instance = factory.getObjectInstance();
instance.sell();
}
}
使用了动态代理,我们思考下面问题:
-
ProxyFactory是代理类吗?
ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:
package com.sun.proxy;
import com.itheima.proxy.dynamic.jdk.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
}
catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
public final boolean equals(Object object) {
try {
return (Boolean)this.h.invoke(this, m1, new Object[]{object});
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode() {
try {
return (Integer)this.h.invoke(this, m0, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final void sell() {
try {
this.h.invoke(this, m3, null);
return;
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
}
-
从上面的类中,我们可以看到以下几个信息:
- 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
- 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
-
动态代理的执行流程是什么样?
下面是摘取的重点代码:
//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
// 构造器,将我们自己写的 invocationHandler 传递给 Proxy 类中的 invocationHandler
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
}
// 代理类中的 sell 方法
public final void sell() {
// 调用了 jdk 中 Proxy类中 InvocationHandler 的 invoke 方法
// 而在我们的getProxyObject方法中重写了 invoke方法,因此实际上执行的是我们自己写的 invoke 方法。
this.h.invoke(this, m3, null);
}
}
//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
this.h = h;
}
}
//代理工厂类
public class ProxyFactory {
private TrainStation station = new TrainStation();
public SellTickets getProxyObject() {
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
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;
}
}
//测试访问类
public class Client {
public static void main(String[] args) {
//获取代理对象
ProxyFactory factory = new ProxyFactory();
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
执行流程如下:
- 在测试类中通过代理对象proxyObject调用sell()方法
- 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
- 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
- invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
1.3 CGLIB 动态代理
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
public class TranStation {
public void sell() {
System.out.println("火车站卖票");
}
}
public class ProxyFactory implements MethodInterceptor {
private TranStation station = new TranStation();
// 获取代理对象
public TranStation getObjectInstance() {
Enhancer enhancer = new Enhancer();
// 指定父类
enhancer.setSuperclass(TranStation.class);
// 设置回调函数
// MethodInterceptor 实现了 Callback接口
enhancer.setCallback(this);
// 创建代理对象
TranStation obj = (TranStation) enhancer.create();
return obj;
}
@Override
public TranStation intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("代售点收取一些服务费用【cglib】");
/*
intercept方法参数说明:
o : 代理对象
method : 真实对象中的方法的Method实例
args : 实际参数
methodProxy :代理对象中的方法的method实例
*/
TranStation invokeSuper = (TranStation) methodProxy.invokeSuper(o, objects);
return invokeSuper;
}
}
1.4 优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
2、适配器模式
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。
定义
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
类适配器模式使用继承的方式来兼容接口,因此耦合度较高
对象适配器模式使用组合/聚合 的方式兼容接口,耦合度较低
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承适配者类,实现目标接口,将不兼容的接口转换成目标接口。
2.1 类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口【SD接口】,同时又继承希望支持的业务组件【TF类】。
例子
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
类图如下:
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}
//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
//电脑类
public class Computer {
public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}
//TF卡实现类
public class TFCardImpl implements TFCard {
public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
//定义适配器类(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);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}
总结
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
如果没有 SDCard 接口,适配器又无法同时继承 SDCard 和 TFCardImpl, 因此就无法实现适配器模式
2.2 对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
使用对象适配器模式将读卡器的案例进行改写。类图如下:
/**
*
* Author: YZG
* Date: 2023/2/25 15:43
* Description: 适配器 : 使用对象适配器模式
*/
public class SDAdepterTF implements SDCard {
private TFCard tfCard;
public SDAdepterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
return tfCard.readTF();
}
@Override
public void writeSD(String msg) {
tfCard.writeTF(msg);
}
}
// 测试
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
String msg = computer.readSD(new SDCardImpl());
System.out.println(msg);
// 通过适配器模式,使电脑也能够读取 TFCard
// 创建适配器对象
SDAdepterTF adepterTF = new SDAdepterTF(new TFCardImpl());
String msg1 = computer.readSD(adepterTF);
System.out.println(msg1);
}
}
注意
还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
2.3 JDK 源码解析
Reader 是字符流的顶级父类
InputStream 是字节流的顶级父类
可以通过 InputStreamReader 可以实现字节输入流转换为字符输入流,其中就使用到了 适配器模式
// InputStreamReader 聚合了 StreamDecoder,继承了Reader
// 实际上 InputStreamReader 就是对 StreamDecoder的一个封装
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
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);
}
}
// 在 StreamDecoder 中继承了 Reader 同时聚合了 InputStream
// StreamDecoder 就是一个适配器,使用对象适配模式
public class StreamDecoder extends Reader {
private InputStream in;
}
他们之间的类图如下:
InputStream 可以认为一个组件,但是当前业务并不支持。通过 StreamDecoder 适配器将 InputStream 引入,同时继承业务所支持的Reader。
3、装饰者模式
我们先来看一个快餐店的例子。
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
传统的方式就是使用继承,使用继承的方式存在的问题:
-
扩展性不好
如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。
-
产生过多的子类
装饰者模式定义
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
装饰(Decorator)模式中的角色:
- 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。
类图如下:
/**
*
* Author: YZG
* Date: 2023/2/25 16:47
* Description: 抽象构件:快餐店
* * 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
* * 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
* * 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
* * 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
*/
public abstract class FastFood {
// 快餐本身价格
private float price;
// 快餐描述
private String desc;
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
public FastFood() {}
// 快餐总体价格:快餐+一些配料【培根、鸡蛋】
// 供子类重写
public abstract float cost();
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 16:55
* Description: 具体构件【炒面】: 实现抽象构件,通过装饰角色为其添加一些职责。
*/
public class FriedNoodles extends FastFood{
// 同样提供一个无参构造,调用父类的有参构造,进行赋值
public FriedNoodles() {
super(12, "炒面");
}
@Override
public float cost() {
return getPrice();
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 16:49
* Description: 具体构件【炒饭】: 实现抽象构件,通过装饰角色为其添加一些职责。
*/
public class FriedRice extends FastFood{
// 声明无参构造:调用父类的有参构造赋值。
public FriedRice() {
super(10,"炒饭");
}
// 炒饭价格
@Override
public float cost() {
// super
return getPrice();
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 16:56
* Description: 抽象装饰类:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
*/
public abstract class Garnish extends FastFood {
// 需要明确为哪个构建提供装饰,依赖于抽象类。
private FastFood fastFood;
public Garnish(float price, String desc, FastFood fastFood) {
super(price, desc);
this.fastFood = fastFood;
}
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 16:58
* Description: 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
*/
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(1, "鸡蛋", fastFood);
}
@Override
public float cost() {
// 鸡蛋价格 + 快餐价格 = 总价格
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
// 鸡蛋描述 + 快餐描述 = 总描述
return super.getDesc() + super.getFastFood().getDesc();
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 17:11
* Description: 具体装饰者类【培根】
*/
public class Bacon extends Garnish{
public Bacon( FastFood fastFood) {
super(2, "培根", fastFood);
}
@Override
public float cost() {
// 培根价格 + 快餐价格 = 总价格
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
// 培根描述 + 快餐描述 = 总描述
return super.getDesc() + super.getFastFood().getDesc();
}
}
/**
*
* Author: YZG
* Date: 2023/2/25 17:12
* Description: 测试
*/
public class Client {
public static void main(String[] args) {
// 点一份炒饭
FastFood fastFood = new FriedRice();
System.out.println(fastFood.getDesc() + " " + fastFood.cost() + "元");
System.out.println("==================");
// 炒饭加一个鸡蛋
fastFood = new Egg(fastFood);
System.out.println(fastFood.getDesc() + " " + fastFood.cost() + "元");
System.out.println("==================");
// 加一个培根
fastFood = new Bacon(fastFood);
System.out.println(fastFood.getDesc() + " " + fastFood.cost() + "元");
}
}
优点
- 装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
比如想要新增一个火腿配料,只需要创建一个火腿类继承抽象装饰者类即可。
或者想要增加一个菜品牛头面,只需要创建一个牛肉面类继承抽象构件即可。
- 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
使用场景
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
- 系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 类定义不能继承(如final类)
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当对象的功能要求可以动态地添加,也可以再动态地撤销时。
3.1 JDK 源码解析
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。
我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter
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();
}
}
使用起来感觉确实像是装饰者模式,接下来看它们的结构:
BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。
3.2 代理模式与装饰者模式的区别
静态代理和装饰者模式的区别:
- 相同点:
- 都要实现与目标类相同的业务接口
- 在代理类、抽象装饰者类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
- 不同点:
- 目的不同
装饰者是为了增强目标对象
静态代理是为了保护和隐藏目标对象 - 获取目标对象构建的地方不同
装饰者是由外界传递进来,可以通过构造方法传递
静态代理是在代理类内部创建,以此来隐藏目标对象
- 目的不同
4、桥接模式
定义:
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
例子
【例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
类图如下:
//视频文件
public interface VideoFile {
void decode(String fileName);
}
//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}
//rmvb文件
public class REVBBFile implements VideoFile {
public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}
//操作系统版本
public abstract class OperatingSystemVersion {
protected VideoFile videoFile;
public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}
public abstract void play(String fileName);
}
//Windows版本
public class Windows extends OperatingSystem {
public Windows(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
//mac版本
public class Mac extends OperatingSystemVersion {
public Mac(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
//测试类
public class Client {
public static void main(String[] args) {
OperatingSystem os = new Windows(new AVIFile());
os.play("战狼3");
}
}
优点
-
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。
-
实现细节对客户透明
使用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
5、外观模式
定义:
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则”的典型应用
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
例子
【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}
public void off() {
System.out.println("关闭了灯....");
}
}
//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}
public void off() {
System.out.println("关闭了电视....");
}
}
//控制类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}
public void off() {
System.out.println("关闭了空调....");
}
}
//智能音箱
public class SmartAppliancesFacade {
private Light light;
private TV tv;
private AirCondition airCondition;
public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}
public void say(String message) {
if(message.contains("打开")) {
on();
} else if(message.contains("关闭")) {
off();
} else {
System.out.println("我还听不懂你说的!!!");
}
}
//起床后一键开电器
private void on() {
System.out.println("起床了");
light.on();
tv.on();
airCondition.on();
}
//睡觉一键关电器
private void off() {
System.out.println("睡觉了");
light.off();
tv.off();
airCondition.off();
}
}
//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}
好处:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
使用场景
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
6、组合模式
概述
对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。
对于以上树形结构,最顶层的文件夹称为根节点,根节点下有若干个 树枝节【文件夹】点和叶子结点【文件】,其中树枝节点中还可有有若干个 树枝节点和 叶子结点。
定义:
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式主要包含三种角色:
- 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
- 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
例子
【例】软件菜单
如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。
对应的类图:
其中 MenuComponent 为抽象根节点,定义子节点的共有方法和属性。
Menu 为菜单,可能有多个子菜单或者子菜单项
MenuItem 为子菜单项,相当于叶子结点。
这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。
// 菜单组件: 抽象根节点,定义菜单共有的属性和方法
public abstract class MenuComponent {
// 将属性定义为 protected,方便子类进行赋值
// 菜单名
protected String name;
// 菜单级别
protected int level;
// 增加菜单: 可能是子菜单也可能是子菜单项。因此依赖于抽象类
public void add(MenuComponent component){
// 如果是菜单项,无法在增加或者删除,直接抛出异常。
throw new UnsupportedOperationException("菜单项不支持增加操作");
}
// 删除菜单
public void remove(MenuComponent component){
throw new UnsupportedOperationException("菜单项不支持删除操作");
}
// 通过索引获取菜单
public MenuComponent getMenu(int index){
throw new UnsupportedOperationException("菜单项不支持获取操作");
}
// 打印菜单名称或者菜单项名称
public String getName() {
return name;
}
// 打印菜单: 菜单与菜单项的打印方式不同,交给各自的类去重写
public abstract void print();
}
// 菜单: 看做是树枝节点
public class Menu extends MenuComponent{
// 集合里可能存储子菜单或者菜单项,因此直接聚合抽象类。
// 体现了依赖倒置原则:一个类不应该依赖于具体的类,而是依赖于接口或者抽象类。
private List<MenuComponent> components = new ArrayList<>();
// 创建菜单时,提供菜单名以及级别
public Menu(String name,int leve){
super.name = name;
super.level = leve;
}
// 增加菜单
@Override
public void add(MenuComponent component) {
components.add(component);
}
// 删除菜单
@Override
public void remove(MenuComponent component) {
components.remove(component);
}
// 获取菜单
@Override
public MenuComponent getMenu(int index) {
return components.get(index);
}
@Override
public void print() {
// 打印菜单,因为通过有参构造已经设置了菜单名,直接输出即可
System.out.println(name);
// component可能是菜单也可能是菜单项
// 如果是菜单,就调用 Menu 重写的 print 方法
// 如果是菜单项,就调用 MenuItem 重写的 print 方法
for (MenuComponent component : components) {
component.print();
}
}
}
// 菜单项:相当于叶子节点
public class MenuItem extends MenuComponent{
// 设置菜单项的名字以及级别
public MenuItem(String name,int level) {
super.name = name;
super.level = level;
}
/*
* 菜单项下没有子菜单或者子菜单项,无需增加、删除。
* */
@Override
public void print() {
System.out.println(name);
}
}
// 测试
public class Client {
public static void main(String[] args) {
// 创建菜单树
Menu menu = new Menu("菜单管理",2);
menu.add(new MenuItem("页面访问",3));
menu.add(new MenuItem("展开菜单",3));
Menu menu1 = new Menu("权限配置",2);
menu1.add(new MenuItem("页面访问",3));
menu1.add(new MenuItem("提交保存",3));
Menu menu2 = new Menu("角色管理",2);
menu2.add(new MenuItem("页面访问",3));
menu2.add(new MenuItem("新增角色",3));
// 创建根级菜单
Menu root = new Menu("系统管理",1);
root.add(menu);
root.add(menu1);
root.add(menu2);
// 由于菜单项并没有重写 add 方法,直接调用父类的 add,抛出异常。
// MenuItem menuItem = new MenuItem("1",1);
// menuItem.add(root);
root.print();
}
}
6.1 组合模式的分类
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式
和安全组合模式
两种形式。
-
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
MenuComponent
声明了add
、remove
、getChild
方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
-
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点
Menu
类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
6.2 优点及使用场景
优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
使用场景
组合模式正是应树形结构
而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
7、享元模式
定义:
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的
可共享部分
。 - 外部状态,指随环境改变而改变的
不可以共享的部分
。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
例子
【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
方块的形状是对象的内部状态,可以共享。同一个形状的方块颜色可能是不同的,颜色就是外部状态。不可以共享。
先来看类图:
代码实现
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
// 抽象享元角色
public abstract class AbstractBox {
// 获取图形
public abstract String getShape();
// 打印图形
public void display(String color){
System.out.println("方块的形状: " +getShape() + ", 方块的颜色: " + color);
}
}
接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
创建一个 享元工厂,只需要一个设置为单例模式,并创建一个map结构的享元池,用来保存享元对象。
// 享元工厂
public class BoxFactory {
// 享元池:保存具体的享元角色
private Map<String,AbstractBox> map ;
private BoxFactory() {
map = new HashMap<>();
// 创建享元角色
map.put("I",new IBox());
map.put("L",new LBox());
map.put("O",new OBox());
}
// 单例模式
private static final BoxFactory factory = new BoxFactory();
public static BoxFactory getInstance(){
return factory;
}
// 获取享元角色
public AbstractBox getShape(String name){
return map.get(name);
}
}
测试
从测试结果中也可以看出,相同形状的方块会重复利用同一个对象。达到复用的效果。
public class Client {
public static void main(String[] args) {
// 获取I图形
AbstractBox box = BoxFactory.getInstance().getShape("I");
box.display("红色");
AbstractBox box1 = BoxFactory.getInstance().getShape("L");
box1.display("绿色");
AbstractBox box2 = BoxFactory.getInstance().getShape("O");
box2.display("蓝色");
AbstractBox box3 = BoxFactory.getInstance().getShape("O");
box3.display("白色");
System.out.println("box2 和 box3 是否是同一个享元对象?" + (box3 == box2));
}
}
// 方块的形状: I, 方块的颜色: 红色
// 方块的形状: L, 方块的颜色: 绿色
// 方块的形状: O, 方块的颜色: 蓝色
// 方块的形状: O, 方块的颜色: 白色
// box2 和 box3 是否是同一个享元对象?true
7.1 优缺点和使用场景
1,优点
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
2,缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
3,使用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
7.2 JDK 源码分析
Integer类就使用了享元模式:
public class Demo {
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2)); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4)); // false
}
}
在使用自动装箱时,实际上会调用 Integer 的 valueof 方法,将 int ——》Integer
在 valueOf 方法中,就使用到了 享元池【IntegerCache】。当调用 valueOf
时如果参数在 -128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的 Integer
对象。
public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}