设计模式の享元&模板&代理模式

文章目录


前言

本篇是关于设计模式中享元模式、模板模式、以及代理模式的学习笔记。


一、享元模式

享元模式是一种结构型设计模式,目的是为了相似对象的复用,减少内存的消耗,在享元模式中,主要包含以下的角色:

  • Flyweight(抽象享元):定义享元对象的接口,规定了可以共享的内容。
  • ConcreteFlyweight(不可变对象):实现抽象享元接口,封装内部状态,确保可以被共享。
  • UnsharedConcreteFlyweight(可变对象):不需要共享的对象。
  • FlyweightFactory(享元工厂):管理和创建享元对象,确保可以复用已经存在的享元。
  • Client(客户端):使用享元对象,负责将外部状态传递给享元对象。

下面通过一个案例说明一下,例如围棋,有黑白两种颜色的棋子,如果为每个棋子都创建一次对象,会占用很大的内存。如果利用享元模式进行改造,可以将下棋的行为看做是抽象享元,而棋子可以视为实现了抽象享元接口的不可变对象下棋的选手可以看作是可变对象

java 复制代码
public interface PlayChess {

    /**
     * 下棋
     * @param player
     */
    void playChess(Player player);

}
java 复制代码
/**
 * 棋子类
 */
public class GochessPieces implements PlayChess{

    private String color;

    public GochessPieces() {
    }

    public GochessPieces(String color) {
        this.color = color;
    }

    @Override
    public void playChess(Player player) {
        System.out.println(player + "落" + color);
    }
}
java 复制代码
/**
 * 玩家
 */
public class Player {

    private String name;

    public Player() {
    }

    public Player(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Player{" +
                "name='" + name + '\'' +
                '}';
    }
}
java 复制代码
/**
 * 享元工厂
 */
public class ChessFactory {

    private HashMap<String,PlayChess> map = new HashMap<>();

    public PlayChess getChess(String color){
        if (!map.containsKey(color)){
            map.put(color,new GochessPieces(color));
        }
        return (PlayChess)map.get(color);
    }

    public int poolSize(){
        return map.size();
    }
}

模拟下棋的操作:

java 复制代码
/**
 * 模拟下棋
 */
public class Client {
    public static void main(String[] args) {
        ChessFactory chessFactory = new ChessFactory();
        PlayChess chess1 = chessFactory.getChess("白子");
        chess1.playChess(new Player("张三"));

        PlayChess chess2 = chessFactory.getChess("黑子");
        chess2.playChess(new Player("张三"));


        PlayChess chess3 = chessFactory.getChess("白子");
        chess3.playChess(new Player("李四"));


        PlayChess chess4 = chessFactory.getChess("黑子");
        chess4.playChess(new Player("李四"));


        PlayChess chess5 = chessFactory.getChess("白子");
        chess5.playChess(new Player("张三"));


        System.out.println("chessFactory.poolSize() = " + chessFactory.poolSize());
    }
}

Player{name='张三'}落白子

Player{name='张三'}落黑子

Player{name='李四'}落白子

Player{name='李四'}落黑子

Player{name='张三'}落白子

chessFactory.poolSize() = 2

可以看出,虽然棋子被多次利用,但是在享元工厂中,最多只有两个棋子对象,即黑子白子,其关键点在于:

  • 共享对象:黑棋和白棋各只有一个实例,无需为每个棋子创建新对象。
  • 内外部状态分离:颜色作为内部状态是共享的;位置作为外部状态在调用时传入。
  • 减少内存占用:通过共享棋子对象,大幅减少了内存开销。

二、模板方法模式

模板方法模式是一种行为设计模式,通过定义一个算法框架,允许子类在不改变算法结构的情况下重新定义算法的某些步骤。模板方法模式的主要目的是将算法的不变部分抽取到父类中,而将可变部分留给子类去实现,提高代码的复用性和灵活性,其中还涉及到钩子方法的使用。

举一个生活中的案例,比如制作豆浆的过程,有相同的操作,也有针对不同类型的豆浆采取的特殊处理,用一个抽象的模板方法类,说明制作豆浆的步骤,以及对外提供制作的方法:

java 复制代码
public abstract class CreateSoyaMilk {
    
    final void make(){
        prepare();
        if (isSoakBeans()){
            soakBeans();
        }
        grinding();
        boil();
    }

    /**
     * 钩子方法
     * @return
     */
    boolean isSoakBeans(){
        return true;
    }

    /**
     * 准备材料
     * 不同的豆浆准备不同材料
     */
    public abstract void prepare();

    /**
     * 浸泡豆子
     * 不同的豆浆浸泡不同的豆子
     */
    public abstract void soakBeans();

    public void grinding(){
        System.out.println("打磨豆浆");
    }

    public void boil(){
        System.out.println("煮沸豆浆");
    }
}

对于制作不同类型的豆浆,只需要继承模板方法类,然后根据自己的特点进行特殊处理即可:

java 复制代码
public class RedBeanSoybeanMilk extends CreateSoyaMilk{
    @Override
    public void prepare() {
        System.out.println("准备红豆");
    }

    @Override
    public void soakBeans() {
        System.out.println("浸泡红豆");
    }
}

如果黄豆豆浆不需要浸泡,则重写模板中的钩子方法

java 复制代码
public class YellowBeanSoybeanMilk extends CreateSoyaMilk{
    @Override
    public void prepare() {
        System.out.println("准备黄豆");
    }

    /**
     * 假设黄豆豆浆不需要浸泡
     */
    @Override
    public void soakBeans() {

    }

    @Override
    boolean isSoakBeans() {
        return false;
    }
}

制作豆浆:

java 复制代码
public class Client {
    public static void main(String[] args) {
        new RedBeanSoybeanMilk().make();
        System.out.println("**************************");
        new YellowBeanSoybeanMilk().make();
    }
}

准备红豆

浸泡红豆

打磨豆浆

煮沸豆浆


准备黄豆

打磨豆浆

煮沸豆浆

模板方法模式也存在一定的局限性,因为是使用继承的体系结构,如果父类的修改影响较大,会导致所有子类需要同步更新。通常适用于有固定的算法结构,但子类的实现细节不同的场景。

三、代理模式

代理模式是一种结构型设计模式,其核心目的是为了在使用目标对象的前后,对其进行拦截,增加一些统一的操作或是校验,是一种非常重要的模式,Spring AOP就是基于代理实现。

代理模式可以分为静态代理动态代理,动态代理又可以细分为JDK动态代理Cglib动态代理。前者需要目标类实现接口,后者不需要,会在运行时动态生成目标类的子类。

3.1、静态代理

首先演示一下什么是静态代理,静态代理要求目标类和代理类都要同时去实现接口:

java 复制代码
public interface ITeacher {
    void teach();
}
java 复制代码
public class TeacherDao implements ITeacher{
    @Override
    public void teach() {
        System.out.println("开始上课。。。。");
    }
}
java 复制代码
/**
 * 静态代理案例
 * 缺点,如果接口中要增加方法,被代理类和代理类都需要同步增加
 */
public class TeacherDaoProxy implements ITeacher{

    private ITeacher teacher;

    public TeacherDaoProxy(ITeacher teacher){
        this.teacher = teacher;
    }


    @Override
    public void teach() {
        System.out.println("课前准备。。。。");
        teacher.teach();
        System.out.println("下课。。。。。");
    }
}

课前准备。。。。

开始上课。。。。

下课。。。。。

3.2、JDK动态代理

JDK动态代理的实现,主要依靠java.lang.reflect.Proxy包下的newProxyInstance方法,该方法会在运行时动态的生成代理类,详见:从源码角度分析JDK动态代理

java 复制代码
public interface ITeacher {
    void teach();
}
java 复制代码
public class TeacherDao implements ITeacher {
    @Override
    public void teach() {
        System.out.println("开始上课。。。。");
    }
}
java 复制代码
public class JdkProxyFactory {

    /**
     * 被代理的目标类
     */
    private Object target;

    public JdkProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    System.out.println("课前准备");
                    //通过反射调用目标方法
                    Object o = method.invoke(target, args);
                    System.out.println("下课");
                    return o;
                });
    }
}
java 复制代码
public class Client {
    public static void main(String[] args) {

        JdkProxyFactory jdkProxyFactory = new JdkProxyFactory(new TeacherDao());
        ITeacher proxyInstance = (ITeacher) jdkProxyFactory.getProxyInstance();
        proxyInstance.teach();
    }
}

3.3、Cglib动态代理

JDK动态代理的实现,主要依靠org.springframework.cglib.proxy.MethodInterceptor包下的intercept方法,和JDK动态代理不同的是,不要求目标类实现接口,而是直接通过字节码技术生成目标类的子类

java 复制代码
public class TeacherDao{
   
    public void teach() {
        System.out.println("开始上课。。。。");
    }
}
java 复制代码
/**
 * cglib动态代理,不需要目标类实现接口
 * 运行时使用字节码技术动态增强
 */
public class CglibProxyFactory implements MethodInterceptor {

    Object target;

    public CglibProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance(){
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(this);
        enhancer.setSuperclass(target.getClass());
        return enhancer.create();

    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("课前准备");
        Object invoke = method.invoke(target, objects);
        System.out.println("下课");
        return invoke;
    }
}
java 复制代码
public class Client {
    public static void main(String[] args) {
        CglibProxyFactory cglibProxyFactory = new CglibProxyFactory(new TeacherDao());
        TeacherDao proxyInstance = (TeacherDao) cglibProxyFactory.getProxyInstance();
        proxyInstance.teach();
    }
}

但是目标类,或方法如果被privatefinal关键字修饰,以及目标方法被static修饰,则无法进行代理,因为上述修饰符的方法无法被继承。

3.4、小结

动态代理和静态代理的区别,主要体现在:

  • 静态代理是在编译期间由开发者显式编写代理类或通过工具生成,代理类在运行时已经存在:
  • 动态代理在运行时动态生成代理类,代理类的字节码不会提前编写,而是通过反射或字节码操作框架生成。(使用JDK或第三方字节码库提供的API)
    运行时,JDK 动态代理会生成类似下面的代理类:com.light.proxy.jdkproxy.$Proxy0,其不会存在于target目录下。
      在Spring AOP中,也会根据是否实现了接口,去选择不同的动态代理。

相关推荐
xmh-sxh-131413 分钟前
使用java缓存的场景介绍
java
全栈老实人_19 分钟前
时间管理系统|Java|SSM|JSP|
java·开发语言·tomcat·maven
正在努力中的小白♤23 分钟前
多个JAVA环境变量安装配置
java·开发语言·python
岁月变迁呀24 分钟前
Spring Cloud Gateway 源码
java·spring·spring cloud·gateway
一棵星1 小时前
EMQX构建简易的云服务
java
老马啸西风1 小时前
分布式链路追踪简介-01-dapper 论文思想介绍
java
Zhu_S W2 小时前
SpringBoot项目的创建方式(五种)
java·spring boot·后端·maven·idea
Jack Yan2 小时前
【编辑器扩展】打开持久化路径/缓存路径/DataPath/StreamingAssetsPath文件夹
java·开发语言
我不想写昵称2 小时前
1. JasperSoft介绍与安装
java·后端·报表
蜜獾云2 小时前
maven-resources-production:ratel-fast: java.lang.IndexOutOfBoundsException
java·maven·intellij-idea