编程

设计模式之代理(Proxy)模式

265 2024-01-29 01:12:00

意图

代理模式(Proxy)是一种结构型设计模式,允许你提供为另一个对象提供替代或占位符。代理控制对原始对象的访问,允许你在请求到达原始对象之前或之后执行某些操作。

问题描述

为什么要控制对对象的访问?这里有一个例子:你有一个巨大的对象,它消耗了大量的系统资源。你有时需要它,但并不总是如此。

你可以实现惰性初始化:只在实际需要时创建此对象。对象的所有客户端都需要执行一些延迟初始化代码。不幸的是,这可能会导致大量的代码重复。

在理想的情况下,我们希望将这些代码直接放入对象的类中,但这并不总是可能的。例如,该类可能是封闭的第三方库的一部分。

方案

代理模式建议你创建一个与原始服务对象具有相同接口的新代理类。然后更新应用程序,使其将代理对象传递给原始对象的所有客户端。在接收到来自客户端的请求后,代理创建一个真实的服务对象,并将所有工作委派给它。

但是有什么好处呢?如果你需要在类的主逻辑之前或之后执行某些内容,则代理允许你在不更改该类的情况下执行。由于代理实现了与原始类相同的接口,因此它可以传递给任何需要实际服务对象的客户端。

真实世界类比

信用卡是银行账户的代理,也是一捆现金的代理。两者都实现了相同的接口:它们可以用于支付。消费者感觉很好,因为没有必要随身携带大量现金。店主也很高兴,因为交易收入可以通过电子方式添加到商店的银行账户中,而不会有丢失存款或在去银行的路上被抢的风险。

结构

服务端接口(Service Interface)声明了服务的接口。代理必须遵循这个接口,以便将自己乔装成服务端对象。

服务端(Service)类是一个提供一些有用的业务逻辑的类。

代理(Proxy)类有一个引用字段指向了服务端对象。在代理完成它的处理(比如,惰性初始化,日志,权限控制,缓存等)后,它将请求传递给服务端对象。

通常,代理管理他的服务对象的整个声明周期。

客户端(Client)类应该通过同一个接口同服务端和代理类协作。这样你可用将代理传递给任何需要服务对象的代码。

伪代码

此示例说明了代理(Proxy)模式如何帮助将延迟初始化及缓存引入到第三方 YouTube 集成库。

 

该库提供了视频下载类。不过,它非常低效。如果客户端应用同一个视频请求多次,该库只会一次又一次地下载,而不是将第一次下载文件缓存重用。

代理类实现了原始下载器通用的接口并委派全部任务。不过,它会跟踪下载共的文件,并且在应用对同一个视频请求多次时缓存结果。

// 远程服务接口
interface ThirdPartyYouTubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)

// The concrete implementation of a service connector. Methods
// of this class can request information from YouTube. The speed
// of the request depends on a user's internet connection as
// well as YouTube's. The application will slow down if a lot of
// requests are fired at the same time, even if they all request
// the same information.
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib is
    method listVideos() is
        // Send an API request to YouTube.

    method getVideoInfo(id) is
        // Get metadata about some video.

    method downloadVideo(id) is
        // Download a video file from YouTube.

// To save some bandwidth, we can cache request results and keep
// them for some time. But it may be impossible to put such code
// directly into the service class. For example, it could have
// been provided as part of a third party library and/or defined
// as `final`. That's why we put the caching code into a new
// proxy class which implements the same interface as the
// service class. It delegates to the service object only when
// the real requests have to be sent.
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeLib
    private field listCache, videoCache
    field needReset

    constructor CachedYouTubeClass(service: ThirdPartyYouTubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)

// The GUI class, which used to work directly with a service
// object, stays unchanged as long as it works with the service
// object through an interface. We can safely pass a proxy
// object instead of a real service object since they both
// implement the same interface.
class YouTubeManager is
    protected field service: ThirdPartyYouTubeLib

    constructor YouTubeManager(service: ThirdPartyYouTubeLib) is
        this.service = service

    method renderVideoPage(id) is
        info = service.getVideoInfo(id)
        // Render the video page.

    method renderListPanel() is
        list = service.listVideos()
        // Render the list of video thumbnails.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// The application can configure proxies on the fly.
class Application is
    method init() is
        aYouTubeService = new ThirdPartyYouTubeClass()
        aYouTubeProxy = new CachedYouTubeClass(aYouTubeService)
        manager = new YouTubeManager(aYouTubeProxy)
        manager.reactOnUserInput()

适用

有很多种方法可以利用代理模式。让我们来看看最流行的用法。

延迟初始化(虚拟代理)。这是在你有一个重量级的服务端对象时,它总是出于活动状态,从而浪费系统资源,而你只是偶尔需要它。

可以将对象的初始化延迟到真正需要的时候,而不是在应用程序启动时创建对象。

权限控制 (保护代理)。当你只有特定客户端需要使用服务端对象时:比如,当对象是操作系统的关键部分,而客户端是各种启动的应用程序时。

只有当客户端的凭据符合某些条件时,代理才能将请求传递给服务对象。

远程服务(远程代理)的本地执行。当服务端对象位于远程服务器时。

在这种情况下,代理通过网络传递客户端请求,处理与网络合作的所有令人讨厌的细节。

日志请求(日志代理)。当你要保存服务端对象的请求历史记录时。 

代理可以在将每个请求传递给服务之前对其进行日志记录。

缓存请求结果 (缓存代理)。当你需要缓存客户端请求的结果并管理这个缓存的声明周期时,特别是当结果太大时。

代理可以让总是产生相同结果的重复请求实现缓存。代理可以使用请求参数作为缓存密钥。

智能引用。当没有客户端使用重量级对象时,你需要能够将其忽略。

代理可以跟踪获得引用服务对象或其结果的客户端。代理可以时不时地检查客户端并检查它们是否仍处于活动状态。如果客户端列表为空,则代理可能会忽略服务对象并释放底层系统资源。

代理也可以跟踪客户端是否修改了服务端对象。然后,未改变的对象可以被其他客户端重用。

如何实现

  1. 如果没有预先存在的服务接口,请创建一个接口以使代理和服务对象可互换。从服务类中提取接口并不总是可能的,因为你需要更改所有服务的客户端才能使用该接口。另一种方式是使代理成为服务类的子类,这样它将继承服务的接口。
  2. 创建代理类。该代理类应该又一个字段来存储服务端引用。通常,代理创建并管理服务的整个声明周期。在极少数情况下,客户端会通过构造函数将服务传递给代理。
  3. 根据意图实现代理方法。大部分情况下,做完这些工作后,代理应该委派任务给服务对象。
  4. 可以考虑引入一种创建方法来决定客户端是获得代理还是获得真正的服务。这可以是代理类中的一个简单静态方法,也可以是一个全面的工厂方法。
  5. 可以考虑实现服务对象的延迟初始化。

优缺点

  • ✔️可以以在客户端不了解服务端的情况下控制服务对象。
  • ✔️当客户端不关心服务对象时,你可以管理它的生命周期。
  • ✔️即使服务端对象未就绪或不可用,代理也能工作。
  • ✔️开闭原则。你可以在不修改服务端或者客户端的情况下引入新的代理。
  • ❌由于需要引入许多新类,代码可能变得更加复杂。
  • ❌服务端获得的响应可能会有延迟。

与其他模式的关系

使用适配器(Adapter),你可以通过不同接口获取现有对象。使用代理(Proxy),接口不需改变。使用装饰器(Decorator),你可以通过增强的接口访问对象。

装饰器(Decorator)代理(Proxy)结构相似,不过意图不同。这两个模式都是基于组合原则构建的,其中一个对象应该将部分工作委托给另一个对象。不同之处在于,代理(proxy)通常自行管理其服务对象的生命周期,而装饰器(Decorator)的组合始终由客户端控制。

代码示例

index.php: 概念示例

<?php

namespace RefactoringGuru\Proxy\Conceptual;

/**
 * The Subject interface declares common operations for both RealSubject and the
 * Proxy. As long as the client works with RealSubject using this interface,
 * you'll be able to pass it a proxy instead of a real subject.
 */
interface Subject
{
    public function request(): void;
}

/**
 * The RealSubject contains some core business logic. Usually, RealSubjects are
 * capable of doing some useful work which may also be very slow or sensitive -
 * e.g. correcting input data. A Proxy can solve these issues without any
 * changes to the RealSubject's code.
 */
class RealSubject implements Subject
{
    public function request(): void
    {
        echo "RealSubject: Handling request.\n";
    }
}

/**
 * The Proxy has an interface identical to the RealSubject.
 */
class Proxy implements Subject
{
    /**
     * @var RealSubject
     */
    private $realSubject;

    /**
     * The Proxy maintains a reference to an object of the RealSubject class. It
     * can be either lazy-loaded or passed to the Proxy by the client.
     */
    public function __construct(RealSubject $realSubject)
    {
        $this->realSubject = $realSubject;
    }

    /**
     * The most common applications of the Proxy pattern are lazy loading,
     * caching, controlling the access, logging, etc. A Proxy can perform one of
     * these things and then, depending on the result, pass the execution to the
     * same method in a linked RealSubject object.
     */
    public function request(): void
    {
        if ($this->checkAccess()) {
            $this->realSubject->request();
            $this->logAccess();
        }
    }

    private function checkAccess(): bool
    {
        // Some real checks should go here.
        echo "Proxy: Checking access prior to firing a real request.\n";

        return true;
    }

    private function logAccess(): void
    {
        echo "Proxy: Logging the time of request.\n";
    }
}

/**
 * The client code is supposed to work with all objects (both subjects and
 * proxies) via the Subject interface in order to support both real subjects and
 * proxies. In real life, however, clients mostly work with their real subjects
 * directly. In this case, to implement the pattern more easily, you can extend
 * your proxy from the real subject's class.
 */
function clientCode(Subject $subject)
{
    // ...

    $subject->request();

    // ...
}

echo "Client: Executing the client code with a real subject:\n";
$realSubject = new RealSubject();
clientCode($realSubject);

echo "\n";

echo "Client: Executing the same client code with a proxy:\n";
$proxy = new Proxy($realSubject);
clientCode($proxy);

Output.txt: 执行结果

Client: Executing the client code with a real subject:
RealSubject: Handling request.

Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.