04-结构型模式(7)
- 总结了一些类和对象组合在一起的经典结构,这些经典结构可以解决对应特定场景的问题
- 一共包括七种:
- 代理模式、桥接模式、装饰者模式、适配器模式
- 门面(外观)模式、组合模式、享元模式
1. 代理模式
- 代理模式(Proxy Design Pattern)原始定义:提供对象的替代品或其占位符。代理控制着对原对象的访问,并允许将请求提交给对象前后进行一些处理
1. 介绍
- 在软件开发中,由于一些原因,客户端不想或不能直接访问一个对象,可以通过一个"代理"的第三者来间接访问
- 现实生活中的代理: 海外代购
- 软件开发中的代理
- 引入一个新的代理对象,代理对象在客户端对象和目标对象之间起中介作用,它去掉客户不能看到的内容和服务、增加客户需要的额外的新服务
2. 原理
- 抽象主题类(Subject):真实主题、代理主题的共同接口,保证任何使用真实主题的地方都可以使用代理主题,客户端一般针对抽象主题类进行编程
- 真实主题类(Real Subject):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,最终要引用的对象
- 代理类(Proxy):其内部含有对真实主题的引用,它可以在任何时候访问、控制、扩展真实主题的功能
3. 静态代理实现
代理对象和目标对象实现一样的接口
- 优点:不修改目标对象前提下扩展目标对象的功能
- 缺点:
- 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类
- 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改
// 接口类
public interface IUserDao {
void save();
}
// 目标对象
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
// 静态代理对象:UserDaoProxy 需要实现IUserDao接口
public class UserDaoProxy implements IUserDao {
private final IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开启事务"); // 扩展额外的功能
target.save();
System.out.println("提交事务");
}
}
/*
* 静态代理
* 优点: 可以在不修改目标类的前提下, 扩展目标类的功能
* 缺点:
* 1.冗余. 由于代理对象要实现和目标对象一致的接口, 会产生很多的代理
* 2.不易维护. 一旦接口中增加方法,目标对象和代理对象都要进行修改
*/
@Test
public void testStaticProxy() {
// 目标类
IUserDao dao = new UserDaoImpl();
// 代理对象
UserDaoProxy proxy = new UserDaoProxy(dao);
proxy.save();
}
4. JDK动态代理
1. 实现
- 动态代理利用了JDK_API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理
- 静态代理与动态代理的区别:
- 静态代理在编译时就已经实现了,编译完成后代理类是一个实际的class文件
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中.
JDK中生成代理对象主要涉及的类有
java.lang.reflect.Proxy
,主要方法为
static Object newProxyInstance(
ClassLoader loader, // 指定当前目标对象使用类加载器
Class<?>[] interfaces, // 目标对象实现的接口的类型
InvocationHandler h // 事件处理器
)
// 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序
java.lang.reflect.InvocationHandler
,主要方法为
Object invoke(Object proxy, Method method, Object[] args)
// 在代理实例上处理方法调用并返回结果
public interface ISubject {
void request();
}
public class Subject implements ISubject {
@Override
public void request() {
System.out.println("执行了被代理角色。。。");
}
}
/**
* 代理工厂类 - 动态的生成代理对象
*/
@Setter
public class ProxyFactory {
// 被代理对象
private Object target;
public ProxyFactory(Object t) {
target = t;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类使用的类加载器
target.getClass().getInterfaces(), // 目标类实现的接口
new InvocationHandler() { // 事件处理器匿名实现类
/**
* @param proxy 代理对象
* @param method 代理对象的方法
* @param args 方法参数
* @return 代理对象方法的返回值,没有就返回null
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
}
);
}
private void before() {
System.out.println("before method...");
}
private void after() {
System.out.println("after method...");
}
}
@Test
public void jdk_proxy() {
Subject subject = new Subject();
System.out.println(subject); // 目标对象信息
ISubject proxy = (ISubject) new ProxyFactory(subject).getProxyInstance();
System.out.println(proxy);
proxy.request();
}
2. 类是如何动态生成的
- JVM类加载过程分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
- 从本地获取
- 从网络中获取
- 运行时计算生成,这种场景使用最多的是动态代理技术。
java.lang.reflect.Proxy
类中,就是用了ProxyGenerator.generateProxyClass
来为特定接口生成形式为*$Proxy
的代理类的二进制字节流
- 动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用
3. 代理类的调用过程
- 通过借用阿里巴巴的一款线上监控诊断产品Arthas(阿尔萨斯),对动态生成的代理类代码进行查看
package com.sun.proxy;
import com.listao.dp._05_proxy.example01.IUserDao;
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 IUserDao {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
try {
m3 = Class.forName("com.listao.dp._05_proxy.example01.IUserDao").getMethod("save", new Class[0]);
return;
}
}
public final void save() {
try {
this.h.invoke(this, m3, null);
return;
}
}
}
- 动态代理类对象,继承了
Proxy
,实现了被代理的所有接口,以及equals, hashCode, toString
等方法 - 代理类的构造函数,参数是
InvocationHandler
实例,Proxy.newInstance
方法就是通过这个构造函数来创建代理实例的 - 类和所有方法都被
public final
修饰,所以代理类只可被使用,不可以再被继承 - 每个方法都有一个
Method
对象来描述,Method
对象在static静态代码块中创建,以m + 数字
的格式命名 - 调用方法的时候通过
this.h.invoke(this, m3, null));
。实际上 h.invoke就是在调用ProxyFactory中我们重写的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开启事务");
// 执行目标对象方法
method.invoke(target, args);
System.out.println("提交事务");
return null;
}
4. 演进
/**
* 1. 问题: 想记录坦克的移动时间
*/
interface Movable {
void move();
}
public class TankV1 implements Movable {
/**
* 模拟坦克移动了一段儿时间
*/
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 问题:我想记录坦克的移动时间
* 1. 最简单的办法:修改原代码,记录时间
* 2. 问题2:如果无法改变方法源码呢?benchmark:性能测试经常用到
*/
public class TankV2 implements Movable {
@Override
public void move() {
long start = System.currentTimeMillis();
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
// 原代码,记录时间
System.out.println(end - start);
}
public static void main(String[] args) {
new TankV2().move();
}
}
/**
* 问题:我想记录坦克的移动时间
* 最简单的办法:修改代码,记录时间
* 问题2:如果无法改变方法源码呢?
* 1. 用继承? 继承父类, 重写方法中调用父类方法
*/
public class TankV3 implements Movable {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Tank2().move();
}
}
/**
* 继承父类,重写方法中调用父类方法
* 慎用继承,耦合度太高。这次是记录时间,下次就是日志
*/
class Tank2 extends TankV3 {
@Override
public void move() {
long start = System.currentTimeMillis();
// 调用父类方法
super.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
/**
* 问题2:如果无法改变方法源码呢?
* 2. 使用代理
*/
public class TankV4 implements Movable {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TankTimeProxyV4(new TankV4()).move();
}
}
/**
* 时间代理(聚合)
*/
class TankTimeProxyV4 implements Movable {
TankV4 tank;
public TankTimeProxyV4(TankV4 tank) {
this.tank = tank;
}
@Override
public void move() {
long start = System.currentTimeMillis();
tank.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
/**
* 静态代理
* 1. 各种类型代理,嵌套
* 代理对象和被代理对象都实现接口,可互相嵌套
* 越来越像decorator
*/
public class TankV5 implements Movable {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TankV5 t = new TankV5();
TankTimeProxyV5 ttp = new TankTimeProxyV5(t);
TankLogProxyV5 tlp = new TankLogProxyV5(ttp);
tlp.move();
System.out.println("======== 以下是嵌套模式 ========");
new TankLogProxyV5(
new TankTimeProxyV5(
new TankV5()
)
).move();
}
}
class TankLogProxyV5 implements Movable {
// 代理对象和被代理对象都实现接口,可互相嵌套
Movable m;
public TankLogProxyV5(Movable m) {
this.m = m;
}
@Override
public void move() {
System.out.println("start moving...");
m.move();
System.out.println("stopped!");
}
}
class TankTimeProxyV5 implements Movable {
Movable m;
public TankTimeProxyV5(Movable m) {
this.m = m;
}
@Override
public void move() {
long start = System.currentTimeMillis();
m.move();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
/**
* 动态代理(jdk)
* 代理行为,被代理对象分离
* v09生成代理文件
*/
public class TankV6 implements Movable {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(10_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TankV6 tank = new TankV6();
// java.lang.reflect.Proxy; reflection 通过二进制字节码分析class的属性和方法
// .getClass() 可以找到class对象,.getClassLoader() 可以找到是谁把其load内存的
// 所谓的反射是不需要看到原码,反射通过分析class类的字节码,就得到了类的属性和方法
// jdk动态代理必须有实现的接口
Movable m = (Movable) Proxy.newProxyInstance(
TankV6.class.getClassLoader(), // classLoader和被代理对象用同一个即可
new Class[]{Movable.class}, // tank.class.getInterfaces() 代理和被代理对象实现的接口的class对象
new ProxyHandlerLogV6(tank)
);
m.move();
System.out.println("======= 以下是匿名内部类 =======");
Movable mv = (Movable) Proxy.newProxyInstance(
TankV6.class.getClassLoader(),
new Class[]{Movable.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method " + method.getName() + " start...");
Object o = method.invoke(tank, args);
System.out.println("method " + method.getName() + " end!");
return o;
}
}
);
mv.move();
}
}
class ProxyHandlerLogV6 implements InvocationHandler {
Movable movable;
public ProxyHandlerLogV6(Movable movable) {
this.movable = movable;
}
/**
* 调用move()方法,执行了invoke()方法
* getClass.getMethods[] 可以得到类的所有方法
*
* @param proxy 代理对象
* @param method 被代理对象方法
* @param args 方法参数
* @return 返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method " + method.getName() + " start...");
Object o = method.invoke(movable, args);
System.out.println("method " + method.getName() + " end!");
return o;
}
}
/**
* 1. 生成代理对象$Proxy0
* 2. stop()
* 横切代码与业务逻辑代码分离 AOP
* 通过反射观察生成的代理对象
* jdk反射生成代理必须面向接口,这是由Proxy的内部实现决定的
*/
interface MovableV7 {
void move();
void stop();
}
public class TankV7 implements MovableV7 {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void stop() {
System.out.println("Tank stop!!!");
}
public static void main(String[] args) {
// ProxyGenerator代理产生器,产生的代理对象保存下来。必须在main方法中执行,直接用junit的test方法调用无法生成
// java 8
// System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// java 11
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
TankV7 tank = new TankV7();
MovableV7 m = (MovableV7) Proxy.newProxyInstance(TankV7.class.getClassLoader(),
new Class[]{Movable.class}, // tank.class.getInterfaces()
new ProxyHandlerLogV7(tank)
);
m.move();
System.out.println("====== another method ======");
m.stop();
}
}
class ProxyHandlerLogV7 implements InvocationHandler {
MovableV7 movable;
public ProxyHandlerLogV7(MovableV7 m) {
this.movable = m;
}
public void before() {
System.out.println("method start..");
}
public void after() {
System.out.println("method stop..");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Arrays.stream(proxy.getClass().getMethods()).map(Method::getName).forEach(System.out::println);
before();
// void 返回值就为null
Object o = method.invoke(movable, args);
after();
return o;
}
}
5. cglib动态代理
1. 实现
- cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展
- cglib为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充
- 最底层是字节码
- ASM是操作字节码的工具
- cglib基于ASM字节码工具操作字节码(即动态生成代理,对方法进行增强)
- SpringAOP基于cglib进行封装,实现cglib动态代理
使用cglib需要引入cglib的jar包,如果你已经有spring-core
的jar包,则无需引入,因为Spring中包含了cglib
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
@Data
@AllArgsConstructor
public class User {
private String name;
private int age;
}
public class UserServiceImpl {
// 查询功能
public List<User> findUserList() {
return Collections.singletonList(new User("tom", 23));
}
}
- cglib代理类,需要实现
MethodInterceptor
接口,并指定代理目标类target
public class UserLogProxy implements MethodInterceptor {
/**
* 生成CGLIB动态代理类方法
*
* @param target 需要被代理的目标类
*/
public Object getLogProxy(Object target) {
// 增强器类,用来创建动态代理类
Enhancer enhancer = new Enhancer();
// 设置代理类的父类字节码对象
enhancer.setSuperclass(target.getClass());
// 设置回调:对于代理类上所有的方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦截
enhancer.setCallback(this);
// 创建动态代理对象,并返回
return enhancer.create();
}
/**
* 实现回调方法
*
* @param o 代理对象
* @param method 目标对象中的方法的Method实例
* @param args 实际参数
* @param methodProxy 代理类对象中的方法的Method实例
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 目标对象
Calendar instance = Calendar.getInstance();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(format.format(instance.getTime()) + "[ " + method.getName() + "] 查询用户信息...");
Object result = methodProxy.invokeSuper(o, args);
return result;
}
}
@Test
public void testCglibProxy() {
// 目标对象
UserServiceImpl userService = new UserServiceImpl();
System.out.println(userService.getClass());
// 代理对象
UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
System.out.println(proxy.getClass());
List<User> list = proxy.findUserList();
System.out.println("用户信息: " + list);
}
2. cglib代理流程
/*
* cglib动态代理不需要接口,底层也是asm
* - 缺点:final类不能用cglib生成proxy类。asm可以,直接改2进制文件
*/
public class Cglib {
public static void main(String[] args) {
// 增强器
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(Tank.class);
enhancer.setCallback(new TimeMethodInterceptor());
Tank tank = (Tank) enhancer.create();
tank.move();
}
}
/**
* 不需要接口
*/
class Tank {
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class TimeMethodInterceptor implements MethodInterceptor {
/**
* intercept
*
* @param o 代理对象
* @param method method
* @param objects objects
* @param methodProxy methodProxy
* @return Object
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 父类为tank
System.out.println(o.getClass().getSuperclass().getName());
System.out.println("before");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("after");
return result;
}
}
6. SpringAop
1. jdk
<aop:aspectj-autoproxy/>
<bean id="tank_v2" class="com.listao.dp._05_proxy._03_spring.v2.jdk.Tank"/>
<bean id="timeProxy" class="com.listao.dp._05_proxy._03_spring.v2.jdk.TimeProxy"/>
/**
* spring aop test
*/
public class Main {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
ApplicationContext context = new ClassPathXmlApplicationContext("app_jdk.xml");
// jdk 只能用movable来接,代理类实现这个接口
// cglib
Movable t = context.getBean("tank_v2", Movable.class);
t.move();
}
}
interface Movable {
void move();
}
class Tank implements Movable {
@Override
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Aspect
class TimeProxy implements Movable {
@Before("execution (void com.listao.dp._05_proxy._03_spring.v2.jdk.Tank.move())")
@Override
public void move() {
System.out.println("method start.." + System.currentTimeMillis());
}
}
2. cglib
<bean id="tank" class="com.listao.dp._05_proxy._03_spring.v1.Tank"/>
<bean id="timeProxy" class="com.listao.dp._05_proxy._03_spring.v1.TimeProxy"/>
<aop:config>
<aop:aspect id="time" ref="timeProxy">
<aop:pointcut id="onMove" expression="execution(void com.listao.dp._05_proxy._03_spring.v1.Tank.move())"/>
<aop:before method="before" pointcut-ref="onMove"/>
<aop:after method="after" pointcut-ref="onMove"/>
</aop:aspect>
</aop:config>
/**
* AOP: Aspect Oriented Programming
*/
public class SpringAop {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
ApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
Tank t = (Tank) context.getBean("tank");
t.move();
}
}
class Tank {
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class TimeProxy {
public void before() {
System.out.println("method start..." + System.currentTimeMillis());
}
public void after() {
System.out.println("method stop..." + System.currentTimeMillis());
}
}
<aop:aspectj-autoproxy/>
<bean id="tank_v2" class="com.listao.dp._05_proxy._03_spring.v2.cglib.Tank"/>
<bean id="timeProxy" class="com.listao.dp._05_proxy._03_spring.v2.cglib.TimeProxy"/>
/**
* spring aop test
*/
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("app_cglib.xml");
Tank t = context.getBean("tank_v2", Tank.class);
t.move();
}
}
class Tank {
public void move() {
System.out.println("Tank moving claclacla...");
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Aspect
class TimeProxy {
@Before("execution (void com.listao.dp._05_proxy._03_spring.v2.cglib.Tank.move())")
public void before() {
System.out.println("method start.." + System.currentTimeMillis());
}
@After("execution (void com.listao.dp._05_proxy._03_spring.v2.cglib.Tank.move())")
public void after() {
System.out.println("method stop.." + System.currentTimeMillis());
}
}
7. 总结
1. 三种代理模式对比
- jdk代理和CGLIB代理
- 使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类
- 在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理
- 动态代理和静态代理
- 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转
- 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
2. 优缺点
- 优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
- 代理对象可以扩展目标对象的功能
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
- 缺点:
- 增加了系统的复杂度
3. 使用场景
- 功能增强
- 当需要对一个对象的访问提供一些额外操作时,可以使用代理模式
- 远程(Remote)代理
- 实际上,RPC框架也可以看作一种代理模式,GoF的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来
- 客户端在使用RPC服务时,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节
- 防火墙(Firewall)代理
- 当将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再转给浏览器
- 保护(Protect or Access)代理
- 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限
2. 桥接模式
- 桥接模式(Bridge Pattern)定义:将抽象部分与它的实现部分分离,使它们都可以独立变化
- 蜡笔
- 毛笔
1. 介绍
- 桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联来取代传统的多层继承,将类之间的静态继承关系转变为动态的组合关系,使得系统更加灵活,并易于扩展,有效的控制了系统中类的个数(避免了继承层次的指数级爆炸)
2. 原理
- 抽象化角色(Abstraction):定义出该角色的行为 ,并包含一个实现化对象的引用
- 扩展抽象化角色(RefinedAbstraction):实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
- 实现化角色(Implementor):定义实现化角色的接口,包含角色必须的行为和属性,并供扩展抽象化角色调用
- 具体实现化角色(Concrete Implementor):实现化角色接口的具体实现
- 桥接模式原理核心:首先有要识别出一个类所具有的的两个独立变化维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合
- 总结一句话就是:抽象角色引用实现角色
毛笔举例:型号、颜色是两个维度(实体与行为分离)
- 型号是其固有的维度,所以抽象出一个毛笔类,而将各种型号的毛笔作为其子类,也就是下图的右侧抽象部分内容
- 颜色是毛笔的另一个维度, 它与毛笔之间存在一种设置的关系,因此可以提供一个抽象的颜色接口,将具体颜色作为该接口的子类
3. 应用实例
- 模拟不同的支付工具对应不同的支付模式。支付操作其实就有两个维度, 包括:支付渠道和支付方式
- 微信、支付宝
- 扫码支付、密码支付、人脸支付等
1. 常规写法
/**
* 支付接口
*/
public class PayCtl {
/**
* 支付功能
*
* @param uId 用户id
* @param tradeId 交易id
* @param amount 交易金额
* @param channelType 渠道类型: 1-微信, 2-支付宝
* @param modeType 支付类型: 1-密码, 2-人脸, 3-指纹
*/
public boolean doPay(String uId, String tradeId, BigDecimal amount, int channelType, int modeType) {
// 微信支付
if (channelType == 1) {
System.out.println("微信渠道支付开始...");
if (modeType == 1) {
System.out.println("密码支付");
} else if (modeType == 2) {
System.out.println("人脸支付");
} else if (modeType == 3) {
System.out.println("指纹支付");
}
}
// 支付宝支付
if (channelType == 2) {
System.out.println("支付宝渠道支付开始...");
if (modeType == 1) {
System.out.println("密码支付");
} else if (modeType == 2) {
System.out.println("人脸支付");
} else if (modeType == 3) {
System.out.println("指纹支付");
}
}
return true;
}
}
@Test
public void test01() {
PayCtl payCtl = new PayCtl();
System.out.println("测试: 微信支付 --> 人脸支付方式");
payCtl.doPay("wx_001", "100010000", new BigDecimal(100), 1, 2);
System.out.println();
System.out.println("测试: 支付宝支付 --> 指纹支付方式");
payCtl.doPay("zfb_001", "100010000", new BigDecimal(100), 2, 3);
}
2. 桥接模式重构
- AbsPay抽象类
- 支付渠道子类:微信支付
- 支付渠道子类:支付宝支付
- IPayMode接口
- 支付模式实现:刷脸支付
- 支付模式实现:指纹支付
- 支付模式实现:密码支付
- 支付渠道 * 支付模式 = 相对应的组合
- 支付模式接口 (实现化角色)
/**
* 支付模式接口
*/
public interface IPayMode {
// 安全校验功能: 对各种支付模式进行风控校验操作
boolean security(String uId);
}
- 三种具体支付模式(具体实现化角色)
/**
* 密码支付
*/
public class ModeCypher implements IPayMode {
@Override
public boolean security(String uId) {
System.out.println("密码支付, 风控校验 --> 环境安全");
return true;
}
}
/**
* 刷脸支付
*/
public class ModeFace implements IPayMode {
@Override
public boolean security(String uId) {
System.out.println("人脸支付, 风控校验 --> 脸部识别");
return true;
}
}
/**
* 指纹支付
*/
public class ModeFingerprint implements IPayMode {
@Override
public boolean security(String uId) {
System.out.println("指纹支付, 风控校验 --> 指纹信息");
return true;
}
}
- 支付抽象类(抽象化角色)
/**
* 支付抽象类
*/
public abstract class AbsPay {
// 桥接对象
protected IPayMode payMode;
public AbsPay(IPayMode payMode) {
this.payMode = payMode;
}
// 划账
public abstract String transfer(String uId, String tradeId, BigDecimal amount);
}
- 支付渠道实现(扩展抽象化角色)
/**
* 支付渠道-微信支付
*/
public class PayWx extends AbsPay {
public PayWx(IPayMode payMode) {
super(payMode);
}
@Override
public String transfer(String uId, String tradeId, BigDecimal amount) {
System.out.println("微信渠道支付划账开始...");
boolean security = payMode.security(uId);
System.out.println("微信渠道支付风险校验: " + uId + ", " + tradeId + ", " + security);
if (!security) {
System.out.println("微信渠道支付划账失败!!!");
return "500";
}
System.out.println("微信渠道划账成功! 金额: " + amount);
return "200";
}
}
public class PayZfb extends AbsPay {
public PayZfb(IPayMode payMode) {
super(payMode);
}
@Override
public String transfer(String uId, String tradeId, BigDecimal amount) {
System.out.println("支付宝渠道支付划账开始...");
boolean security = payMode.security(uId);
System.out.println("支付宝渠道支付风险校验: " + uId + ", " + tradeId + ", " + security);
if (!security) {
System.out.println("支付宝渠道支付划账失败!!!");
return "500";
}
System.out.println("支付宝渠道划账成功! 金额: " + amount);
return "200";
}
}
- 测试
@Test
public void test02() {
System.out.println("测试场景1: 微信支付, 人脸方式");
AbsPay wxPay = new PayWx(new ModeFace());
wxPay.transfer("wx_0001001", "100090009", new BigDecimal(100));
System.out.println();
System.out.println("测试场景2: 支付宝支付, 指纹方式");
AbsPay zfbPay = new PayZfb(new ModeFingerprint());
zfbPay.transfer("zfb_0001001", "100090009", new BigDecimal(400));
}
3. 礼物demo
@Builder
public class GG {
public void chase(MM mm) {
AbsGift g = new Book();
give(mm, g);
}
public void give(MM mm, AbsGift g) {
}
public static void main(String[] args) {
GG.builder().build().chase(new MM());
}
}
class MM {
String name;
}
abstract class AbsGift {
}
class Flower extends AbsGift {
}
class Book extends AbsGift {
}
// ------------------------------------------------------
class WarmGift extends AbsGift {
}
class WildGift extends AbsGift {
}
/*
* 如果礼物分为温柔的礼物和狂野的礼物:WarmGift, WildGift
* - WarmFlower WildFlower
* - WarmBook WildBook
*
* 如果再有别的礼物,比如抽象类型:ToughGift ColdGift
* 或者具体的某种实现:Ring Car
*
* 会产生类的爆炸
* WarmCar ColdRing WildCar WildFlower ...
*/
class WarmFlower extends Flower {
}
@Builder
public class GG {
public void chase(MM mm) {
// 两两可以迪卡尔积,实现爆炸数量组合
AbsGift g = new WarmGift(new Flower());
give(mm, g);
}
public void give(MM mm, AbsGift g) {
}
/**
* 使用桥接模式:
* 分离抽象与具体实现,让他们可以独自发展
* Gift -> WarmGift WildGift ColdGift
* GiftImpl -> Flower Book Ring Car
*/
public static void main(String[] args) {
GG.builder().build().chase(new MM());
}
}
class MM {
String name;
}
abstract class AbsGiftImpl {
}
class Flower extends AbsGiftImpl {
}
class Book extends AbsGiftImpl {
}
// ------------------------------------------------------
abstract class AbsGift {
AbsGiftImpl impl;
}
class WarmGift extends AbsGift {
public WarmGift(AbsGiftImpl impl) {
this.impl = impl;
}
}
class WildGift extends AbsGift {
public WildGift(AbsGiftImpl impl) {
this.impl = impl;
}
}
4. 总结
1. 优点
- 分离抽象接口及其实现部分。《对象间的关联关系》解耦了抽象和实现之间固有的绑定关系,使抽象、实现可以沿着各自的维度来变化
- 很多情况下,桥接模式可以取代多层继承方案。多层继承方案违背了单一职责原则,复用性差,类的个数多
- 桥接模式提高了系统的扩展性,在两个变化维度中任意扩展一个维度都不需要修改原有系统,符合单一职责原则、开闭原则
2. 缺点
- 增加系统的理解和设计难度,由于关联关系建立在抽象层,要求开发者一开始就要对抽象层进行设计和编程
- 要求正确识别出系统中的两个独立变化的维度,因此具有一定的局限性,并且如果正确的进行维度的划分,也需要相当丰富的经验
3. 使用场景
- 需要提供平台独立性的应用程序时
- eg:不同数据库的 JDBC 驱动程序、硬盘驱动程序等
- 需要在某种统一协议下增加更多组件时
- eg:在支付场景中,期望支持微信、支付宝、各大银行的支付组件等。这里的统一协议是收款、支付、扣款,而组件就是微信、支付宝等
- 基于消息驱动的场景。 虽然消息的行为比较统一,主要包括发送、接收、处理和回执,但其实具体客户端的实现通常却各不相同
- eg:手机短信、邮件消息、QQ消息、微信消息等
- 拆分复杂的类对象时
- 当一个类中包含大量对象和方法时,既不方便阅读,也不方便修改
- 希望从多个独立维度上扩展时
- eg:系统功能性和非功能性角度,业务或技术角度等
3. 装饰器模式
- 装饰模式(Decorator Pattern)原始定义:动态给一个对象添加一些额外职责。就扩展功能而言,装饰器模式提供了一种比使用子类更加灵活的替代方案
1. 介绍
- 在软件设计中,装饰器模式是一种用于替代继承的技术,无须定义子类的方式给对象动态的增加职责,用对象间的关联关系取代类之间的继承关系
假设现在有一块蛋糕,如果只涂上奶油就是普通的奶油蛋糕,如果添加上一些蓝莓,就是蓝莓蛋糕。如果再拿一块黑巧克力,写上姓名、插上代表年龄的蜡烛,就是一块生日蛋糕
2. 原理
- 抽象构件角色(Component):具体构件、抽象装饰类的共同父类,声明了具体构件中实现的业务方法。使客户端以一致的方式处理被装饰对象以及装饰后对象,实现客户端的透明操作
- 具体构件角色(Concrete Component):抽象构件类子类。定义具体的构建对象,实现了在抽象构建中声明的方法
- 抽象装饰角色(Decorator):抽象构件类子类。给具体构件增加职责,在其子类中实现。维护了一个指向抽象构件对象的引用,调用被装饰方法
- 具体装饰角色(ConcreteDecorator):抽象装饰类子类。负责向构件添加新的职责
/**
* 抽象构建类
*/
public abstract class AbsComponent {
// 抽象方法
public abstract void oper();
}
/**
* 具体构建类(被装饰类)
*/
public class ConcreteComponent extends AbsComponent {
public void oper() {
// 基础功能的实现(一些复杂功能通过装饰类进行扩展)
}
}
/**
* 抽象装饰类 - 装饰者模式的核心
*/
public abstract class AbsDecorator extends AbsComponent {
// 维持一个对抽象构件对象的引用
private final AbsComponent component;
// 通过构造注入一个抽象构件类型的对象
public AbsDecorator(AbsComponent component) {
this.component = component;
}
public void oper() {
// 调用原有的业务方法, 并没有真正的进行装饰, 是提供了一个统一的接口, 将装饰的过程交给子类完成
component.oper();
}
}
/**
* 具体装饰类
*/
public class ConcreteDecorator extends AbsDecorator {
public ConcreteDecorator(AbsComponent component) {
super(component);
}
@Override
public void oper() {
// 调用原有的业务方法
super.oper();
// 调用新增的方法
ooxx();
}
// 新增业务方法
public void ooxx() {
// ...
}
}
3. 应用实例
1. 文件读写器
/**
* 抽象的文件读取接口
*/
public abstract class AbsDataLoader {
public abstract String read();
public abstract void write(String data);
}
/**
* 具体组件: 抽象文件读取接口的实现类
*/
public class DataLoaderFile extends AbsDataLoader {
private final String filePath;
public DataLoaderFile(String filePath) {
this.filePath = filePath;
}
// 读
public String read() {
try {
String result = FileUtils.readFileToString(new File(filePath), "utf-8");
return result;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 写
public void write(String data) {
try {
FileUtils.writeStringToFile(new File(filePath), data, "utf-8");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 抽象装饰者类
*/
public abstract class AbsDataLoaderDecorator extends AbsDataLoader {
private final AbsDataLoader dataLoader;
public AbsDataLoaderDecorator(AbsDataLoader dataLoader) {
this.dataLoader = dataLoader;
}
public String read() {
return dataLoader.read();
}
public void write(String data) {
dataLoader.write(data);
}
}
/**
* 具体装饰者类-对文件内容进行加密和解密
*/
public class EncryptDecorator extends AbsDataLoaderDecorator {
public EncryptDecorator(AbsDataLoader dataLoader) {
super(dataLoader);
}
@Override
public String read() {
return decode(super.read());
}
@Override
public void write(String data) {
super.write(encode(data));
}
// 加密操作
public String encode(String data) {
try {
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = data.getBytes("utf-8");
String result = encoder.encodeToString(bytes);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 解密操作
public String decode(String data) {
try {
Base64.Decoder decode = Base64.getDecoder();
String result = new String(decode.decode(data), "utf-8");
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class T_decorator {
public static void main(String[] args) {
String info = "name:tom, age:30";
// 1. 不加密
AbsDataLoader loader = new DataLoaderFile("demo.txt");
loader.write(info);
String read = loader.read();
System.out.println(read);
// 2. 加密
AbsDataLoader decorator = new EncryptDecorator(new DataLoaderFile("demo2.txt"));
decorator.write(info);
String dr = decorator.read();
System.out.println(dr);
}
}
2. 数据处理器
public abstract class AbsSyncDataComponent {
public abstract void handleData(Object obj);
}
public class SendMqComponent extends AbsSyncDataComponent {
@Override
public void handleData(Object obj) {
System.out.println("发送消息进MQ!" + obj);
}
}
@Setter
public abstract class AbsDecorator extends AbsSyncDataComponent {
// - 子类拥有父类private属性和方法,却无法用自身对象访问到
// - 只能通过super关键字访问
// - 子类被创建时,父类像饺子馅,子类的特有属性、方法像饺子皮
private AbsSyncDataComponent syncDataComponent;
public AbsDecorator(AbsSyncDataComponent syncDataComponent) {
this.syncDataComponent = syncDataComponent;
}
@Override
public void handleData(Object obj) {
if (this.syncDataComponent != null) {
this.syncDataComponent.handleData(obj);
}
}
}
public class DecoratorCheckParam extends AbsDecorator {
public DecoratorCheckParam(AbsSyncDataComponent syncDataComponent) {
super(syncDataComponent);
}
@Override
public void handleData(Object obj) {
System.out.println("校验接收的参数信息!" + obj);
super.handleData(obj);
}
}
public class DecoratorHandleData extends AbsDecorator {
public DecoratorHandleData(AbsSyncDataComponent syncDataComponent) {
super(syncDataComponent);
}
@Override
public void handleData(Object obj) {
System.out.println("处理数据!" + obj);
super.handleData(obj);
}
}
public class DecoratorLog extends AbsDecorator {
public DecoratorLog(AbsSyncDataComponent syncDataComponent) {
super(syncDataComponent);
}
@Override
public void handleData(Object obj) {
System.out.println("记录日志!" + obj);
super.handleData(obj);
}
}
@Test
public void fang() {
// 多线程访问是没有问题的,不过对象不能单例
for (int i = 0; i < 2; i++) {
new Thread(() -> {
new DecoratorCheckParam(
new DecoratorHandleData(
new DecoratorLog(
new SendMqComponent()
)
)
).handleData("{\"name\" : \"ooxx\"}");
}).start();
}
}
3. BufferedWriter
@Test
public void test3() throws Exception {
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(
new File("/Users/list/Downloads/test.json")
)
)
);
bw.write("http://www.ooxx.com");
bw.flush();
bw.close();
}
4. 总结
1. 优点
- 扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
- 通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
- 可以对一个对象进行多次装饰,使用不同的具体装饰类及不同的排列组合可以得到更加强大的对象
- 具体构建类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构建类和具体装饰类,符合开闭原则
2. 缺点
- 产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值不同,大量的小对象势必会占用更多系统资源,一定程度上影响程序性能
- 比继承更加灵活,同时比继承更加易于出错,排错也更加困难
3. 适用场景
- 快速动态扩展、撤销一个类的功能场景
- eg:有的场景下对API接口的安全性要求较高,就可以使用装饰模式对传输的数据进行压缩、加密。反之,不使用
- 不支持继承扩展类的场景。eg:使用
final
关键字的类,或者系统中存在大量通过继承产生的子类。
4. 适配器模式
- 适配器模式(Adapter Pattern)原始定义:将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作
1. 介绍
欧洲标准插座,左图。中国插头右图。需要一个插座转换器,使得插头在当地能使用。手机充电器(220v转换为5v电压),读卡器等
- 将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。两种实现方式:
- 类适配器。继承关系实现(耦合度高)
- 对象适配器。组合关系实现
2. 原理
- 目标接口(Target):当前系统业务所期待的接口,可以是抽象类或接口
- 适配者类(Adaptee):被适配的角色,它是被访问和适配的现存组件库中的组件接口
- 适配器类(Adapter):转换器。通过继承或引用适配者对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者
3. 应用实例
- 一台电脑目前只能读取SD卡信息,创建一个读卡器,使用电脑读取TF卡的内容
/**
* SD卡接口
*/
public interface ISDCard {
// 读取SD卡
String readSD();
// 写入SD卡
void writeSD(String msg);
}
/**
* SD卡实现类
*/
public class SDCardImpl implements ISDCard {
@Override
public String readSD() {
return "sd card reading data";
}
@Override
public void writeSD(String msg) {
System.out.println("sd card write data: " + msg);
}
}
/**
* TF卡接口
*/
public interface ITFCard {
// 读取TF卡
String readTF();
// 写入TF卡
void writeTF(String msg);
}
public class TFCardImpl implements ITFCard {
@Override
public String readTF() {
return "tf card reading data";
}
@Override
public void writeTF(String msg) {
System.out.println("tf card write data: " + msg);
}
}
/**
* 电脑类
*/
public class Computer {
public String read(ISDCard sdCard) {
return sdCard.readSD();
}
}
1. SDAdapterTF
1. 类适配器
/**
* 类适配器(SD兼容TF)
*/
public class SDAdapterTF_c extends TFCardImpl implements ISDCard {
@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);
}
}
@Test
public void clazz() {
Computer computer = new Computer();
ISDCard sdCard = new SDCardImpl();
String read = computer.read(sdCard);
System.out.println(read);
System.out.println("=====================");
SDAdapterTF_c adapterTF = new SDAdapterTF_c();
System.out.println(computer.read(adapterTF));
}
2. 对象适配器
- 釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口
/**
* 对象适配器-组合形式
*/
public class SDAdapterTF_o implements ISDCard {
private final ITFCard tfCard;
public SDAdapterTF_o(ITFCard 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);
}
}
@Test
public void obj() {
Computer computer = new Computer();
ISDCard sdCard = new SDCardImpl();
System.out.println(computer.read(sdCard));
System.out.println("=======================");
ITFCard tfCard = new TFCardImpl();
SDAdapterTF_o adapterTF = new SDAdapterTF_o(tfCard);
System.out.println(computer.read(adapterTF));
}
2. LogAdapter
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class LogInfo {
private String id;
private String log;
}
interface ILogToDB {
void writeLogToDB(LogInfo log);
}
// -----------------------------------------------------------------
@Data
public class LogAdapter implements ILogToDB {
private ILogToFile logToFile;
public LogAdapter(ILogToFile logToFile) {
this.logToFile = logToFile;
}
@Override
public void writeLogToDB(LogInfo log) {
System.out.println("日志写入到了数据库:" + log);
logToFile.writeLogToFile(log);
}
}
interface ILogToFile {
void writeLogToFile(LogInfo log);
}
// -----------------------------------------------------------------
class LogToFile implements ILogToFile {
@Override
public void writeLogToFile(LogInfo log) {
System.out.println("日志记录写入到文件中:" + log);
}
}
class z_main {
public static void main(String[] args) {
new LogAdapter(new LogToFile()).writeLogToDB(
new LogInfo("log-101", "新的需求是日志需要记录到文件的同时还要记录到数据库中!")
);
}
}
2. MediaAdapter
interface IAdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// ------------------------------------------------------------
class Mp4Player implements IAdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// 什么也不做
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
}
class VlcPlayer implements IAdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
// 什么也不做
}
}
interface IMediaPlayer {
void play(String audioType, String fileName);
}
public class MediaAdapter implements IMediaPlayer {
IAdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
class AudioPlayer implements IMediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
// 播放 mp3 音乐文件的内置支持
if ("mp3".equalsIgnoreCase(audioType)) {
System.out.println("Playing mp3 file. Name: " + fileName);
}
// mediaAdapter 提供了播放其他文件格式的支持
else if ("vlc".equalsIgnoreCase(audioType) || "mp4".equalsIgnoreCase(audioType)) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
class z_main {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}
4. 总结
1. 优点
- 将目标类、适配者类解耦。通过引入一个适配器类来重用现有的适配者类,无序修改原有结构
- 增加了类的透明性、复用性。将具体业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
- 灵活性、扩展性都非常好。通过使用配置文件可以很方便的更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,符合开闭原则
2. 缺点
- 类适配器
- 对于Java等不支持多重继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 适配者类不能为
final
类
- 对象适配器
- 与类适配器模式相比,在适配器中置换适配者类的某些方法比较麻烦
3. 适用场景
统一多个类的接口设计时
某个功能的实现依赖多个外部系统(或类)。通过适配器模式,将它们的接口适配为统一的接口定义
需要依赖外部系统时
把项目中依赖的一个外部系统替换为另一个外部系统时,利用适配器模式,可以减少对代码的改动
原有接口无法修改时或原有接口功能太老旧但又需要兼容
JDK1.0
Enumeration
到Iterator
的替换,适用适配器模式保留Enumeration
类,并将其实现替换为直接调用Itertor
适配不同数据格式时
Slf4j日志框架。定义了打印日志的统一接口,提供针对不同日志框架的适配器
4. 代理, 桥接, 装饰器, 适配器区别
代码结构非常相似。但各自用意却不同,它们之间的关系:
- 代理模式:在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,和装饰器模式最大不同
- 桥接模式:将接口部分、实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
- 装饰器模式:在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
- 适配器模式:将一个类的接口转换为客户希望的另一个接口。适配器模式让那些不兼容的类可以一起工作
5. 外观模式
- 外观(门面)模式(Facade Pattern),原始定义:为子系统中的一组接口提供统一的接口。它定义了一个更高级别的接口,使子系统更易于使用
/fəˈsɑːd/
1. 介绍
- 通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问。门面类充当了系统中的"服务员"
- 该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,大大降低应用程序的复杂度,提高程序的可维护性
- 门面模式有点类似迪米特法则(最少知识原则)和接口隔离原则:两个有交互的系统,只暴露有限的必要的接口
2. 原理
外观角色(Facade):为多个子系统对外提供一个共同的接口
知道多个相关的子系统中的功能和责任。在正常情况下,它将所有从客户端发来的请求委派到相应的子系统,传递给相应的子系统对象处理
子系统角色(Sub System):实现系统的部分功能,客户可以通过外观角色访问它
每一个子系统可以是一个类也可以是多个类的集合。每一个子系统都可以被客户端直接调用,或被外观角色调用。子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另一个客户端而已
public class SubSystemA {
public void methodA() {
// 业务代码
}
}
public class SubSystemB {
public void methodB() {
}
}
public class SubSystemC {
public void methodC() {
}
}
public class Facade {
private final SubSystemA obj1 = new SubSystemA();
private final SubSystemB obj2 = new SubSystemB();
private final SubSystemC obj3 = new SubSystemC();
public void method() {
obj1.methodA();
obj2.methodB();
obj3.methodC();
}
}
@Test
public void test1() {
Facade facade = new Facade();
facade.method();
}
3. 应用实例
- 通过智能音箱来统一控制室内的 灯、电视、空调
/**
* 电视类
*/
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 Light {
public void on() {
System.out.println("打开灯...");
}
public void off() {
System.out.println("关闭灯...");
}
}
/**
* 智能音箱外观类
*/
public class AppFacade {
private final Light light;
private final TV tv;
private final AirCondition airCondition;
public AppFacade() {
this.light = new Light();
this.tv = new TV();
this.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 T_facade {
@Test
public void test1() {
Facade facade = new Facade();
facade.method();
}
@Test
public void test2() {
// 创建外观对象
AppFacade facade = new AppFacade();
facade.say("打开家电");
facade.say("关闭家电");
}
}
4. 总结
1. 优点
- 客户端屏蔽了子系统组件,减少了客户端所需处理对象数目
- 子系统与客户端之间松耦合,子系统的变化不会影响到客户端,只需要调整外观类即可
- 一个子系统的修改对其他子系统没有任何影响,而子系统内部变化也不会影响到外观对象
2. 缺点
- 不能很好的控制客户端直接使用子系统类,减少了可变性和灵活性
- 设计不当,增加新的子系统可能需要修改外观类,违背开闭原则
3. 使用场景
简化复杂系统
一整套的电商系统(包括订单、商品、支付、会员等系统),不能让用户依次使用这些系统后才能完成商品的购买,而是需要一个门户网站或App简化过的门面系统来提供在线的购物功能
减少客户端处理的系统数量
在Web应用中,持久层需要处理打开DB、转换Model对象、关闭连接等操作,太麻烦了。创建一个DB使用的门面(DAO层)
让一个系统(或对象)为多个系统(或对象)工作
线程池 ThreadPool 就是一个门面模式,它为系统提供了统一的线程对象的创建、销毁、使用等
联合更多的系统来扩展原有系统。 当我们的电商系统中需要一些新功能时
人脸识别,不需要自行研发,购买别家公司的系统来提供服务,这时通过门面系统就能方便快速地进行扩展
作为一个简洁的中间层。 门面模式还可以用来隐藏或封装系统中的分层结构,同时作为一个简化的中间层来使用
在秒杀、库存、钱包等场景中,需要共享有状态的数据时(eg:商品库存、账户里的钱),在不改变原有系统前提下,通过一个中间的共享层(eg:将秒杀活动的商品库存总数统一放在Redis里),就能统一进行各种服务(eg:秒杀详情页、商品详情页、购物车等)的调用
6. 组合模式
- 组合模式(Composite Pattern)定义:将对象组合成树形结构以表示整个部分的层次结构。可以让用户统一对待单个对象和对象的组合
1. 介绍
很容易将“组合模式”和“组合关系”搞混
- 组合模式:用于解决树形结构场景,更多的是处理对象组织结构之间的问题
- 组合关系:将不同对象封装起来完成一个统一功能
eg:windows中的目录结构,其实就是树形目录结构,通过tree命令实现树形结构展示
- 包含了文件夹、文件两类不同元素,其中在文件夹中可以包含文件、子文件夹。文件夹形成了一种容器结构(树形结构)
- 思考:虽然文件夹、文件是不同类型对象,但是有一个共性,都可以被放入文件夹中。其实可以当做是同一种对象看待
- 组合模式:将一组对象(文件夹、文件)组织成树形结构,以表示一种'部分-整体'的层次结构(目录与子目录的嵌套结构)
- 组合模式让客户端可以统一单个对象(文件)、组合对象(文件夹)的处理逻辑(递归遍历)
- 组合模式更像是一种数据结构和算法的抽象,其中数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现
2. 原理
抽象根节点(Component):定义系统各层次对象的共有方法、属性,可以预先定义一些默认行为、属性
包含所有子类共有行为的声明和实现。在抽象根节点中定义了访问、管理其子构件的方法。eg:增加子节点、删除子节点、获取子节点等
树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点、叶子节点形成一个树形结构
- 包含树枝节点或叶子节点,有一个属性存储子节点集合
- 实现抽象根节点中定义的行为(访问及管理子构件的方法),在其业务方法中可以递归调用子节点的业务方法
叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位
没有子节点,实现抽象根节点中定义的行为
3. 实现
- 组合模式关键:定义一个抽象根节点类,它既可以代表叶子,又可以代表树枝节点
- 客户端就是针对该抽象类进行编程,不需要知道它到底表示的是叶子还是容器,可以对其进行统一处理
- 树枝节点对象和抽象根节点类之间建立了一个聚合关联关系,在树枝节点对象中可以包含叶子节点、树枝节点,以此实现递归组合,形成一个树形结构
/*
* 抽象根节点角色
* 对客户端而言, 只需要针对抽象编程, 无需关心具体子类是树枝节点还是叶子节点
*/
public abstract class Component {
// 增加节点
public abstract void add(Component c);
// 删除节点
public abstract void remove(Component c);
// 获取节点
public abstract Component getChild(int i);
// 业务方法
public abstract void operation();
}
/*
* 树枝节点
* 树枝节点类是一个容器对象, 它既可以包含树枝节点也可以包含叶子节点
*/
public class Composite extends Component {
// 定义集合属性, 保存子节点的数据
private final ArrayList<Component> list = new ArrayList<>();
@Override
public void add(Component c) {
list.add(c);
}
@Override
public void remove(Component c) {
list.remove(c);
}
@Override
public Component getChild(int i) {
return list.get(i);
}
// 具体业务方法
@Override
public void operation() {
// 在循环中, 递归调用其他节点中的operation()
for (Component component : list) {
component.operation();
}
}
}
/*
* 叶子节点
* 叶子节点中不包含子节点
*/
public class Leaf extends Component {
@Override
public void add(Component c) {
}
@Override
public void remove(Component c) {
}
@Override
public Component getChild(int i) {
return null;
}
@Override
public void operation() {
// 叶子节点中的具体方法
}
}
4. 应用实例
1. 目录下所有文件
- Entry类:抽象类,用来定义
File
、Directory
共性内容
/**
* Entry抽象类 (文件夹+文件)
*/
@Getter
public abstract class AbsEntry {
// 文件的名字
public String name;
// 获取文件大小
public abstract int getSize();
// 显示指定目录下的所有文件的信息
public abstract void printList(String prefix);
@Override
public String toString() {
return getName() + "(" + getSize() + ")";
}
}
- File类,叶子节点,表示文件
/**
* File类, 表示文件
*/
public class File extends AbsEntry {
// 文件大小
private final int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public int getSize() {
return this.size;
}
@Override
public void printList(String prefix) {
System.out.println(prefix + "/" + this);
}
}
- Directory类,树枝节点,表示文件
/**
* Directory 容器对象, 表示文件夹
*/
public class Directory extends AbsEntry {
// 文件夹和文件的集合
private final ArrayList<AbsEntry> directory = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
/*
* 获取文件大小
* 1.如果entry对象是file类型,则调用getSize方法获取文件大小
* 2.如果entry对象是Directory类型,会继续调用子文件夹的getSize()方法,形成递归调用
*/
@Override
public int getSize() {
int size = 0;
// 遍历获取文件大小
for (AbsEntry entry : directory) {
size += entry.getSize();
}
return size;
}
// 添加文件或者文件夹方法
public AbsEntry add(AbsEntry entry) {
directory.add(entry);
return this;
}
@Override
public void printList(String prefix) {
System.out.println("/" + this);
for (AbsEntry entry : directory) {
entry.printList("/" + name);
}
}
}
@Test
public void test1() {
// 创建根节点
Directory rootDir = new Directory("root");
// 创建树枝节点
Directory binDir = new Directory("bin");
// 向bin目录添加叶子节点
binDir.add(new File("vi", 10_000));
binDir.add(new File("test", 20_000));
Directory tmpDir = new Directory("tmp");
Directory usrDir = new Directory("usr");
Directory mysqlDir = new Directory("mysql");
mysqlDir.add(new File("my.cnf", 30));
mysqlDir.add(new File("test.db", 25_000));
usrDir.add(mysqlDir);
// 将所有子文件夹封装到根节点
rootDir.add(binDir);
rootDir.add(tmpDir);
rootDir.add(usrDir);
rootDir.printList("");
}
2. AbsNode
/**
* 组合模式
* 树状结构专用模式
*/
public abstract class AbsNode {
String name;
abstract public void print();
}
class LeafNode extends AbsNode {
public LeafNode(String name) {
this.name = name;
}
@Override
public void print() {
System.out.println(name);
}
}
class BranchNode extends AbsNode {
List<AbsNode> nodes = new ArrayList<>();
public BranchNode(String name) {
this.name = name;
}
@Override
public void print() {
System.out.println(name);
}
public void add(AbsNode n) {
nodes.add(n);
}
}
@Test
public void test2() {
BranchNode root = new BranchNode("root");
BranchNode chapter1 = new BranchNode("chapter1");
BranchNode chapter2 = new BranchNode("chapter2");
AbsNode r1 = new LeafNode("r1");
AbsNode c11 = new LeafNode("c11");
AbsNode c12 = new LeafNode("c12");
BranchNode b21 = new BranchNode("section21");
AbsNode c211 = new LeafNode("c211");
AbsNode c212 = new LeafNode("c212");
root.add(chapter1);
root.add(chapter2);
root.add(r1);
chapter1.add(c11);
chapter1.add(c12);
chapter2.add(b21);
b21.add(c211);
b21.add(c212);
tree(root, 0);
}
static void tree(AbsNode b, int depth) {
// 处理层级缩进
for (int i = 0; i < depth; i++) System.out.print("|--");
b.print();
if (b instanceof BranchNode) {
for (AbsNode n : ((BranchNode) b).nodes) {
// 处理层级缩进
tree(n, depth + 1);
}
}
}
root
|--chapter1
|--|--c11
|--|--c12
|--chapter2
|--|--section21
|--|--|--c211
|--|--|--c212
|--r1
3. listao_doc
/**
* Entry抽象类 (文件夹 + 文件)
*/
@Getter
public abstract class Entry {
protected String name;
// 显示指定目录下的所有文件的信息
public abstract String toString(String prefix, String tab);
}
/**
* File类, 表示文件
*/
public class FFile extends Entry {
public FFile(String text) {
this.name = text;
}
@Override
public String toString(String prefix, String tab) {
return tab + "'" + prefix + "/" + this.getName() + "',\n";
}
}
/**
* Directory 容器对象, 表示文件夹
*/
public class DDir extends Entry {
// 文件夹和文件的集合
public final ArrayList<Entry> dirs = new ArrayList<>();
public DDir(String name) {
this.name = name;
}
public Entry add(Entry entry) {
dirs.add(entry);
return this;
}
@Override
public String toString(String prefix, String tab) {
StringBuilder s = new StringBuilder();
// 子节点排序
dirs.sort(new Comparator_fileName());
// 打印子节点
for (Entry entry : dirs) {
s.append(entry.toString(prefix + "/" + this.getName(), tab + "\t\t"));
}
// 一级目录只打印其子节点
if (!tab.contains("\t")) {
return "const " + this.getName() + "Menu = [\n" + s
+ "]\n" + "\n"
+ "export {\n"
+ "\t" + this.getName() + "Menu,\n"
+ "}";
}
return tab + "{\n" +
tab + "\ttext: '" + this.getName() + "',\n" +
tab + "\tcollapsible: true,\n" +
tab + "\tchildren: [\n" + s +
tab + "\t]\n" +
tab + "},\n";
}
/**
* 排序规则
* 1. 文件夹类型优先
* 2. 类型一样,名称自然顺序
*/
static class Comparator_fileName implements Comparator<Entry> {
@Override
public int compare(Entry e1, Entry e2) {
String o1;
String o2;
boolean isDir; // 文件夹优先标识
isDir = !(e1 instanceof FFile);
o1 = e1.getName();
if (e2 instanceof FFile) {
// 1. o1文件夹、o2文件,不交换
if (isDir) {
return -1;
}
} else {
// 2. o1文件、o2文件夹,交换
if (!isDir) {
return 1;
}
}
o2 = e2.getName();
// 3. o1、o2同为文件夹|文件
return o1.compareTo(o2);
}
}
}
public class X_Menu {
@Test
public void test1() {
String outputPath = "/Users/listao/mca/listao_doc/docs/.ooxx/menuVariable/";
String rootPath = "/Users/listao/mca/listao_doc/docs/";
ArrayList<String> menu = CollUtil.newArrayList("dev", "java", "life", "os", "proj");
for (String one : menu) {
String rootPath_tmp = rootPath + one;
DDir rootDir = new DDir(one);
// 1. 递归生成一级目录
File file = new File(rootPath_tmp);
toDir(file, rootDir);
// 2. 打印对象,覆盖文件到指定位置
String outputPath_tmp = outputPath + one + ".js";
FileWriter writer = new FileWriter(outputPath_tmp);
writer.write(rootDir.toString("", ""));
}
}
private void toDir(File rootFile, DDir rootDir) {
File[] ls = FileUtil.ls(rootFile.getPath());
if (ls.length < 1) { // base case
return;
}
for (File f : ls) {
// 路径下一个文件
String fName = FileNameUtil.mainName(f);
// .开头的文件排除
if (StrUtil.isNotBlank(fName)) {
if (FileUtil.isDirectory(f) && !fName.contains("img")) {
DDir dir = new DDir(fName);
rootDir.add(dir);
toDir(f, dir);
} else if (FileUtil.isFile(f) && !fName.contains("README")) {
FFile fFile = new FFile(fName);
rootDir.add(fFile);
}
}
}
}
}
5. 总结
1. 分类
- 透明组合模式(标准形式)
- 抽象根节点角色中,声明了所有管理成员对象的方法
- eg:在示例中
Component
声明了add()
、remove()
、getChild()
,确保所有的构件类都有相同的接口
- eg:在示例中
- 缺点:不够安全。叶子对象、容器对象在本质上是有区别的,叶子不可能包含成员对象,因此为其提供
add()
、remove()
没有意义,编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(没有提供相应的错误处理代码)
- 抽象根节点角色中,声明了所有管理成员对象的方法
- 安全组合模式
- 抽象构件角色中,没有声明任何用于管理成员对象的方法,而是在树枝节点类中单独声明、实现
- 缺点:不够透明,因为叶子构件、容器构件具有不同方法。因此客户端不能完全针对抽象编程,必须有区别地对待叶子、容器构件
2. 优点
- 可以清楚地定义分层次的复杂对象,表示对象的全部、部分层次。它让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
- 增加新的树枝节点、叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”
- 为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点、树枝节点的递归组合,可以形成复杂的树形结构,对树形结构的控制却非常简单
3. 缺点
- 应用场景也比较局限,需要表示成树形结构
4. 场景分析
- 处理一个树形结构。eg:公司人员组织架构、订单信息等
- 跨越多个层次结构聚合数据。eg:统计文件夹下文件总数
- 统一处理一个结构中的多个对象。eg:遍历文件夹下所有 XML 类型文件内容
7. 享元模式
- 享元模式(Flyweight Pattern)原始定义:摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,从而让我们能在有限的内存容量中载入更多对象
1. 介绍
- 作用:节约内存空间,找出相似对象之间的共有特征,然后复用这些特征。所谓“享元”,顾名思义就是被共享的单元
- eg:一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间
- 享元模式通过共享技术实现相同、相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,物理上他们共享同一个享元对象
- 两种状态:享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
- 内部状态:不会随着环境的改变而改变的可共享部分
- 外部状态:随着环境的改变而改变的不可以共享的部分
2. 原理
- 享元模式的结构较为复杂,通常会结合工厂模式一起使用,结构图中包含了一个享元工厂类
- 抽象享元角色(Flyweight):一个接口、抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
- 可共享的具体享元角色(Concrete Flyweight):为内部状态提供了存储空间。通常结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象
- 非共享的具体享元角色(Unshared Flyweight):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类为非共享具体享元类。可以直接通过实例化创建
- 享元工厂角色(Flyweight Factory):负责创建、管理享元角色。当客户对象请求一个享元对象时,享元工厂如果存在则提供;如果不存创建存储并提供
3. 实现
/**
* 抽象享元类
*/
public abstract class AbsFlyweight {
public abstract void operation(String state);
}
- 具体享元类:要将内部状态、外部状态分开处理,内部状态作为具体享元类的成员变量,而外部状态通过注入的方式添加到具体享元类中
/*
* 可共享的 - 具体享元类
* 注意: 在具体享元类中, 需要将内部状态和外部状态分开处理
*/
public class ConcreteFw extends AbsFlyweight {
// 内部状态: inState作为一个成员变量, 同一个享元对象的内部状态是一致的
private final String inState;
public ConcreteFw(String inState) {
this.inState = inState;
}
/**
* 外部状态在使用的时候, 通常是有外部设置, 不保存在享元对象中, 即使是同一个对象
*/
@Override
public void operation(String state) {
System.out.println("=== 享元对象的内部状态: " + inState + ", 外部状态: " + state);
}
}
- 非共享享元类:不复用享元工厂内部状态,是抽象享元类的子类或实现类
/**
* 非共享 - 具体享元类
*/
public class UnsharedFlyweight extends AbsFlyweight {
private final String inState;
public UnsharedFlyweight(String inState) {
this.inState = inState;
}
@Override
public void operation(String state) {
System.out.println("=== 使用不共享对象, 内部状态: " + inState + ", 外部状态: " + state);
}
}
- 享元工厂类:管理一个享元对象类的缓存池。存储享元对象之间需要传递的共有状态
/*
* 享元工厂类
* 作用: 作为存储享元对象的享元池. 用户获取享元对象时先从享元池中获取, 有则返回, 没有创建保存并返回
*/
public class FlyweightFactory {
// 定义一个Map集合用于存储享元对象,实现享元池
private final Map<String, AbsFlyweight> pool = new HashMap<>();
// 实现享元对象之间的状态传递
public FlyweightFactory() {
// 添加对应的内部状态
pool.put("A", new ConcreteFw("A"));
pool.put("B", new ConcreteFw("B"));
pool.put("C", new ConcreteFw("C"));
}
// 根据内部状态进行查找
public AbsFlyweight getFlyweight(String key) {
// 对象是否存在
if (pool.containsKey(key)) {
System.out.println("=== 享元池中存在, 直接复用, key : " + key);
return pool.get(key);
} else {
// 如果对象不存在, 就创建一个添加到享元池
System.out.println("=== 享元池中不存在, 创建并复用, key: " + key);
AbsFlyweight fw = new ConcreteFw(key);
pool.put(key, fw);
return fw;
}
}
}
@Test
public void test1() {
// 获取工厂对象
FlyweightFactory factory = new FlyweightFactory();
// 通过工厂对象获取共享的享元对象
AbsFlyweight a1 = factory.getFlyweight("A");
a1.operation("a1ExState");
AbsFlyweight a2 = factory.getFlyweight("A");
a2.operation("a2ExState");
System.out.println(a1 == a2);
// 获取非共享的享元对象
UnsharedFlyweight u1 = new UnsharedFlyweight("A");
UnsharedFlyweight u2 = new UnsharedFlyweight("A");
System.out.println(u1 == u2);
}
4. 应用实例
1. 五子棋
- 五子棋有黑子、白子。形状大小都一样,只是出现的位置不同。所以一个棋子作为一个独立的对象存储在内存中,导致大量的内存浪费,使用享元模式进行优化
/**
* 抽象享元类: 五子棋
*/
public abstract class AbsGobangFlyweight {
public abstract String getColor();
public void display() {
System.out.println("棋子颜色: " + this.getColor());
}
}
/**
* 共享享元类 - 黑色棋子
*/
public class GobangBlack extends AbsGobangFlyweight {
@Override
public String getColor() {
return "黑色";
}
}
/**
* 共享享元类 - 白色棋子
*/
public class GobangWrite extends AbsGobangFlyweight {
@Override
public String getColor() {
return "白色";
}
}
/**
* 享元工厂类 - 生产五子棋棋子
*/
public class GobangFactory {
// 享元池
private static Map<String, AbsGobangFlyweight> pool;
// 创建共享享元对象, 设置对象的内部状态
private GobangFactory() {
pool = new HashMap<>();
// 黑子
AbsGobangFlyweight black = new GobangBlack();
// 白子
AbsGobangFlyweight write = new GobangWrite();
pool.put("b", black);
pool.put("w", write);
}
// 获取唯一享元工厂对象的方法
public static GobangFactory getInstance() {
return SingletonHolder.INSTANCE;
}
// 静态内部类
private static class SingletonHolder {
private static final GobangFactory INSTANCE = new GobangFactory();
}
// 通过key获取集合中的享元对象
public AbsGobangFlyweight getGobang(String key) {
return pool.get(key);
}
}
@Test
public void test2() {
GobangFactory instance = GobangFactory.getInstance();
// 获取3颗黑子
AbsGobangFlyweight b1 = instance.getGobang("b");
AbsGobangFlyweight b2 = instance.getGobang("b");
AbsGobangFlyweight b3 = instance.getGobang("b");
System.out.println("判断黑子是否是同一对象: " + (b1 == b2)); // true
AbsGobangFlyweight w1 = instance.getGobang("w");
AbsGobangFlyweight w2 = instance.getGobang("w");
System.out.println("判断白子是否是同一对象: " + (w1 == w2)); // true
// 显示棋子
b1.display();
b2.display();
b3.display();
w1.display();
w2.display();
}
- 三颗黑子(两颗白子)对象比较之后内存地址都是一样的。说明它们是同一个对象。实现享元模式时使用了单例模式、简单工厂模式,保证了享元工厂对象的唯一性,并提供工厂方法向客户端返回享元对象
2. String
@Test
public void test4() {
String s1 = "abc";
String s2 = "abc";
// new堆里的对象
String s3 = new String("abc");
String s4 = new String("abc");
// java所有的字符串都放到一个常量池里,已经有的直接指向
// s1 s2 的引用是一样的
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s3 == s4);
// intern()对象里指向常量的引用
System.out.println(s3.intern() == s1);
System.out.println(s3.intern() == s4.intern());
}
5. 总结
1. 优点
极大减少内存中相似、相同对象数量,节约系统资源,提供系统性能
- 当大量商家的商品图片、固定文字(商品介绍、商品属性)在不同的网页进行展示时,通常不需要重复创建对象,以避免重复存储而浪费内存空间
- 通过享元模式构建的对象是共享的,所以当程序运行时不仅不用重复创建,还能减少程序与操作系统的IO交互次数,大大提升了读写性能
外部状态相对独立,且不影响内部状态
2. 缺点
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
3. 使用场景
- 一个系统有大量相同、相似的对象,造成内存的大量耗费
- 注意:维护一个存储享元对象的享元池,需要耗费一定的系统资源。应当在需要多次重复使用享元对象时,才值得使用享元模式
- 享元模式本质:找到对象的不可变特征,并缓存起来,当类似对象使用时从缓存中读取,以达到节省内存空间的目的
@Test
public void test3() {
Integer i1 = 127;
Integer i2 = 127;
// -128 ~ 127 常用。进行享元
System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));
Integer i3 = 128;
Integer i4 = 128;
System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
}
- Java中,享元模式常用场景,使用数据类的包装类对象的
valueOf()
- 使用
Integer.valueOf()
时,实际的代码实现中有IntegerCache
静态类,它缓存了(-127 ~ 128)
数值 - 如果参数在
-128 ~ 127
间则计算下标,从缓存中返回,否则创建一个新的Integer
对象
- 使用
public final class Integer extends Number implements Comparable<Integer> {
// 传入的值在 `(-128 - 127)` 之间,直接从缓存中返回
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
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) {
// If the property cannot be parsed into an int, ignore it.
}
}
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() {}
}
}