学习设计模式第十五 概述 意图 UML 参与者 适用性 DoFactory GoF代码 Proxy模式解说 来自《深入浅出设计模式》的例子 .NET中的代理模式 效果及实现要点 总结

本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory

在软件系统中,有些对象有时候由于跨越网络或者其他的障碍,而不能够或者不想直接访问另一个对象,如果直接访问会给系统带来不必要的复杂性,这时候可以在客户程序和目标对象之间增加一层中间层,让代理对象来代替目标对象打点一切。这就是本文要说的Proxy模式。

意图

为其他对象提供一种代理以控制对这个对象的访问。

UML

学习设计模式第十五
概述
意图
UML
参与者
适用性
DoFactory GoF代码
Proxy模式解说
来自《深入浅出设计模式》的例子
.NET中的代理模式
效果及实现要点
总结

图1 Proxy模式UML图

参与者

这个模式涉及的类或对象:

  • Proxy

    • 维护一个引用使代理可以访问真实的目标。代理可能引用一个Subject,如果RealSubject和Subject的接口是相同的。

    • 提供一个与Subject完全一致的接口,从而让proxy可以完全代替真实目标。

    • 控制对真实目标的访问,同时可能负责真实目标的创建或销毁。

    • 依赖下面这些类型代理的其它责任:

    • 远程代理负责编码一个请求及其参数并且可以将编码后的请求发送到一个位于不同地址空间的真实对象。

    • 虚拟代理可以缓存有关真实目标的附加信息,以便可以在稍后访问它。

    • 保护代理检查调用者是否有执行请求所需的权限。

  • Subject

    • 定义RealSubject与Proxy的共同接口,从而可以在任何需要使用RealSubject的地方使用Proxy代替。

  • RealSubject

    • 定义Proxy所代表的真实对象。

适用性

在面向对象的语言中,对象完成通过公共接口声明的工作。这些对象的客户期待这些工作被快速高效的完成。然而某些情况下,一个对象被严格限制并且无法完成它的功能。典型的,这个情况在依赖一个远程资源(如访问另一台电脑)或需要长时间加载一个对象时会发生。类似这些情况下,你可以应用Proxy模式,创建一个代理对象来"表示"原始对象。代理将请求传递给目标对象。代理对象的接口与原始对象的接口完全一致,客户端甚至可能不知道它在与代理交互而不是真实对象。

代理模式的重要在于为另一个对象提供一个替代物或占位符以控制对它的访问。有4种不同类型的代理:

  • 远程代理负责编码一个请求并将编码的请求转发到一个不同地址空间(应用程序域,进程或机器)的真实对象。如Visual Studio中添加Web服务引用时生成的代理就属于这种类型。

  • 虚拟代理会缓存关于真实对象的附加信息,以至于可以延迟访问这些真实对象(这个过程有很多众所周知的名字按需加载或延迟加载)。如我们打开一个很大的HTML页面时,里面可能有很多文字和图片,为了使用户可以快速打开并查看页面,文字首先被加载,而图片通过存储了真实图片路径和尺寸的虚拟代理来代替,并在后台逐一加载。

  • 保护代理检查调用者是否有执行请求所需的权限。

  • 智能指引(Smart Reference)取代了简单的指针(一般常见于C++等使用指针的语言),它在访问对象时执行一些附加操作。 它的典型用途包括:对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为SmartPointers)。当第一次引用一个持久对象时,将它装入内存。在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用 Proxy 模式。

DoFactory GoF代码

结构化的示例演示了提供一个代表对象(代理)来控制到另一个接口相同的对象的访问。

// Proxy pattern 
// Structural example 
using System;
 
namespace DoFactory.GangOfFour.Proxy.Structural
{
    // MainApp test application
    class MainApp
    {
        static void Main()
        {
            // Create proxy and request a service
            Proxy proxy = new Proxy();
            proxy.Request();
 
            // Wait for user
            Console.ReadKey();
        }
    }
 
    // "Subject"
    abstract class Subject
    {
        public abstract void Request();
    }
 
    // "RealSubject"
    class RealSubject : Subject
    {
        public override void Request()
        {
            Console.WriteLine("Called RealSubject.Request()");
        }
    }
 
    // "Proxy"
    class Proxy : Subject
    {
        private RealSubject _realSubject;
 
        public override void Request()
        {
            // Use 'lazy initialization'
            if (_realSubject == null)
            {
                _realSubject = new RealSubject();
            }
            _realSubject.Request();
        }
    }
}

这个例子演示了通过MathProxy对象来带表Math对象这样一个代理模式的例子。

例子中涉及到的类与代理模式中标准的类对应关系如下:

  • Proxy – MathProxy

  • Subject – Imath

  • RealSubject - Math

// Proxy pattern 
// Real World example 
using System;
 
namespace DoFactory.GangOfFour.Proxy.RealWorld
{
    // Mainapp test application 
    class MainApp
    {
        static void Main()
        {
            // Create math proxy
            MathProxy proxy = new MathProxy();
 
            // Do the math
            Console.WriteLine("4 + 2 = " + proxy.Add(4, 2));
            Console.WriteLine("4 - 2 = " + proxy.Sub(4, 2));
            Console.WriteLine("4 * 2 = " + proxy.Mul(4, 2));
            Console.WriteLine("4 / 2 = " + proxy.Div(4, 2));
 
            // Wait for user
            Console.ReadKey();
        }
    }
 
    // "Subject"
    public interface IMath
    {
        double Add(double x, double y);
        double Sub(double x, double y);
        double Mul(double x, double y);
        double Div(double x, double y);
    }
 
    // "RealSubject"
    class Math : IMath
    {
        public double Add(double x, double y) { return x + y; }
        public double Sub(double x, double y) { return x - y; }
        public double Mul(double x, double y) { return x * y; }
        public double Div(double x, double y) { return x / y; }
    }
 
    // "Proxy Object"
    class MathProxy : IMath
    {
        private Math _math = new Math();
 
        public double Add(double x, double y)
        {
            return _math.Add(x, y);
        }
        public double Sub(double x, double y)
        {
            return _math.Sub(x, y);
        }
        public double Mul(double x, double y)
        {
            return _math.Mul(x, y);
        }
        public double Div(double x, double y)
        {
            return _math.Div(x, y);
        }
    }
}

.NET优化的代码实现了与上一个例子相同的功能,但采用了更多.NET内置的现代化特性。代码展示了远程代理模式,其中有一个代表对象(也就是说替身对象)来控制到一个位于不同AppDomain的另一个对象的访问。事实上,MathProxy对象中的'math'成员变量是'隐含'.NET代理来表示MathDomain中的Math对象。

// Proxy Design Pattern
// .NET optimized example
using System;
using System.Runtime.Remoting;
 
namespace DoFactory.GangOfFour.Proxy.NETOptimized
{
    class MainApp
    {
        static void Main()
        {
            // Create math proxy
            var proxy = new MathProxy();
 
            // Do the math
            Console.WriteLine("4 + 2 = " + proxy.Add(4, 2));
            Console.WriteLine("4 - 2 = " + proxy.Sub(4, 2));
            Console.WriteLine("4 * 2 = " + proxy.Mul(4, 2));
            Console.WriteLine("4 / 2 = " + proxy.Div(4, 2));
 
            // Wait for user
            Console.ReadKey();
        }
    }
 
    // "Subject"
    public interface IMath
    {
        double Add(double x, double y);
        double Sub(double x, double y);
        double Mul(double x, double y);
        double Div(double x, double y);
    }
 
    // "RealSubject"
    class Math : MarshalByRefObject, IMath
    {
        public double Add(double x, double y) { return x + y; }
        public double Sub(double x, double y) { return x - y; }
        public double Mul(double x, double y) { return x * y; }
        public double Div(double x, double y) { return x / y; }
    }
 
    // "The remote 'Proxy Object'"
    class MathProxy : IMath
    {
        private Math _math;
 
        // Constructor
        public MathProxy()
        {
            // Create Math instance in a different AppDomain
            var ad =  AppDomain.CreateDomain("MathDomain", null, null);
 
            var o = ad.CreateInstance(
                "DoFactory.GangOfFour.Proxy.NETOptimized",
                "DoFactory.GangOfFour.Proxy.NETOptimized.Math");
            _math = (Math)o.Unwrap();
        }
 
        public double Add(double x, double y)
        {
            return _math.Add(x, y);
        }
 
        public double Sub(double x, double y)
        {
            return _math.Sub(x, y);
        }
 
        public double Mul(double x, double y)
        {
            return _math.Mul(x, y);
        }
 
        public double Div(double x, double y)
        {
            return _math.Div(x, y);
        }
    }
}

Proxy模式解说

在软件系统中,我们无时不在跨越障碍,当我们访问网络上一台计算机的资源时,我们正在跨越网络障碍,当我们去访问服务器上数据库时,我们又在跨越数据库访问障碍,同时还有网络障碍。跨越这些障碍有时候是非常复杂的,如果我们更多的去关注处理这些障碍问题,可能就会忽视了本来应该关注的业务逻辑问题,Proxy模式有助于我们去解决这些问题。我们以一个简单的数学计算程序为例,这个程序只负责进行简单的加减乘除运算:

public class Math
{
    public double Add(double x, double y)
    {
        return x + y;
    }
    public double Sub(double x, double y)
    {
        return x - y;
    }
    public double Mul(double x, double y)
    {
        return x * y;
    }
    public double Dev(double x, double y)
    {
        return x / y;
    }
}

如果说这个计算程序部署在我们本地计算机上,使用就非常之简单了,我们也就不用去考虑Proxy模式了。但现在问题是这个Math类并没有部署在我们本地,而是部署在一台服务器上,也就是说Math类根本和我们的客户程序不在同一个地址空间之内,我们现在要面对的是跨越Internet这样一个网络障碍:

学习设计模式第十五
概述
意图
UML
参与者
适用性
DoFactory GoF代码
Proxy模式解说
来自《深入浅出设计模式》的例子
.NET中的代理模式
效果及实现要点
总结

图2.一个分布式计算机的需求

这时候调用Math类的方法就没有下面那么简单了,因为我们更多的还要去考虑网络的问题,对接收到的结果解包等一系列操作。

public class App
{
    public static void Main()
    {
        Math math = new Math();
        // 对接收到的结果数据进行解包
        double addresult = math.Add(2, 3);
        double subresult = math.Sub(2, 3);
        double mulresult = math.Mul(2, 3);
        double devresult = math.Dev(2, 3);
    }
}

为了解决由于网络等障碍引起复杂性,就引出了Proxy模式,我们使用一个本地的代理来替Math类打点一切,即为我们的系统引入了一层间接层,示意图如下:

学习设计模式第十五
概述
意图
UML
参与者
适用性
DoFactory GoF代码
Proxy模式解说
来自《深入浅出设计模式》的例子
.NET中的代理模式
效果及实现要点
总结

图3.引入Proxy的分布式计算器

我们在MathProxy中对实现Math数据类的访问,让MathProxy来代替网络上的Math类,这样我们看到MathProxy就好像是本地Math类,它与客户程序处在了同一地址空间内:

public class MathProxy
{
    private Math math = new Math();
 
    // 以下的方法中,可能不仅仅是简单的调用Math类的方法
    public double Add(double x, double y)
    {
        return math.Add(x, y);
    }
    public double Sub(double x, double y)
    {
        return math.Sub(x, y);
    }
    public double Mul(double x, double y)
    {
        return math.Mul(x, y);
    }
    public double Dev(double x, double y)
    {
        return math.Dev(x, y);
    }
}

现在可以说我们已经实现了对Math类的代理,存在的一个问题是我们在MathProxy类中调用了原实现类Math的方法,但是Math并不一定实现了所有的方法,为了强迫Math类实现所有的方法,另一方面,为了我们更加透明的去操作对象,我们在Math类和MathProxy类的基础上加上一层抽象,即它们都实现与IMath接口,示意图如下:

学习设计模式第十五
概述
意图
UML
参与者
适用性
DoFactory GoF代码
Proxy模式解说
来自《深入浅出设计模式》的例子
.NET中的代理模式
效果及实现要点
总结

图4.在代理与服务端上实现一个契约层

示意性代码如下:

public interface IMath
{
    double Add(double x, double y);
    double Sub(double x, double y);
    double Mul(double x, double y);
    double Dev(double x, double y);
}

Math类和MathProxy类分别实现IMath接口:

public class MathProxy : IMath
{
    //  
}
 
public class Math : IMath
{
    //  
}

此时我们在客户程序中就可以像使用Math类一样来使用MathProxy类了:

public class App
{
    public static void Main()
    {
        MathProxy proxy = new MathProxy();
 
        double addresult = proxy.Add(2, 3);
        double subresult = proxy.Sub(2, 3);
        double mulresult = proxy.Mul(2, 3);
        double devresult = proxy.Dev(2, 3);
    }
}

到这儿整个使用Proxy模式的过程就完成了,回顾前面我们的解决方案,无非是在客户程序和Math类之间加了一个间接层,这也是我们比较常见的解决问题的手段之一。另外,对于程序中的接口IMath,并不是必须的,大多数情况下,我们为了保持对对象操作的透明性,并强制实现类实现代理类所要调用的所有的方法,我们会让它们实现与同一个接口。但是我们说代理类它其实只是在一定程度上代表了原来的实现类,所以它们有时候也可以不实现于同一个接口。

来自《深入浅出设计模式》的例子

前面的介绍中提到一种虚拟代理,用于在加载较大资源时起一个占位作用并在后台异步加载的代理。《深入浅出设计模式》中给出了一段虚拟代理的示例:

虚拟代理的实现:

private class ImageProxy
{
    private static Image _image = null;
    private int _width = 133;
    private int _height = 154;
    private bool _retrieving = false;
 
    public int Width
    {
        get{ return _image == null ? _width : _image.Width; }
    }
    public int Height
    {
        get{ return _image == null ? _height : _image.Height; }
    }
 
    public Image Image
    {
        get
        {
            if (_image != null)
                return _image;
            else
            {
                if (!_retrieving)
                {
                    _retrieving = true;
                    Thread retrievalThread = new Thread(new ThreadStart(RetrieveImage));
                    retrievalThread.Start();
                }
                return PlaceHolderImage();
            }
        }
    }
 
    public Image PlaceHolderImage()
    {
        return new Bitmap(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("DoFactory.HeadFirst.Proxy.VirtualProxy.PlaceHolder.jpg"));
    }
 
    public void RetrieveImage()
    {
        // Book image from amazon
        string url = "http://images.amazon.com/images/P/0596007124.01._PE34_SCMZZZZZZZ_.jpg";
 
        HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        _image = Image.FromStream(response.GetResponseStream());
    }
}

使用这个虚拟代理很简单,下面的代码在按钮被点击时通过虚拟代理加载一张图片到PictureBox。另外这个简单的示例没有采用异步通知机制,图片异步加载完成后不会自动出现在PictureBox中,也就是说我们得点击两次才能加载到想要的图片(第一次点击总是虚拟代理中的图片占位符)。

private void buttonTestImageProxy_Click(object sender, System.EventArgs e)
{
    this.pictureBox.Image = new ImageProxy().Image;
}

虚拟代理的代码中,也使用了单例模式。另外我们可以将异步加载的图片缓存,用于后续请求。这就需要实现代理模式另一种变体-缓存代理。

.NET中的代理模式

在.NET中代理模式本身出现在Remoting架构中。在.NET Remoting中,无论何时一个对象请求另一个地址空间(应用程序域,进程,或机器)中的对象时,将创建一个代理发送请求及所有需要的数据到远程对象。如所有代理一样,客户端通常不知道有代理在工作。

WCF服务的客户端也重度依赖自动生成的代理对象。

效果及实现要点

Proxy模式根据种类不同,效果也不尽相同:

  1. 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个局域代表对象。这个不同的地址空间可以是在本机器中,也可是在另一台机器中。远程代理又叫做大使(Ambassador)。好处是系统可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的对象是局域的而不是远程的,而代理对象承担了大部份的网络通讯工作。由于客户可能没有意识到会启动一个耗费时间的远程调用,因此客户没有必要的思想准备。

  2. 虚拟(Virtual)代理:根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。使用虚拟代理模式的好处就是代理对象可以在必要的时候才将被代理的对象加载;代理可以对加载的过程加以必要的优化。当一个模块的加载十分耗费资源的情况下,虚拟代理的好处就非常明显。

  3. Copy-on-Write代理:虚拟代理的一种。把复制(克隆)拖延到只有在客户端需要时,才真正采取行动。

  4. 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。保护代理的好处是它可以在运行时间对用户的有关权限进行检查,然后在核实后决定将调用传递给被代理的对象。

  5. Cache代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

  6. 防火墙(Firewall)代理:保护目标,不让恶意用户接近。

  7. 同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突。

  8. 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等。

总结

在软件系统中,增加一个中间层是我们解决问题的常见手法,这方面Proxy模式给了我们很好的实现。