结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下7种:

  • 代理模式
  • 适配器模式
  • 装饰者模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式

代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

静态代理

火车站买票案例,如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。火车站是目标对象,代售点是代理对象。

//卖票接口
public interface SellTickets {
void sell();
}

//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}

//代售点
public class ProxyPoint implements SellTickets {

private TrainStation station = new TrainStation();

public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}

//测试类
public class Client {
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint();
pp.sell();
}
}

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

JDK动态代理

Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。

代码如下:

//卖票接口
public interface SellTickets {
void sell();
}

//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}

//代理工厂,用来创建代理对象
public class ProxyFactory implements InvocationHandler {
private final Object object; //这里需要保存一下被代理的对象,下面需要用到

public ProxyFactory(Object object) {
this.object = object;
}

//调用代理对象的对应方法时会进入,需要编写如何进行代理
@Override //method是调用的代理对象的方法,args是实参数组
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
Object res = method.invoke(object, args); //在代理中调用被代理对象原本的方法
return res;
}

//测试类
public class Client {
public static void main(String[] args) {
//获取代理对象
TrainStation station = new TrainStation();
InvocationHandler handler = new ProxyFactory(station);
SellTickets proxy = (SellTickets) Proxy.newProxyInstance(
station.getClass().getClassLoader(), //需要传入被代理的类的类加载器
station.getClass().getInterfaces(), //需要传入被代理的类的接口列表
handler); //最后传入我们实现的代理处理逻辑实现类
proxy.sell();
}
}

CGLIB动态代理

JDK提供的动态代理只能使用接口,如果换成我们一开始的抽象类就不可以了,这时我们可以使用一些第三方框架来实现更多方式的动态代理,比如Spring都在使用的CGLib框架,Maven依赖如下:

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>

由于CGlib底层使用ASM框架进行字节码编辑,所以能够实现不仅仅局限于对接口的代理:

//火车站
public class TrainStation {

public void sell() {
System.out.println("火车站卖票");
}
}

//代理工厂
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;
}
}

//测试类
public class Client {
public static void main(String[] args) {
//创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
//获取代理对象
TrainStation proxyObject = factory.getProxyObject();

proxyObject.sell(); // 代理对象的sell方法被调用时,CGLib会自动调用intercept方法
}
}

优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 代理对象可以扩展目标对象的功能
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度

缺点:

  • 增加了系统的复杂度

使用场景

  • 远程(Remote)代理

    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节

  • 防火墙(Firewall)代理

    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器

  • 保护(Protect or Access)代理

    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求了解现有组件库中的相关组件的内部结构。

类适配器模式

想定场景:现有一台电脑只能读取SD卡,现在需要读取TF卡中的内容

代码如下:

//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}

//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(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);
}
}

//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 Computer {

public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}

//测试类
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));
}
}

这种实现方式需要占用一个继承位,如果此时Target不是接口而是抽像类的话,由于Java不支持多继承,那么就无法实现了,且类适配器模式违背了合成复用原则。

类适配器是客户类有一个接口规范的情况下可用,反之不可用。

对象适配器模式

类适配器模式的代码,只需要修改适配器类(SDAdapterTF)和测试类:

//创建适配器对象(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 tfCard.readTF();
}

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();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));

System.out.println("------------");

TFCard tfCard = new TFCardImpl();
SDAdapterTF adapter = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(adapter));
}
}

将对象以组合的形式存放在SDAdapterTF中,通过存放的对象调用具体实现

装饰模式

装饰模式的核心在于不改变一个对象本身功能的基础上,给对象添加额外的行为,并且它是通过组合的形式完成的,而不是传统的继承关系。

比如有一个普通的功能类:

public abstract class Base {   //顶层抽象类,定义了一个test方法执行业务
public abstract void test();
}
public class BaseImpl extends Base{
@Override
public void test() {
System.out.println("我是业务方法"); //具体的业务方法
}
}

不过现在的实现类太单调了,添加一点装饰上去:

public class Decorator extends Base{   //装饰者需要将装饰目标组合到类中

protected Base base;

public Decorator(Base base) {
this.base = base;
}

@Override
public void test() {
base.test(); //这里暂时还是使用目标的原本方法实现
}
}
public class DecoratorImpl extends Decorator{   //装饰实现

public DecoratorImpl(Base base) {
super(base);
}

@Override
public void test() { //对原本的方法进行装饰,我们可以在前后都去添加额外操作
System.out.println("装饰方法:我是操作前逻辑");
super.test();
System.out.println("装饰方法:我是操作后逻辑");
}
}

这样,我们就通过装饰模式对类的功能进行了扩展:

public static void main(String[] args) {
Base base = new BaseImpl();
Decorator decorator = new DecoratorImpl(base); //将Base实现装饰一下
Decorator outer = new DecoratorImpl(decorator); //装饰者还可以嵌套
decorator.test();
outer.test();
}

这样就实现了装饰模式

桥接模式

现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系,可以发现有很多的类,假如再增加几个形状或再增加一种颜色,就需要创建更多的类。在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。

桥接模式可以将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

例如需要开发一个跨平台视频播放器,可以在不同操作系统平台(如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("视频");
}
}

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统

如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化

使用场景

  • 当一个类存在多个独立变化的维度,且这些维度都需要进行扩展时
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系

外观模式

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观模式是“迪米特法则”的典型应用

举例,小明每次起床时都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来比较麻烦。所以小明买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。

//灯类
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();
}
}

//起床后一键开电器
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("关闭");
}
}

好处:

降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类

对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易

缺点:

不符合开闭原则,修改麻烦

使用场景

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性

组合模式

组合模式又名部分整体模式,用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次,它创建了对象组的树形结构。

实际上就是将多个组件进行组合,让用户可以对它们进行一致性处理。比如我们的文件夹,一个文件夹中可以有很多个子文件夹或是文件。像一个树形结构一样,有分支有叶子,组合模式可以对整个树形结构上的所有节点进行递归处理。

举例,使用文件和文件夹例子:

/**
* 首先创建一个组件抽象,组件可以包含组件,组件有自己的业务方法
*/
public abstract class Component {
public abstract void addComponent(Component component); //添加子组件
public abstract void removeComponent(Component component); //删除子组件
public abstract Component getChild(int index); //获取子组件
public abstract void test(); //执行对应的业务方法,比如修改文件名称
}

编写两种实现类:

public class Directory extends Component{   //目录可以包含多个文件或目录

List<Component> child = new ArrayList<>(); //这里我们使用List来存放目录中的子组件

@Override
public void addComponent(Component component) {
child.add(component);
}

@Override
public void removeComponent(Component component) {
child.remove(component);
}

@Override
public Component getChild(int index) {
return child.get(index);
}

@Override
public void test() {
System.out.println("文件名称修改成功!"+this);
child.forEach(Component::test); //将继续调用所有子组件的test方法执行业务
}
}

public class File extends Component{   //文件就相当于是树叶

@Override
public void addComponent(Component component) {
throw new UnsupportedOperationException(); //不支持这些操作
}

@Override
public void removeComponent(Component component) {
throw new UnsupportedOperationException();
}

@Override
public Component getChild(int index) {
throw new UnsupportedOperationException();
}

@Override
public void test() {
System.out.println("文件名称修改成功!"+this);
}
}

测试类:

public static void main(String[] args) {
Directory outer = new Directory(); //新建一个外层目录
Directory inner = new Directory(); //新建一个内层目录
outer.addComponent(inner);
outer.addComponent(new File()); //在内层目录和外层目录都添加点文件
inner.addComponent(new File());
inner.addComponent(new File());
outer.test(); //开始文件名称修改操作
}

享原模式

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

举例,比如现在有两个服务都需要使用数据库操作,实际上这个工具类没必要创建多个,这时就可以使用享元模式,让数据库类作为享元类,通过享元工厂来提供一个共享的数据库工具类:

public class DBUtil {
public void selectDB(){
System.out.println("数据库操作...");
}
}

public class DBUtilFactory {
private static final DBUtil UTIL = new DBUtil(); //享元对象被存放在工厂中

public static DBUtil getFlyweight(){ //获取享元对象
return UTIL;
}
}

需要使用享元对象时,调用享元工厂类:

public class UserService {
public void service(){
DBUtil util = DBUtilFactory.getFlyweight();
util.selectDB();
}
}