结构型模式概述
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
- 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
- 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
- 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
以上 7 种结构型模式,除了适配器模式分为类结构型模式和对象结构型模式两种,其他的全部属于对象结构型模式,下面我们会分别、详细地介绍它们的特点、结构与应用。
4.7 代理模式
定义特点
代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
优缺点
代理模式的主要优点有:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
其主要缺点是:
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
那么如何解决以上提到的缺点呢?答案是可以使用动态代理方式
使用场景
在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。
当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。
前面分析了代理模式的结构与特点,现在来分析以下的应用场景。
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
在静态代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点:
- 真实主题与代理主题一一对应,增加真实主题也要增加代理。
- 设计代理以前真实主题必须事先存在,不太灵活。
采用动态代理模式可以解决以上问题,如 SpringAOP,其结构图如图所示:
实现
静态代理
代理模式的结构比较简单,主要是通过代理类和目标类都实现一个接口,然后把目标类放到代理类中,代理类重写方法时调用目标类的方法,并且可以进行前后操作。
在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。
根据代理的创建时期,代理模式分为静态代理和动态代理。
- 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
- 动态:在程序运行时,运用反射机制动态创建而成。
public class Client {
public static void main(String[] args) {
ProxyTeacher proxyTeacher = new ProxyTeacher(new Teacher());
proxyTeacher.teach();
}
}
interface ITeacher {
void teach();
}
class Teacher implements ITeacher {
@Override
public void teach() {
System.out.println("teach()");
}
}
class ProxyTeacher implements ITeacher {
private ITeacher teacher;
public ProxyTeacher(ITeacher teacher) {
this.teacher = teacher;
}
@Override
public void teach() {
System.out.println("前置操作");
teacher.teach();
System.out.println("后置操作");
}
}
输出结果:
前置操作
teach()
后置操作
动态代理
动态代理有两种:
- JDK代理/接口代理:静态代理的升级版,不用写很多复杂的代理类
- cglib代理:JDK代理/接口代理必须要被代理的类实现接口,而cglib代理在内存中创建对象,不需要实现接口。
JDK代理/接口代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(new Teacher());
ITeacher proxyInstance = (ITeacher) proxyFactory.getProxyInstance();
proxyInstance.teach();
}
}
interface ITeacher {
void teach();
}
class Teacher implements ITeacher {
@Override
public void teach() {
System.out.println("teach()");
}
}
class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
/*
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
loader:target对象的类加载器
interfaces: target对象实现的接口
h: 反射生成代理类的事情处理器
**/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置操作");
Object o = method.invoke(target, args);
System.out.println("后置操作");
return o;
}
});
}
}
输出结果:
Before invoke Request
访问真实主题1方法...
After invoke Request
Before invoke Request
访问真实主题2方法...
After invoke Request
cglib代理
CGLIB是一个强大的高性能的代码生成包。
- 它广泛的被许多AOP的框架使用,例如:Spring AOP和dynaop,为他们提供方法的interception(拦截);
- hibernate使用CGLIB来代理单端single-ended(多对一和一对一)关联(对集合的延迟抓取,是采用其他机制实现的);
- EasyMock和jMock是通过使用模仿(moke)对象来测试java代码的包。
它们都通过使用CGLIB来为那些没有接口的类创建模仿(moke)对象。
需要两个jar包,自己去maven仓库里找(cglib-3.1自带asm-4.2):
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(new Teacher());
Teacher proxyTeacher = (Teacher) proxyFactory.getProxyInstance();
proxyTeacher.teach();
}
}
class Teacher {
public void teach() {
System.out.println("teach()");
}
}
class ProxyFactory implements MethodInterceptor {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
//笔者搞不懂这个方法里面的操作
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("前置操作");
Object object = method.invoke(target, args);
System.out.println("后置操作");
return object;
}
}
输出:
前置操作
teach()
后置操作
4.8 适配器模式
定义特点
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。别名又叫包装器(wrapper)。
优缺点
该模式的主要优点如下。
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
- 在很多业务场景中符合开闭原则。
其缺点是:
- 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。
使用场景
适配器模式(Adapter)通常适用于以下场景:
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
实现
三种适配器,类适配器、对象适配器、接口适配器。
类适配器
adapter(适配类)继承src类(被适配类),实现dst类(使用类)的接口,完成src–>dst
package com.atguigu.shipeiqi.lei;
public class Client {
public static void main(String[] args) {
Phone phone = new Phone();
VoltageAdapter voltageAdapter = new VoltageAdapter();
phone.change(voltageAdapter);
}
}
class Voltage220V {
public int output220V() {
int v_src = 220;
System.out.println("output220V()");
return v_src;
}
}
interface IVoltage5V {
public int output5V();
}
class VoltageAdapter extends Voltage220V implements IVoltage5V {
@Override
public int output5V() {
int v_dst;
System.out.println("output5V()");
int v_src = output220V();
v_dst = v_src / 44;
return v_dst;
}
}
class Phone {
public void change(IVoltage5V iVoltage5V) {
if (iVoltage5V.output5V() == 5) {
System.out.println("充电了,5V");
} else {
System.out.println("充不上,不是5V");
}
}
}
对象适配器
package com.atguigu.shipeiqi.lei;
public class Client {
public static void main(String[] args) {
Phone phone = new Phone();
VoltageAdapter voltageAdapter = new VoltageAdapter(new Voltage220V());
phone.change(voltageAdapter);
}
}
class Voltage220V {
public int output220V() {
int v_src = 220;
System.out.println("output220V()");
return v_src;
}
}
interface IVoltage5V {
public int output5V();
}
class VoltageAdapter implements IVoltage5V {
private Voltage220V voltage220V;
public VoltageAdapter(Voltage220V voltage220V) {
this.voltage220V = voltage220V;
}
@Override
public int output5V() {
int v_dst;
System.out.println("output5V()");
int v_src = voltage220V.output220V();
v_dst = v_src / 44;
return v_dst;
}
}
class Phone {
public void change(IVoltage5V iVoltage5V) {
if (iVoltage5V.output5V() == 5) {
System.out.println("充电了,5V");
} else {
System.out.println("充不上,不是5V");
}
}
}
接口适配器
public class Client {
public static void main(String[] args) {
AbsAdapter adapter = new AbsAdapter() {
@Override
public void operation1() {
System.out.println("override-operation1()");
}
};
adapter.operation1();
adapter.operation2();
}
}
interface Interface4 {
public void operation1();
public void operation2();
public void operation3();
public void operation4();
}
abstract class AbsAdapter implements Interface4 {
@Override
public void operation1() {
}
@Override
public void operation2() {
}
@Override
public void operation3() {
}
@Override
public void operation4() {
}
}
4.9 桥接模式
定义特点
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
优缺点
桥接(Bridge)模式的优点是:
- 抽象与实现分离,扩展能力强
- 符合开闭原则
- 符合合成复用原则
- 其实现细节对客户透明
缺点:
- 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
使用场景
当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。
继承是管对象的,聚合是管对象的属性的。
桥接模式通常适用于以下场景:
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。
因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
实现
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
public class Client {
public static void main(String[] args) {
Implementor concreteImplementorA = new ConcreteImplementorA();
Abstraction refinedAbstraction = new RefinedAbstraction(concreteImplementorA);
refinedAbstraction.Operation();
}
}
//实现化角色
interface Implementor {
public void OperationImpl();
}
//具体实现化角色
class ConcreteImplementorA implements Implementor {
@Override
public void OperationImpl() {
System.out.println("ConcreteImplementorA-OperationImpl()");
}
}
//抽象化角色
abstract class Abstraction {
protected Implementor implementor;
protected Abstraction(Implementor implementor) {
this.implementor = implementor;
}
public abstract void Operation();
}
//扩展抽象化角色
class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
@Override
public void Operation() {
System.out.println("RefinedAbstraction-Operation()");
implementor.OperationImpl();
}
}
案例:
用桥接(Bridge)模式模拟女士皮包的选购:
女士皮包有很多种,可以按用途分、按皮质分、按品牌分、按颜色分、按大小分等,存在多个维度的变化,所以采用桥接模式来实现女士皮包的选购比较合适。
颜色类(Color)是一个维度,定义为实现化角色,它有两个具体实现化角色:黄色和红色,通过 getColor() 方法可以选择颜色;包类(Bag)是另一个维度,定义为抽象化角色,它有两个扩展抽象化角色:挎包和钱包,它包含了颜色类对象,通过 getName() 方法可以选择相关颜色的挎包和钱包。
public class Client {
public static void main(String[] args) {
Wallet wallet = new Wallet(new Red());
System.out.println(wallet.getName());
HangBag hangBag = new HangBag(new Yello());
System.out.println(hangBag.getName());
}
}
interface Color {
String getColor();
}
class Yello implements Color {
@Override
public String getColor() {
return "yellow";
}
}
class Red implements Color {
@Override
public String getColor() {
return "red";
}
}
abstract class Bag {
protected Color color;
public Bag(Color color) {
this.color = color;
}
public abstract String getName();
}
class HangBag extends Bag {
public HangBag(Color color) {
super(color);
}
@Override
public String getName() {
return color.getColor() + " hangBag";
}
}
class Wallet extends Bag {
public Wallet(Color color) {
super(color);
}
@Override
public String getName() {
return color.getColor() + " wallet";
}
}
4.10 装饰器模式
定义特点
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
优缺点
装饰器模式的主要优点有:
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
- 装饰器模式完全遵守开闭原则
其主要缺点是:
- 装饰器模式会增加许多子类,过度使用会增加程序得复杂性。
使用场景
前面讲解了关于装饰器模式的结构与特点,下面介绍其适用的应用场景,装饰器模式通常在以下几种情况使用:
- 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
- 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现。
- 当对象的功能要求可以动态地添加,也可以再动态地撤销时。
装饰器模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。
实现
装饰器模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
public class Client {
public static void main(String[] args) {
ConcreteComponent concreteComponent = new ConcreteComponent();
concreteComponent.opration();
System.out.println("--------------------------");
ConcreteDecorator concreteDecorator = new ConcreteDecorator(concreteComponent);
concreteDecorator.opration();
}
}
interface Component {
public void opration();
}
class ConcreteComponent implements Component {
@Override
public void opration() {
System.out.println("ConcreteComponent-opration()");
}
}
class Decorator implements Component {
private Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void opration() {
component.opration();
}
}
class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}
@Override
public void opration() {
super.opration();
addedFunction();
}
private void addedFunction() {
System.out.println("addedFunction()");
}
}
输出:
ConcreteComponent-opration()
--------------------------
ConcreteComponent-opration()
addedFunction()
案例:
给car、tank等车辆增加不同的功能,有开火、开灯等功能。
public class Client {
public static void main(String[] args) {
car car = new car();
Tank tank = new Tank();
AddGuns gunsCar = new AddGuns(car);
gunsCar.move();
AddLight lightTank = new AddLight(tank);
lightTank.move();
}
}
interface IVehical {
public void move();
}
class Tank implements IVehical {
@Override
public void move() {
System.out.println("履带方式移动");
}
}
class car implements IVehical {
@Override
public void move() {
System.out.println("车轮方式移动");
}
}
class Decorator implements IVehical {
private IVehical vehical;
public Decorator(IVehical vehical) {
this.vehical = vehical;
}
@Override
public void move() {
vehical.move();
}
}
class AddGuns extends Decorator {
public AddGuns(IVehical vehical) {
super(vehical);
}
@Override
public void move() {
fire();
super.move();
}
private void fire() {
System.out.println("开火");
}
}
class AddLight extends Decorator {
public AddLight(IVehical vehical) {
super(vehical);
}
@Override
public void move() {
light();
super.move();
}
private void light() {
System.out.println("开灯");
}
}
4.11 外观模式(Facade模式)
定义特点
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
优缺点
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
外观(Facade)模式的主要缺点如下:
- 不能很好地限制客户使用子系统类,很容易带来未知风险。
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
使用场景
通常在以下情况下可以考虑使用外观模式:
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
实现
外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
- 客户(Client)角色:通过一个外观角色访问各个子系统的功能。
public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.method();
}
}
class Facade {
private SubSystem1 ss1 = new SubSystem1();
private SubSystem2 ss2 = new SubSystem2();
private SubSystem3 ss3 = new SubSystem3();
public void method() {
ss1.method();
ss2.method();
ss3.method();
}
}
class SubSystem1 {
public void method() {
System.out.println("SubSystem1-method()");
}
}
class SubSystem2 {
public void method() {
System.out.println("SubSystem2-method()");
}
}
class SubSystem3 {
public void method() {
System.out.println("SubSystem3-method()");
}
}
4.12 享元模式
定义特点
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
优缺点
享元模式的主要优点是:
- 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
使用场景
当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样,一个对象就可以提供给多出需要使用的地方,避免大量同一对象的多次创建,降低大量内存空间的消耗。
享元模式其实是工厂方法模式的一个改进机制,享元模式同样要求创建一个或一组对象,并且就是通过工厂方法模式生成对象的,只不过享元模式为工厂方法模式增加了缓存这一功能。
前面分析了享元模式的结构与特点,下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式:
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
- 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
- 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
实现
享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
- 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
- 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。
享元模式的本质是缓存共享对象,降低内存消耗。
享元模式的主要角色有如下:
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
public class Client {
public static void main(String[] args) {
FlyweightFactory flyweightFactory = new FlyweightFactory();
Flyweight fw1 = flyweightFactory.getFlyweight("a");
Flyweight fw2 = flyweightFactory.getFlyweight("a");
fw1.operation(new UnsharedConcretrFlyweight("第一次调用"));
fw2.operation(new UnsharedConcretrFlyweight("第二次调用"));
}
}
class UnsharedConcretrFlyweight {
private String info;
public UnsharedConcretrFlyweight(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "UnsharedConcretrFlyweight{" +
"info='" + info + '\'' +
'}';
}
}
interface Flyweight {
public void operation(UnsharedConcretrFlyweight state);
}
class ConcreteFlyweight implements Flyweight {
private String key;
public ConcreteFlyweight(String key) {
this.key = key;
}
@Override
public void operation(UnsharedConcretrFlyweight state) {
System.out.println("ConcreteFlyweight|" + key + "|" + state);
}
}
class FlyweightFactory {
private HashMap<String, Flyweight> flyweights = new HashMap<String, Flyweight>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = flyweights.get(key);
if (flyweight != null) {
System.out.println("get flyweight|" + key);
} else {
ConcreteFlyweight concreteFlyweight = new ConcreteFlyweight(key);
flyweights.put(key, concreteFlyweight);
flyweight = concreteFlyweight;
System.out.println("create flyweight|" + key);
}
return flyweight;
}
}
4.13 组合模式
定义特点
有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,树形结构图如下。
由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
优缺点
组合模式的主要优点有:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
其主要缺点是:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
使用场景
前面分析了组合模式的结构与特点,下面分析它适用的以下应用场景:
- 在需要表示一个对象整体与部分的层次结构的场合。
- 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。
实现
组合模式分为透明式的组合模式和安全式的组合模式。
假如要访问集合 c0={leaf1,{leaf2,leaf3}} 中的元素,其对应的树状图如图所示:
(1) 透明方式
在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。其结构图如下图:
import java.util.ArrayList;
public class Client {
public static void main(String[] args) {
Component c0 = new Composite();
Component c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
interface Component {
public void add(Component component);
public void remove(Component component);
public Component getChild(int i);
public void operation();
}
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void add(Component component) {
}
@Override
public void remove(Component component) {
}
@Override
public Component getChild(int i) {
return null;
}
@Override
public String toString() {
return "Leaf{" +
"name='" + name + '\'' +
'}';
}
@Override
public void operation() {
System.out.println(this + "被访问");
}
}
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public Component getChild(int i) {
return children.get(i);
}
@Override
public void operation() {
for (Object object : children) {
((Component) object).operation();
}
}
}
(2) 安全方式
在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。其结构图如下图所示:
package com.atguigu.zuhemoshi;
import java.util.ArrayList;
public class Client {
public static void main(String[] args) {
Composite c0 = new Composite();
Composite c1 = new Composite();
Component leaf1 = new Leaf("1");
Component leaf2 = new Leaf("2");
Component leaf3 = new Leaf("3");
c0.add(leaf1);
c0.add(c1);
c1.add(leaf2);
c1.add(leaf3);
c0.operation();
}
}
interface Component {
public void operation();
}
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public String toString() {
return "Leaf{" +
"name='" + name + '\'' +
'}';
}
@Override
public void operation() {
System.out.println(this + "被访问");
}
}
class Composite implements Component {
private ArrayList<Component> children = new ArrayList<Component>();
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
public Component getChild(int i) {
return children.get(i);
}
public void operation() {
for (Object object : children) {
((Component) object).operation();
}
}
}