GOF23--23种设计模式(三)

一.桥接模式

Java中的桥接模式(Bridge Pattern)是一种结构性设计模式,它将抽象部分和实现部分分离,使它们可以独立变化,同时通过桥接对象将它们连接起来。

这种模式将抽象与其实现解耦,使得抽象和实现可以独立变化。抽象和它的实现通过一个桥接类进行连接,使得它们可以各自独立地变化。

桥接模式在应用上,通过一个应用实例引入吧

我们在开发一些应用程序或者web网站的时候,不仅仅是把它开发出来这么简单,还需要做一些兼容的东西,比如我们开发环境一般是在widows或者macOS上开发的,那么就有一个兼容性的问题,你开发的项目到底是在哪里运行的,比如一款APP它不仅仅在pc端可以运行,而且还需要再Android上运行,客户的再哪里有需求,我们就需要再哪里做兼容

但是,我们做兼容的时候,这些系统都要做兼容,是不是有一些麻烦呢?我们开发者开发APP和web,系统有windows,mac和Android,如果使用适配器模式的思想,就是一个一个的来做适配

那么小学就学过排列组合,那么需要适配的类有APP-Windows,APP-mac,APP-Android,web-Windows,web-mac,web-Android,一共需要六个适配类

这样适配很冗余,开发难度也加大了,而且不支持横向拓展,我们思考一下,难道世界上就只有这三种系统嘛?很显然不是的,就比如Linux,ChromeOS,ios,HarmonyOS等等

如果这些系统都来做适配的话,那么排列组合就是14种以上,需要手动的实现14种以上的实现类,很显然是不现实的

如下图:

所以我们可以把这种适配的方式抽象出来,对于这种有着某种关系的适配方式,我们可以把它抽象成两个二维坐标,这样操作以后将这个二维坐标的建立一个连接,使得两坐标之间的交点就是实现类,而两个坐标点就是参数,被组合进去的结果

什么意思呢?直接上图示:

 桥接模式下的适配,只需要横向新增,当需要适配的时候,就将两个坐标点来进行连接,这种方式叫桥接

所以实现的关键在于那种连接,就是那座桥

在桥接模式中使用的是组合的方式将一个坐标轴组合进来,在使用适配的时候,会需要用户丢进来两个参数,一个是使用的系统,另一个是需要适配的应用

有了这两个桥接在一起,就可以完成所有的类适配

桥接模式最大的优点是可以横向拓展,现在我新增一个系统,直接在横轴上新增就好了,不需要对纵轴操作,它们之间有桥来联系,相同的新增一个服务,如文件服务,可以直接在纵轴上新增,不需要更改横轴

根本原因是它们已经属于两个域了,他们互不干预,通过桥来连接这两个域

如下图示:

 桥接模式模拟测试:

创建一个Server接口:

//此接口需要APP或Web项目实现
public interface ServerDemo {
    //打开方式
    void open();
}

 

APP实现类:实现上面的接口

public class AppInstance implements ServerDemo{
    //APP,表明是APP的实现类
    @Override
    public void open() {
        System.out.println("打开应用App");
    }
}

 

web实现类

public class WebInstance implements ServerDemo{
    //web,表明是web的实现类
    @Override
    public void open() {
        System.out.println("打开web网页");
    }
}

 

抽象的系统类,将server接口组合进来

使用组合的方式来建立桥接

//抽象的system类
public abstract class SystemDem {
    //将server接口组合进来,使得server和system有一种桥接关系
    protected ServerDemo sd;

    public SystemDem(ServerDemo sd) {
        this.sd = sd;
    }
    public void open(){
        sd.open();
    }
}

 

各个系统继承这个抽象类,并重写这其中的方法:

class WindowsOs extends SystemDem{
    //windows系统
    public WindowsOs(ServerDemo sd) {
        super(sd);
    }

    @Override
    public void open() {
        System.out.println("WindowsOS =>");
        super.open();

    }
}

class MacOs extends SystemDem {
    //macOS
    public MacOs(ServerDemo sd) {
        super(sd);
    }

    @Override
    public void open() {
        System.out.println("MacOS =>");
        super.open();

    }
}

 

写主方法:

    public static void main(String[] args) {
        //在windows系统下打开APP
        WindowsOs ws = new WindowsOs(new AppInstance());
        ws.open();
        //在Mac系统下打开Web网页
        MacOs ms = new MacOs(new WebInstance());
        ms.open();
    }

 

查看上面例子:

如果我们要在windos上适配APP,只需要在Windows系统中将App实例丢进去就行了

这个例子实现桥接模式就在system的抽象类中将需要适配的Server接口组合进来了,使得它们之间产生了联系,仿佛一座桥一样把他们连接在了一起

现在大概了解到了什么是将抽象部分和实现部分分离,代码实现的是坐标轴,使用的时候是坐标轴的交点(也是抽象出来的部分)

注意点:

桥接模式和适配器模式是兼容的,在这些设计模式之间,都是互相兼容的

 二.静态代理模式

静态代理模式是一种设计模式,它通过创建一个代理类来处理被代理类的所有方法调用,从而实现了一些额外的功能或逻辑。

在静态代理模式中,代理类和被代理类通常具有相同的方法和属性,这样就可以在被代理类的所有方法调用时,先经过代理类的处理。这种模式通常用于在不修改原有代码的情况下,增加一些额外的功能或逻辑,例如日志记录、事务处理等。

实现静态代理模式需要以下步骤:

  1. 定义被代理类:被代理类通常是需要被代理的对象,它包含一些方法和属性。
  2. 定义代理类:代理类需要与被代理类具有相同的方法和属性,以便在调用被代理类的方法时能够进行相应的处理。
  3. 在代理类中实现被代理类的方法:在代理类中实现被代理类的方法时,可以调用被代理类的方法,同时可以加入一些额外的逻辑。
  4. 使用代理类:在使用被代理类的对象时,实际上使用的是代理类的对象。因此,可以在使用过程中调用代理类的方法,以实现额外的功能或逻辑。

静态代理模式的主要优点是可以实现对多个对象的代理,同时不需要修改原有的代码。此外,静态代理模式还可以在调用被代理类的方法时进行性能优化,例如缓存结果等。

浅浅的举一个例子来表示静态代理模式;

现在我是一个北漂的打工人,刚到北京,需要租一套房子,但是北京很大,我往往都是看得到房子看不到房东主人,为什么?

因为科技发达的今天,房东早就不自己出租房子的,而是交接房屋中介,那么找中介很简单,我直接再信息栏看传单就行了,无形中我们就完成了一个建议代理模式的构造

谁要租房子?我

谁可以帮我租到房子?房屋中介

谁真的有房子?房东主人

那么这三者种,谁是代理?肯定是房屋中介

我们思考一下为什么要找中介租房?中介有房东托付给他的房源,中介更加专业,可以带我看房,并且根据我的需求选房

换到正题上来,这就是为什么需要使用代理模式,中介有房东托付给他的房源:用设计模式来说就是代理类拥有被代理的属性和方法;而可以带我看房,并且根据我的需求选房:表示代理类会比真实类多更多的方法,和拓展业务,但是又不改变原来的业务代码,使得横向拓展业务

 静态代理模式模拟

代理类需要的接口:

//中介的接口
//代理类需要实现的方法
public interface ProxyDemo {
    //出租房子
    void send();
    //带租客看房子
    void sayHouse();
    //根据租客的需求分析房子
    void analyseDemand();
}

 

代理类,实现上面的接口,增加拓展业务:

public class ProxyRole implements ProxyDemo{
    //将被代理类组合进来
    private Landlord ld;
    //需要那个房东的房子直接注入进来
    public void setLd(Landlord ld) {
        this.ld = ld;
    }

    @Override
    public void send() {
        ld.send();
        System.out.println("中介出租");
    }

    @Override
    public void sayHouse() {
        System.out.println("中介带你看房");
    }

    @Override
    public void analyseDemand() {
        System.out.println("中介根据需求分析,然后提出适合你的房子");
    }
    public void achieveSend(){
        analyseDemand();
        sayHouse();
        send();
    }
}

 

被代理的类,真实角色,把自己的业务组合给代理角色,保证自己的纯粹的业务:

//房东本人
public class Landlord {
    //很纯粹,出租一个房子
    public  void send(){
        System.out.println("出租房子(房东)");
    }
}

 

租房人,向中介租房(主方法):

public static void main(String[] args) {
    //我需要住房ld的房子
    Landlord ld = new Landlord();
    //将这个房东的信息交给中介,它带我去看房
    ProxyRole pr = new ProxyRole();
    pr.setLd(ld);
    //开始租房
    pr.achieveSend();
}

 

输出结果:

 静态代理再理解

 在我们的业务开发中,假设我们已经写好了一个对用户进行增删改查的类

它可以实现对用户进行操作,但是现在公司的需求新增了,要做日志功能,每个用户进行了什么操作都要在日志中展示,用于维护数据安全

我们可以把原始的类进行改造,在其中加上日志的信息,但是一个写好的类,你去更改它,如果改坏了怎么办,再说了在公司中去更改已经跑过的原代码本来就是大忌

这个时候就可以使用代理模式,在不改变原始代码的类的情况下,横向拓展出来一个日志功能

当然我们可以使用这种方式横向拓展出来很多的方法,日志功能只是一个简单的测试,但目的都是为了保证原始代码更加的纯粹

图解:

 代码模拟

代理类需要实现的接口,也是代理需要拓展的功能:

//代理类需要实现的接口
public interface userImpInterface {
    //新增日志方法
    void printLog(String msg);
    //新增事务方法
    void useTransaction(String msg);
    //原始业务
    void addUser();
    void delUser();
    void updateUser();
    void queryUser();
}

 

代理类,需要实现上述接口,并拓展方法:

//代理类
public class userImp implements userImpInterface{
    //将被代理类组合进来
    private userService us;
    //注入被代理角色
    public void setUs(userService us) {
        this.us = us;
    }
    //打印日志
    @Override
    public void printLog(String msg) {
        System.out.println("使用了"+msg+"方法");
    }
    //使用事务
    @Override
    public void useTransaction(String msg) {
        System.out.println(msg+"方法使用了事务");
    }

    @Override
    public void addUser() {
        printLog("addUser()");
        us.addUser();
        useTransaction("addUser()");
    }

    @Override
    public void delUser() {
        printLog("addUser()");
        us.delUser();
    }

    @Override
    public void updateUser() {
        printLog("addUser()");
        us.updateUser();
    }

    @Override
    public void queryUser() {
        printLog("addUser()");
        us.queryUser();
    }
}

 

真实角色,被代理的类,拥有一些增删改查的初始方法,不改动,保持纯粹:

//真实类,提供对用户的操作
public class userService {
    public void  addUser(){
        System.out.println("新增用户的方法");
    }
    public void delUser(){
        System.out.println("删除用户的方法");
    }
    public void updateUser(){
        System.out.println("更改用户的方法");
    }
    public void queryUser(){
        System.out.println("查询用户的方法");
    }
}

 

主方法:

public static void main(String[] args) {
   //原始业务
    userService us = new userService();
    //组合到代理类中
    userImp ui = new userImp();
    ui.setUs(us);
    //代理类实现了日志方法的拓展,可以直接调用代理的方法
    ui.addUser();
}

 

结果展示:

静态代理模式的缺点:

 很明显,每个真实角色都会有一个代理类进行拓展,这使得代码量会翻倍,即使一个代理类可以代理多个真实角色,也会有代码翻倍的情况

由于需要创建额外的代理类,因此会增加代码的复杂性和维护成本。

三.动态代理 

上面学习了静态代理以后,我们知道了静态代理的缺点就是代码翻倍,每个真实角色都会使代理类的代码翻倍

究其原因呢?就是静态代理无法实现对真实角色进行统一管理,如果我们能让真实角色变成可自己加载到代理类,而调用那个代理类就加载那个真实角色就好了

这就是动态代理要做的,动态代理,动在可以自己加载真实角色,从而使代理类成为一个模板,需要使用那个真实角色,调用代理类模板加载就行了

百度百科:动态代理是一种设计模式,它允许你在运行时动态地创建代理对象,对原始对象进行封装,从而实现对原始对象的操作进行拦截、增强或修改。

动态代理的主要目的是提供一种更加灵活的方式来扩展对象的行为,而不是直接修改原始代码。

在Java中,动态代理主要通过java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler接口来实现

实现动态代理的方式有很多,官方推荐使用基于接口的JDK动态代理,也就是需要使用到Proxy方法和实现InvocationHandler接口

举个例子,是静态代理租房子的例子

代理类需要实现的接口:

//中介的接口
//代理类需要实现的方法
public interface ProxyDemo {
    //出租房子
    void send();
}

 

 

真实角色,被代理的类:

//房东本人
public class Landlord implements ProxyDemo {
    //很纯粹,出租一个房子
    public  void send(){
        System.out.println("出租房子(房东)");
    }
}

 

 

第一步:实现InvocationHandler接口

实现这个接口就需要重写里面的invoke方法,这个方法是调用代理类的时候,自动就会执行的

invoke方法里可以调用其它的方法将其添加在真实角色方法的前面或者后面

//动态生成的代理类
public class MyInvocationHandler implements InvocationHandler {
    //要被代理的接口
    private ProxyDemo pr;

    public void setPr(ProxyDemo pr) {
        this.pr = pr;
    }
    //处理代理实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理的拓展方法
        beforeMethod();
        //这里执行的真实角色的方法,具体执行什么需要让调用者调度
        Object invoke = method.invoke(pr, args);
        //代理类的拓展方法
        afterMethod();
        return invoke;
    }
    //在执行真实角色前的方法
    public void beforeMethod(){
        System.out.println("执行方法前");
    }
    //在执行真实角色后的方法
    public void afterMethod(){
        System.out.println("执行方法后");
    }
}

 

 

第二步:通过Proxy类生成动态代理类

  //生成得到代理类
    public  Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), pr.getClass().getInterfaces(),this);
    }

 

它也需要写到实现InvocationHandler接口的类下

Proxy.newProxyInstance()方法创建动态代理的实例

生成动态代理类需要的三个参数

  • 第一个参数:用于加载代理类的 ClassLoader,当前类就是代理类,所以使用它的类加载器
  • 第二个参数:需要代理的接口,面向接口代理,传入接口类
  • 第三个参数:实现了InvocationHandler的类对象,一般就是当前类,所以传入this

第三步:测试

public static void main(String[] args) {
    //真实角色
    Landlord ld = new Landlord();
    //构造生成代理类对象
    MyInvocationHandler handler = new MyInvocationHandler();
    handler.setPr(ld);
    //生成代理类
    ProxyDemo proxy = (ProxyDemo)handler.getProxy();
    //调用此方法时就会执行动态代理的invoke方法
    //会将send()方法传给invoke()方法
    proxy.send();
}

 

总结:

灵活性:动态代理可以在运行时动态地创建代理对象,因此可以灵活地根据需要改变目标对象的行为。这使得动态代理非常适合用于实现一些需要在运行时动态改变行为的场景

扩展性:由于动态代理使用的是接口,因此它可以轻松地扩展到任何实现了接口的目标对象。这使得动态代理非常适合用于实现一些需要适配不同接口的场景

性能优化:虽然动态代理会增加一些额外的开销,但是通过合理的优化和缓存机制,可以有效地提高目标对象的性能

最大的优点就是解决了静态代理一次只能代理一个真实角色类的情况,动态代理一次代理的就是一个接口,所以它能一次代理一类业务,通过丢入不同的真实角色,生成不同的代理类,使得代码在运行中有非常高的灵活性

并且动态代理上面的步骤都是模板,记住模板可以自己手写动态代理不是难事

 

posted @ 2023-12-27 22:52  回忆也交给时间  阅读(4)  评论(0编辑  收藏  举报