结构型模式描述如何将类或对象按某种布局组成更大的结构。
它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
代理模式
适配器模式
装饰者模式
桥接模式
外观模式
组合模式
享元模式
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理
和动态代理
。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理
和CGLib代理
两种。
代理(Proxy)模式分为三种角色:
我们通过案例来感受一下静态代理。
【例】火车站卖票
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
代码如下:
// 卖票接口
public interface SellTickets {
void sell();
}
// 火车站类(目标类、被代理类)
public class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代售点类(代理类)
public class ProxyPoint implements SellTickets {
private TrainStation trainStation = new TrainStation();
@Override
public void sell() {
System.out.println("代售点收取一些服务费(静态代理)");
trainStation.sell();
}
}
// 测试类
public class Client {
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
proxyPoint.sell();
}
}
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
动态代理最主要的核心功能是在不修改源码额基础上,对逻辑进行增强。
接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类Proxy
,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代码如下:
// 卖票接口
public interface SellTickets {
void sell();
}
// 火车站类
public class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 代理工厂类
public class ProxyFactory {
// 声明目标对象
private TrainStation trainStation = new TrainStation();
// 返回代理对象(代理类也实现了对应的接口)
public SellTickets getProxyObject() {
/*
因为jdk动态代理是基于接口的,所以代理类必须和目标类实现相同的接口interfaces,类加载器loader用于JVM动态创建代理对象,InvocationHandler方法就是需要书写的额外功能
ClassLoader loader:类加载器,用于加载代理类。可以通过目标对象的字节码对象获取类加载器对象
Class>[] interfaces:代理类实现的接口的字节码对象,底层让生成的代理类实现该接口
InvocationHandler h:代理对象的调用处理程序
*/
SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
trainStation.getClass().getClassLoader(),
trainStation.getClass().getInterfaces(),
new InvocationHandler() {
/*
代理对象执行的方法(new InvocationHandler().invoke())
Object proxy:代理对象,和proxyObject是同一个对象
Method method:对接口中的方法进行封装的method,代理对象调用哪个方法,method就是哪个方法
Object[] args:调用方法的实际参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点收取一定的服务费(jdk动态代理)"); // 代理方法的增强
Object obj = method.invoke(trainStation, args); // method对象调用执行目标对象的方法
return obj; // method对象方法的返回值
}
}
);
return proxyObject;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 通过代理对象工厂来获取代理对象
ProxyFactory factory = new ProxyFactory();
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
使用了动态代理,我们思考下面问题:
阿里巴巴开源的 Java 诊断工具
(Arthas 阿尔萨斯
)查看代理类的结构:public class Client {
public static void main(String[] args) {
// 通过代理对象工厂来获取代理对象
ProxyFactory factory = new ProxyFactory();
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
// 先把代理对象的字节码对象的名称打印出来
System.out.println(proxyObject.getClass());
// 再让程序不要停一直执行,这样运行过程中动态在内存中生成的类就不会被释放
while (true);
// 运行测试类,使用Java诊断工具Arthas进行分析
// java -jar arthas-boot.jar,选择对应的Java程序
// jad com.sun.proxy.$Proxy0
}
}
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的重点代码:程序运行过程中,内存中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
}
public final void sell() {
this.h.invoke(this, m3, null);
return;
}
}
// Java提供的动态代理相关类
public class Proxy {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
this.h = h;
}
}
// InvocationHandler接口
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
// 代理工厂类
public class ProxyFactory {
private TrainStation trainStation = new TrainStation();
public SellTickets getProxyObject() {
// Proxy.newProxyInstance():创建并返回代理类的对象,并设置代理的方法
SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
trainStation.getClass().getClassLoader(),
trainStation.getClass().getInterfaces(),
// 以匿名内部类的方式创建InvocationHandler接口的子实现类对象
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点收取一定的服务费(jdk动态代理)");
Object obj = method.invoke(trainStation, args);
return obj;
}
}
);
return proxyObject;
}
}
// 测试访问类
public class Client {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory();
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
执行流程如下:
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标(导入pom.xml文件):
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.aizengroupId>
<artifactId>DesignPatternsartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.2.12version>
dependency>
dependencies>
project>
代码如下:
// 火车站类
public class TrainStation {
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理对象工厂,用来获取代理对象(目标对象的子类)
public class ProxyFactory implements MethodInterceptor {
// 声明目标对象
private TrainStation trainStation = new TrainStation();
public TrainStation getProxyObject() {
// 创建增强器Enhancer对象,类似于JDK代理中的Proxy类
Enhancer enhancer = new Enhancer();
// 设置父类的字节码对象,因为cglib的代理对象是属于目标对象的子类对象
enhancer.setSuperclass(TrainStation.class);
// 设置回调函数,参数为MethodInterceptor接口的子实现类对象。MethodInterceptor是Callback的子接口,所以回调函数可以放this
enhancer.setCallback(this);
// 创建代理对象
TrainStation proxyObject = (TrainStation) enhancer.create();
return proxyObject;
}
// MethodInterceptor中的intercept方法,在代理对象调用目标对象的方法时执行
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("代售点收取一定的服务费用(CGLib代理)"); // 增强
Object obj = null;
// 可以使用getName方法来指定想要增强的特定方法名
if ("sell".equals(method.getName())) {
obj = method.invoke(trainStation, objects);
}//else if...
return obj;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
// 获取代理工厂对象
TrainStation proxyObject = factory.getProxyObject();
// 调用代理对象中的sell方法进行卖票
proxyObject.sell();
}
}
jdk代理和CGLIB代理
动态代理和静态代理
优点:
保护
:代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;增强
:代理对象可以扩展目标对象的功能,不修改原来代码的功能,可以增加新的功能,如记录系统日志、结果封装等;解耦
:代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;缺点:
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器,转接头,拓展坞等,其实就是使用到了适配器模式。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者(继承实现)类之间的耦合度比后者(聚合、组合实现)高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
适配器模式(Adapter)包含以下主要角色:
实现方式:定义一个适配器类来实现当前系统的业务接口(目标接口),同时又继承现有组件库中已经存在的组件(适配者类)。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
代码如下:
// 目标接口
public interface SDCard {
// 从SD卡中读取数据
String readSD();
// 往SD卡中写数据
void writeSD(String msg);
}
// 目标类,具体的SD卡
public class SDCardImpl implements SDCard {
@Override
public String readSD() {
String msg = "SDCard read msg: Hello SDCard";
return msg;
}
@Override
public void writeSD(String msg) {
System.out.println("SDCard write msg: " + msg);
}
}
// 计算机类
public class Computer {
// 从SD卡中读取数据
public String readSD(SDCard sdCard) {
if (sdCard == null) {
throw new NullPointerException("SD card cannot be empty!");
}
return sdCard.readSD();
}
}
// 适配者类的接口
public interface TFCard {
// 从TF卡中读取数据
String readTF();
// 往TF卡中写数据
void writeTF(String msg);
}
// 适配者类
public class TFCardImpl implements TFCard {
@Override
public String readTF() {
String msg = "TFCard read msg: Hello TFCard";
return msg;
}
@Override
public void writeTF(String msg) {
System.out.println("TFCard write msg: " + msg);
}
}
// 适配器类:定义一个适配器类来实现当前系统的业务接口(目标接口),同时又继承现有组件库中已经存在的组件(适配者类)
public class SDAdapterTF extends TFCardImpl implements SDCard {
@Override
public String readSD() {
System.out.println("adapter read TF card");
return super.readTF();
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write TF card");
super.writeTF(msg);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msgFromSD = computer.readSD(new SDCardImpl());
System.out.println(msgFromSD);
System.out.println("====================");
// 使用该电脑读取TF卡中的数据
// 定义适配器类
String msgFromTF = computer.readSD(new SDAdapterTF());
System.out.println(msgFromTF);
}
}
类适配器模式
违背了合成复用原则(优先使用成员变量聚合或组合,而非继承)
。类适配器是客户类有一个接口规范的情况下可用,反之不可用(继承也不能,因为Java不支持多继承)。
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
// 适配器类:定义一个适配器类来实现当前系统的业务接口(目标接口),同时又继承现有组件库中已经存在的组件(适配者类)
public class SDAdapterTF implements SDCard {
// 声明适配者类
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
System.out.println("adapter read TF card");
return tfCard.readTF();
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write TF card");
tfCard.writeTF(msg);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msgFromSD = computer.readSD(new SDCardImpl());
System.out.println(msgFromSD);
System.out.println("====================");
// 使用该电脑读取TF卡中的数据
// 定义适配器类
String msgFromTF = computer.readSD(new SDAdapterTF(new TFCardImpl()));
System.out.println(msgFromTF);
}
}
对象适配器模式满足合成复用原则,并且对于适配者类没有接口规范的情况下也可用。
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter去实现该接口,重写所有抽象方法,只不过没有具体实现体。如果我们只想用接口中的某一个方法,只需要去继承抽象类Adapter,而此时我们想用哪个方法就去重写哪个方法即可。
这种模式主要在早期版本的java中由于没有默认方法,所以要写大量接口适配器。
字符流Reader(字符输入流顶层父类)、字节流InputStream(字节输入流顶层父类)的适配使用的是InputStreamReader,将字节数据转换成字符数据。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
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);
}
如上代码中的sd(流解码器StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
从上图可以看出:
解码:将字节数据转为字符数据
编码:将字符数据转为字节数据
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder
的设计实现在实际上采用了对象适配器模式
。
我们先来看一个快餐店的例子。
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
使用继承的方式存在的问题:
扩展性不好
产生过多的子类
定义:
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
装饰(Decorator)模式中的角色:
我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。
代码如下:
// 快餐店:抽象构件角色(Component)
public abstract class FastFood {
private float price; // 价格
private String desc; // 描述
public FastFood() {
}
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
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;
}
public abstract float cost();
}
// 炒饭:具体构件角色(Concrete Component)
public class FriedRice extends FastFood {
public FriedRice() {
super(10, "炒饭");
}
@Override
public float cost() {
return getPrice();
}
}
// 炒面:具体构件角色(Concrete Component)
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(12, "炒面");
}
@Override
public float cost() {
return getPrice();
}
}
// 配料类:抽象装饰类(Decorator)
public abstract class Garnish extends FastFood {
// 抽象装饰类聚合抽象构件角色,声明快餐类的变量
private FastFood fastFood;
public Garnish(FastFood fastFood) {
this.fastFood = fastFood;
}
public Garnish(FastFood fastFood, float price, String desc) {
super(price, desc);
this.fastFood = fastFood;
}
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
}
// 鸡蛋配料类:具体装饰角色(Concrete Decorator)
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(fastFood, 1, "鸡蛋");
}
@Override
public float cost() {
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
// 培根配料类:具体装饰角色(Concrete Decorator)
public class Bacon extends Garnish {
public Bacon(FastFood fastFood) {
super(fastFood, 2, "培根");
}
@Override
public float cost() {
return getPrice() + getFastFood().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 点一份炒饭
FastFood food = new FriedRice();
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===========================");
// 在上面的炒饭中加一个鸡蛋
food = new Egg(food);
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===========================");
// 再加两个鸡蛋
food = new Egg(new Egg(food));
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===========================");
// 再点一份培根
food = new Bacon(food);
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("===========================");
// 点一份培根+炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
System.out.println("===========================");
}
}
好处:
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\\Aizen\\Desktop\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);
// 写数据
bw.write("hello Buffered");
bw.close();
}
}
小结:
BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。
静态代理和装饰者模式的区别:
现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系:
我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。
试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸
,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式
。
定义:
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接(Bridge)模式包含以下主要角色:
【例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统
平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式
包括RMVB、AVI、WMV等。该播放器包含了两个维度
,适合使用桥接模式。
代码如下:
// 视频文件接口:实现化角色(Implementor)
public interface VideoFile {
// 解码功能
void decode(String fileName);
}
// AVI视频文件类:具体实现化角色(Concrete Implementor)
public class AVIFile implements VideoFile {
@Override
public void decode(String fileName) {
System.out.println("AVI视频文件:" + fileName);
}
}
// RMVB视频文件类:具体实现化角色(Concrete Implementor)
public class RMVBFile implements VideoFile {
@Override
public void decode(String fileName) {
System.out.println("RMVB视频文件:" + fileName);
}
}
// 操作系统版本类:抽象化角色(Abstraction)
public abstract class OperatingSystem {
// 聚合实现化角色,声明VideoFile对象
protected VideoFile videoFile;
public OperatingSystem(VideoFile videoFile) {
this.videoFile = videoFile;
}
// 播放视频文件的抽象方法
public abstract void play(String fileName);
}
// Windows操作系统版本类:扩展抽象化角色(Refined Abstraction)
public class Windows extends OperatingSystem {
public Windows(VideoFile videoFile) {
super(videoFile);
}
@Override
public void play(String fileName) {
System.out.println("Windows OS下播放:");
videoFile.decode(fileName);
}
}
// Mac操作系统版本类:扩展抽象化角色(Refined Abstraction)
public class Mac extends OperatingSystem {
public Mac(VideoFile videoFile) {
super(videoFile);
}
@Override
public void play(String fileName) {
System.out.println("Mac OS下播放:");
videoFile.decode(fileName);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建Mac系统对象
OperatingSystem os = new Mac(new AVIFile());
// 使用操作系统播放AVI视频文件
os.play("BLEACH");
System.out.println("=======================");
// 创建Mac系统对象
os = new Mac(new RMVBFile());
// 使用操作系统播放RMVB视频文件
os.play("BLEACH");
System.out.println("=======================");
// 创建Windows系统对象
os = new Windows(new AVIFile());
// 使用操作系统播放AVI视频文件
os.play("BLEACH");
System.out.println("=======================");
// 创建Windows系统对象
os = new Windows(new RMVBFile());
// 使用操作系统播放RMVB视频文件
os.play("BLEACH");
}
}
好处:
有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。
定义:
又名门面模式
,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则
”的典型应用
外观(Facade)模式包含以下主要角色:
【例】智能家电控制
小明的爷爷已经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() {
light.on();
tv.on();
airCondition.on();
}
// 一键关闭功能
private void off() {
light.off();
tv.off();
airCondition.off();
}
}
// 客户端类
public class Client {
public static void main(String[] args) {
// 创建智能音箱对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
// 控制家电
facade.say("打开家电");
System.out.println("=================");
facade.say("请关闭所有家电");
}
}
好处:
缺点:
使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。
为什么在此处使用外观模式呢?
定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。
对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象可以是容器对象(文件夹)也可以是叶子对象(文件)。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。
定义:
又名部分整体模式
,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分
以及整体
层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构
。
组合模式主要包含三种角色:
【例】软件菜单
如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。
代码实现:
不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。
// 菜单组件:抽象根节点,菜单类和菜单项类都继承该类
public abstract class MenuComponent {
// 菜单组件的名称
protected String name;
// 菜单组件的层级
protected int level;
// 添加子菜单(菜单或菜单项),如果子类是菜单,可以有子菜单和子菜单项;如果子类是菜单项,则不能使用这个方法
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException(); // 调用父类方法直接报错
}
// 移除子菜单
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
// 获取指定的子菜单
public MenuComponent getChild(int index) {
throw new UnsupportedOperationException();
}
// 获取菜单或菜单项的名称(子类无需定义该方法,提高复用性)
public String getName() {
return name;
}
// 打印菜单名称,包含子菜单和子菜单项
public abstract StringBuffer print(StringBuffer s);
}
这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。
import java.util.ArrayList;
import java.util.List;
// 菜单类:属于树枝结点
public class Menu extends MenuComponent {
// 菜单类聚合,菜单可以有多个子菜单或子菜单项
private List<MenuComponent> menuComponentList = new ArrayList<>();
public Menu(String name, int level) {
this.name = name;
this.level = level;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}
@Override
public MenuComponent getChild(int index) {
return menuComponentList.get(index);
}
@Override
public StringBuffer print(StringBuffer s) {
// 打印菜单名称
for (int i = 0; i < level; i++) s.append("-");
s.append(level).append("-").append(name).append("\n").append("|").append("\n");
// 打印子菜单或子菜单项名称
for (MenuComponent menuComponent : menuComponentList) {
s = menuComponent.print(s);
}
return s;
}
}
Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。
// 菜单项类,属于叶子结点
public class MenuItem extends MenuComponent {
public MenuItem(String name, int level) {
this.name = name;
this.level = level;
}
@Override
public StringBuffer print(StringBuffer s) {
// 打印菜单项的名称
for (int i = 0; i < level; i++) s.append("-");
s.append(level).append("-").append(name).append("\n").append("|").append("\n");
return s;
}
}
MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。
// 测试类
public class Client {
public static void main(String[] args) {
// 创建菜单树(与案例结构图一致)
MenuComponent menu1 = new Menu("菜单管理", 2);
menu1.add(new MenuItem("页面访问", 3));
menu1.add(new MenuItem("展开菜单", 3));
menu1.add(new MenuItem("编辑菜单", 3));
menu1.add(new MenuItem("删除菜单", 3));
menu1.add(new MenuItem("新增菜单", 3));
MenuComponent menu2 = new Menu("权限配置", 2);
menu2.add(new MenuItem("页面访问", 3));
menu2.add(new MenuItem("提交保存", 3));
MenuComponent menu3 = new Menu("角色管理", 2);
menu3.add(new MenuItem("页面访问", 3));
menu3.add(new MenuItem("新增角色", 3));
menu3.add(new MenuItem("修改角色", 3));
// 创建一级菜单
MenuComponent systemMenu = new Menu("系统管理", 1);
systemMenu.add(menu1);
systemMenu.add(menu2);
systemMenu.add(menu3);
// 打印输出所有菜单和菜单项
StringBuffer menu = systemMenu.print(new StringBuffer());
menu.delete(menu.length() - 2, menu.length()); // 优化输出格式,删除index[len-2,len-1]
System.out.println(menu);
}
}
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
MenuComponent
声明了 add
、remove
、getChild
方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。Menu
类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
定义:
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
享元(Flyweight)模式中存在以下两种状态:
享元模式的主要有以下角色:
【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
代码如下:
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
// 抽象享元角色
public abstract class AbstractBox {
// 获取图形的方法(图形:内部状态)
public abstract String getShape();
// 显示图形及颜色(颜色:外部状态,将享元对象的部分状态外部化(将颜色设置为外部状态,通过参数进行设置))
public void display(String color) {
System.out.println("方块形状:" + getShape() + ", 颜色:" + color);
}
}
接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。
// I图形类:具体享元角色
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
// L图形类:具体享元角色
public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
// O图形类:具体享元角色
public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。
import java.util.HashMap;
// 工厂类(将该类设计为单例)
public class BoxFactory {
private HashMap<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 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 box1 = BoxFactory.getInstance().getShape("I");
box1.display("灰色");
// 获取L图形对象
AbstractBox box2 = BoxFactory.getInstance().getShape("L");
box2.display("蓝色");
// 获取O图形对象
AbstractBox box3 = BoxFactory.getInstance().getShape("O");
box3.display("黄色");
// 获取O图形对象
AbstractBox box4 = BoxFactory.getInstance().getShape("O");
box4.display("红色");
System.out.println("两次获取到的O图形对象是否是同一个对象:" + (box3 == box4));
}
}
为了使对象可以共享,需要将享元对象的部分状态外部化(将颜色设置为外部状态),分离内部状态和外部状态,使程序逻辑复杂。
有大量相同或者相似的对象
,造成内存的大量耗费。对象的大部分状态都可以外部化
,可以将这些外部状态传入对象中。存储享元对象
的享元池
(HashMap),而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象
时才值得使用享元模式。Integer类使用了享元模式。我们先看下面的例子:
public class Demo {
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));
Integer i3 = 128;
Integer i4 = 128;
System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
}
}
为什么第一个输出语句输出的是true,第二个输出语句输出的是false?通过反编译软件进行反编译,代码如下:
public class Demo {
public static void main(String[] args) {
Integer i1 = Integer.valueOf((int)127);
Integer i2 Integer.valueOf((int)127);
System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
Integer i3 = Integer.valueOf((int)128);
Integer i4 = Integer.valueOf((int)128);
System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
}
}
上面代码可以看到,直接给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf()
,所以只需要看该方法即可。
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() {}
}
}
可以看到 Integer
默认先创建并缓存 -128 ~ 127
之间数的 Integer
对象,当调用 valueOf
时如果参数在 -128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的 Integer
对象。