代理模式

232 阅读7分钟

代理模式

代理模式和装饰模式的区别可参考zhuanlan.zhihu.com/p/97499017

代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来,使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。

访问互联网

现代社会中,网络已经渗透到人们工作和生活的方方面面,为了满足各种需求,不管是公司还是家庭,网络的组建工作都必不可少。根据网络环境的不同,适当地使用各种网络设备十分重要,例如我们常见的家用路由器,其最重要的一个功能就是代理上网业务,使其下面所有终端设备都能够连入互联网

如图所示的是一个简单的家庭网络的网络结构。从左往右看,首先我们得去网络服务提供商(ISP)申请互联网(Internet)宽带业务,然后通过光纤入户并拿到一个调制解调器(Modem),也就是我们俗称的“猫”(以下简称为猫),它负责在模拟信号(或者光信号)与数字信号之间做调制转换(类似于适配器)。接下来连接的就是我们的主角—路由器(Router)了,它负责代理互联网服务。最后,我们每天使用的一些终端设备,例如笔记本电脑、台式机、手机、电视机等,不管通过Wi-Fi还是网线,都能通过路由器代理成功上网。基于此结构,我们尝试以代码来实现,首先定义一个互联网访问接口Internet

package acting;

public interface Internet {
    void httpAccess(String url);
}

我们对互联网访问接口进行简化,假设它只有一个互联网访问标准(协议)httpAccess,并接受一个url地址。毫无疑问,直接与互联网连接的一定是“猫”,所以我们首先让“猫”实现互联网访问接口

调制解调器Modem

package acting;

public class Modem implements Internet{
    public Modem(String password) throws Exception{
        if (!"123445".equals(password)){
            throw new Exception("拨号失败,请重试");
        }
        System.out.println("拨号上网成功");
    }
    @Override
    public void httpAccess(String url) {//实现互联网访问接口
        System.out.println("正在访问:"+url);
    }
}

调制解调器(猫)实现了互联网访问接口,并在构造方法中进行拨号上网的密码校验,校验通过后用户即可通过调用互联网访问实现方法httpAccess()上网了。此方法来者不拒,接受用户的一切访问

互联网代理

虽然“猫”允许用户直接访问互联网,但用户每次上网都不得不进行拨号操作,这确实不太方便。此外,“猫”要对大量的终端上网设备进行资源分配与管理,难免力不从心。例如,孩子学习时总是偷偷上网看电影或玩游戏,只依靠家长是很难得到有效控制的。再如,我们上网时会遭遇一些高危网站的攻击,严重威胁到我们的网络安全,所以我们有必要采取一些技术手段来屏蔽终端设备对这些有害网站的访问,这些事还是得交给代理去负责,例如建立黑名单机制。

要在用户与互联网之间建立黑名单机制并禁止终端设备对有害网站的访问,我们就得把终端设备(客户端)与“猫”的连接隔离开,并在它们之间加上路由器进行代理管控。当终端设备请求访问互联网时,我们就将其传入的地址与黑名单进行比对,如果该地址存在于黑名单中则禁止访问,反之则通过校验并转交给“猫”以连接互联网。这个逻辑非常清晰,下面我们用路由器来实现

package acting;

import java.util.Arrays;
import java.util.List;

public class RouterProxy implements Internet {
    private Internet modem;//被代理对象
    private List<String> blackList = Arrays.asList("电影","游戏","音乐","小说");

    public RouterProxy() throws Exception {
        this.modem = new Modem("123445");//实例化被代理类
    }

    @Override
    public void httpAccess(String url) {//实现互联网访问接口方法
        for (String keyword:blackList){
            if (url.contains(keyword)){
                System.out.println("禁止访问:"+url);
                return;
            }
        }
        modem.httpAccess(url);//转发请求至猫以访问互联网
    }
    
}

路由器与“猫”一样实现了互联网接口,并于构造方法中主动实例化了“猫”,作为被代理的目标业务(互联网业务)类。重点在于互联网访问实现方法中,我们对提前设定好的黑名单进行遍历,如果访问地址中带有黑名单中的敏感字眼就禁止访问并直接退出,如果遍历结束则代表没有发现任何威胁,此时就可以假设访问地址是相对安全的。当访问地址成功通过安全校验后,代码第18行中路由器移交控制权,将请求转发给“猫”进行互联网访问。可以看到,其实路由器本质上并不具备上网功能,而只是充当代理角色,对访问进行监管、控制与转发。

路由器构造方法为实现对“猫”的全面管控,主动实例化了“猫”对象,而非由外部注入。从某种意义上讲,这是代理模式区别于装饰器模式的一种体现。虽然二者的理念与实现有点类似,但装饰器模式往往更加关注为其他对象增加功能,让客户端更加灵活地进行组件搭配;而代理模式更强调的则是一种对访问的管控,甚至是将被代理对象完全封装而隐藏起来,使其对客户端完全透明。读者大可不必被概念所束缚,属于哪种模式并不重要,最适合系统需求的设计就是最好的设计。

package acting;

public class client {
    public static void main(String[] args) throws Exception {
        Internet proxy = new RouterProxy();//实例化的是代理
        proxy.httpAccess("http://www.电影.com");
        proxy.httpAccess("http://www.游戏.com");
        proxy.httpAccess("http://www.工作.com");

    }
}

补充:动态代理参考

juejin.cn/post/684490…

go语言目前来说是不支持动态代理的

业务增强与管控

管是在编程时预定义静态代理,还是在运行时即时生成代理,它们的基本理念都是通过拦截被代理对象的原始业务并在其之前或之后加入一些额外的业务或者控制逻辑,来最终实现在不改变原始类(被代理类)的情况下对其进行加工、管控。换句话说,虽然动态代理更加灵活,但它也是在静态代理的基础之上发展而来的,究其本质,万变不离其宗,我们来看代理模式的类结构

Subject(业务接口):对业务接口标准的定义与表示,对应本章例程中的互联网访问接口Internet。

RealSubject(被代理业务):需要被代理的实际业务类,实现了业务接口,对应本章例程中的调制解调器Modem。

Proxy(代理):同样实现了业务接口标准,包含被代理对象的实例并对其进行管控,对外提供代理后的业务方法,对应本章例程中的路由器RouterProxy。

Client(客户端):业务的使用者,直接使用代理业务,而非实际业务。

代理模式不仅能增强原业务功能,更重要的是还能对其进行业务管控。对用户来讲,隐藏于代理中的实际业务被透明化了,而暴露出来的是代理业务,以此避免客户端直接进行业务访问所带来的安全隐患,从而保证系统业务的可控性、安全性。

Go版本代码

package acting

import (
    "fmt"
    "strings"
)

type Internet interface {
    HttpAccess(string)
}

type modem struct {
}

func newModem(password string) (*modem, error) {
    if password != "123445" {
        return nil, fmt.Errorf("拨号失败,请重试")
    }
    fmt.Println("拨号上网成功")
    return &modem{}, nil
}
func (m *modem) HttpAccess(url string) { //实现互联网访问接口
    fmt.Println("正在访问:" + url)
}

type RouterProxy struct {
    modem     Internet
    blackList []string
}

func NewRouterProxy() (*RouterProxy, error) {
    modem, err := newModem("123445")
    if err != nil {
        return nil, err
    }
    return &RouterProxy{
        modem:     modem,
        blackList: []string{"电影", "游戏", "音乐", "小说"},
    }, nil
}
func (r *RouterProxy) HttpAccess(url string) {
    for _, v := range r.blackList {
        if strings.Contains(url, v) {
            fmt.Println("禁止访问" + url)
            return
        }
    }
    r.modem.HttpAccess(url)
}
package main

import "desginPatterns/acting"

func main() {
    proxy, err := acting.NewRouterProxy()
    if err != nil {
        panic(err.Error())
    }
    proxy.HttpAccess("http://www.电影.com")
    proxy.HttpAccess("http://www.游戏.com")
    proxy.HttpAccess("http://www.工作.com")
}