Fork me on GitHub

代理模式(Proxy)

1.1.1 摘要

     今天是父亲节,首先祝爸爸父亲节快乐身体健康,随着互联网飞速的发展,现在许多软件系统都提供跨网络和系统的应用,但在跨网络和系统应用时,作为系统开发者并不希望客户直接访问系统中的对象。其中原因很多考虑到系统安全和性能因素,这时候聪明的开发者想到了在客户端和系统端添加一层中间层----代理层,也是即将要介绍的代理模式。

 

  • 定义

代理模式(Proxy)为另一个对象提供一个替身或占位符以控制对这个对象的访问,简而言之就是用一个对象来代表另一个对象。

  • 意图

提供其他对象一个代理或占位符,来控制该对象的访问权限。

  • 动机

      为什么我们要控制对象的访问权限呢?其中一个原因是通过控制来延迟对象的创建和实例化,直到真正需要使用该对象才进行创建和实例化。由于一些对象创建和实例化需要占用大量系统资源,但我们并不能确定用户一定会调用该对象,所以通过延迟对象实例化来减缓系统资源的消耗。例如文档编辑器如word,我们可以在里面插入链接、图片等,但是并不是我们每次打开word时都有创建和实例化这些对象,特别是实例化图片对象很消耗资源,而且我们有必要实例化所有图片吗?当我们在查看word时,只是看到其中的一部分,所以没有必要实例化所以资源,当我们看下一页时再实例化也不迟。

  • 结构图clip_image001

图1代理模式结构图

1.1.2 正文

      软件系统设计可以提供本地或远程的方法,随着互联网的发展特别是WebService技术的提出,使得更多软件系统都提供远程方法调用。当我们访问网络上一台计算机的资源时,我们正在跨越网络障碍,跨越网络障碍有时候是非常复杂,因为要确保数据安全可靠地传输。如果真的要我们都去解决那些复杂网络问题,那么我估计程序员们疯了。还好代理模式(Proxy)帮我们解决了其中的一些问题----WebService技术。

     现在让我们通过一个简单的加减乘除程序为例,说明什么是代理模式(Proxy)和如何实现。

     现在我们服务器端提供计算方法,分别定义计算类Math和代理类MathProxy。然后我们的客户端通过调研MathProxy来间接用Math类的计算方法。

/// <summary>
/// Define a interface, make proxy and subject
/// have the same methods.
/// </summary>
public interface IMath
{
    /// <summary>
    /// Calc methods.
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <returns></returns>
    double Add(double x, double y);
    double Sub(double x, double y);
    double Mul(double x, double y);
    double Div(double x, double y);
}


/// <summary>
/// A proxy class.
/// </summary>
public class MathProxy : IMath
{
    /// <summary>
    /// MathProxy has a Math's reference.
    /// </summary>
    private Math _math;

    public MathProxy()
    {
        AppDomain ad =
            AppDomain.CreateDomain("MathDomain", null, null);

        /// Create a Math object.
        ObjectHandle obj = ad.CreateInstance(
            "Gof.DesignPattern.Proxy.Math",
            "Gof.DesignPattern.Proxy.Math.Math");

        this._math = obj.Unwrap() as Math;
    }

    #region IMath 成员

    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);
    }

    #endregion

}

/// <summary>
/// Due to Math inherits MarshalByRefObject
/// so the class supports remoting.
/// </summary>
public class Math : MarshalByRefObject, IMath
{
    #region 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)
    {
        if (y == 0)
        {
            return 0;
        }
        
        return x / y;
    }

    #endregion
}


public class Program
{
    static void Main(string[] args)
    {
        
        // Create math proxy.
        MathProxy prox = new MathProxy();

        //
        Console.WriteLine(prox.Add(2, 6));
        Console.WriteLine(prox.Sub(88, 2));
        Console.WriteLine(prox.Mul(22, 55));
        Console.WriteLine(prox.Div(5, 0));

        Console.ReadKey();
    }

}
 proxy1

图2网络中代理模式

      现在我们实现了Math类的代理模式,使得客户端通过MathProxy代理类来调用Math提供的Add()、Sub()、Mul() 和Div() 方法。

      但细心的你肯定发现问题了,我的程序根本没有提供夸网络的实现,最多就是夸应用程序的调用,的确我并没有实现跨网络的调用,那我们该如何实现跨网络呢?还记得我们前面介绍的WebService技术吗?要实现跨网络可以选择使用WebService来公开我们Math类中的方法,让客户端添加Math类的Web引用,从而WebService充当了代理角色。

     我们前面提到的图片延迟加载的例子,这其中也是体现了代理模式的思想。例如我们在查看一些网页之后,我们的浏览器会保留一些网页的信息,从而加快下次网页加载的速度。现在让我们通过一个简单WinForm图片加载器程序说明:

     图片加载器首先查看我们本地是否已经存在要加载的图片,如果存在就直接加载图片,如果不存在它会到网络中下载图片然后进行加载。

proxy2

图3图片加载器界面

      由于时间的关系我们已经把界面设计好了,现在我们要完成的是点击Test Image Proxy按钮的逻辑,首先我们图片加载器会查看本地是否已经存在图片,如果不存在就到网络上加载我们需要的图片就OK了。

/// <summary>
/// The image proxy
/// If we have dowload the picture in local,
/// then the program would get it, otherwise
/// the program would get the picture in internet.
/// </summary>
private class ImageProxy
{
    private static Image _image = null;

    private int _width = 123;
    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; }
    }

    /// <summary>
    /// Gets the image.
    /// </summary>
    public Image Image
    {
        get 
        {
            if (_image != null)
            {
                return _image;
            }
            else
            {
                if (!_retrieving)
                {
                    _retrieving = true;
                    //// Initialize a thread.
                    Thread retrieveThread = new Thread(
                        new ThreadStart(RetrieveImage));
                    //// Start thread.
                    retrieveThread.Start();
                }
            }
            //// If the picture in local, we get it directly.
            return PlaceHolderImage(); 
        }
    }

    /// <summary>
    /// Places the holder image.
    /// Get the picture in local.
    /// </summary>
    /// <returns></returns>
    public Image PlaceHolderImage()
    {
        return new Bitmap(System.Reflection.Assembly.GetExecutingAssembly().
            GetManifestResourceStream(this.GetType().Namespace.ToString() + ".googlefathersday.jpg"));
    }


    /// <summary>
    /// Retrieves the image.
    /// Get the picture in internet.
    /// </summary>
    public void RetrieveImage()
    {
        string url = @"http://www.google.com.hk/logos/2011/fathersday11-hp.jpg";

        HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
        HttpWebResponse response = request.GetResponse() as HttpWebResponse;

        _image = Image.FromStream(response.GetResponseStream());

    }
}

     上面我们完成了点击加载的逻辑,我们提供一个图片加载代理ImageProxy类,然后分别提供本地加载和网络加载功能PlaceHolderImage()方法和RetrieveImage()方法,PlaceHolderImage()方法把图片嵌入到程序中,而RetrieveImage()到网络中加载图片。

     proxy3
proxy4

图4图片加载效果

    这里我们使用了灰色和彩色图片分别表示本地和网络加载效果,现在我们通过代理模式(Proxy)完成了图片延迟加载的效果。

    我觉得《HeadFirst设计模式》中的糖果机代理模式比较有趣,现在也让我们通过C#来实现糖果机代理模式。

    问题描述:现在有些分布在不同地方的糖果机(类似自动售卖机),我们要通过网络方式获得糖果机中的糖果数量和糖果机的状态。

    现在我们要添加三个项目分别是:GumballState.Machine、Host和Client(其中Host是WebService),在GumballState.Machine里面我们定义糖果机售卖逻辑和状态,Host把我们定义售卖机方法通过网络形式发布,Client通过添加Web引用间接调用GumballState.Machine中的方法。

proxy5

图5糖果机程序

 

    首先我们GumballState.Machine类添加五个方法分别是:StartWithQuarters()、InsertQuarter()、TurnCrank()、EjectQuarter()和GetReport()。

  • StartWithQuarters()方法初始化糖果数量和修改糖果机状态。
/// <summary>
/// Starts the with quarters.
/// Calc the current qty and set machine state.
/// </summary>
/// <param name="cnt">The CNT.</param>
public void StartWithQuarters(int cnt)
{
    this._cnt = cnt;
    if (this._cnt > 0)
    {
        this._state = GumballMachineState.NoQuarter;
    }
}
  • InsertQuarter()方法判断用户付款操作和修改糖果机状态。
/// <summary>
/// Inserts the quarter.
/// The user inserts money
/// </summary>
public void InsertQuarter()
{
    switch (this._state)
    {
        case (GumballMachineState.HasQuarter):
            this._logMsg.Append("You can't insert another quarter\n");
            return;

        case (GumballMachineState.NoQuarter):
            this._state = GumballMachineState.HasQuarter;
            this._logMsg.Append("You inserted a quarter\n");
            return;

        case (GumballMachineState.SoldOut):
            this._logMsg.Append("You can't insert a quarter, the machine is sold out\n");
            return;

        case (GumballMachineState.Sold):
            this._logMsg.Append("Please wait, we're already giving you a gumball\n");
            return;

    }
}
  • TurnCrank()方法判断用户确定购买操作和修改糖果机状态。
    /// <summary>
    /// Turns the crank.
    /// The user presses "OK" button to get gumball.
    /// </summary>
    public void TurnCrank()
    {
        if (this._state == GumballMachineState.SoldOut)
        {
            this._logMsg.Append("You turned, but there are no gumballs\n");
        }
        else if (this._state == GumballMachineState.HasQuarter)
        {
            this._logMsg.Append("You turned please wait...\n");
            this._state = GumballMachineState.Sold;
            this.Dispense();
        }
        else if (this._state == GumballMachineState.NoQuarter)
        {
            this._logMsg.Append("You turned but there's no quarter\n");
        }
        else
        {
            this._logMsg.Append("Turning twice doesn't get you another gumball!\n");
        }
    }
  • EjectQuarter()方法判断用户退款操作和修改糖果机状态。
/// <summary>
/// Ejects the quarter.
/// The user cancels order and get money back.
/// </summary>
public void EjectQuarter()
{
    switch (this._state)
    {
        case (GumballMachineState.HasQuarter):
            this._state = GumballMachineState.NoQuarter;
            this._logMsg.Append("Quarter returned\n");
            return;

        case (GumballMachineState.NoQuarter):
            this._state = GumballMachineState.HasQuarter;
            this._logMsg.Append("You haven't inserted a quarter\n");
            return;

        case (GumballMachineState.SoldOut):
            this._logMsg.Append("Sorry, you already turned the crank\n");
            return;

        case (GumballMachineState.Sold):
            this._logMsg.Append("You can't eject, you haven't inserted a quarter yet\n");
            return;

    }
}
  • GetReport()方法返回糖果机销售量和状态给管理者。
/// <summary>
/// Gets the report.
/// </summary>
/// <returns></returns>
public string GetReport()
{
    StringBuilder result = new StringBuilder();
    result.Append("\nMighty Gumball, Inc.");
    result.Append("\n.NET3.5-enabled Standing Gumball Model #2104\n");
    result.Append("Inventory: " + this._cnt + " gumball");
    if (this._cnt != 1) result.Append("s");

    result.Append("\nMachine is ");
    if (_state == GumballMachineState.SoldOut)
    {
        result.Append("sold out");
    }
    else if (_state == GumballMachineState.NoQuarter)
    {
        result.Append("waiting for quarter");
    }
    else if (_state == GumballMachineState.HasQuarter)
    {
        result.Append("waiting for turn of crank");
    }
    else if (_state == GumballMachineState.Sold)
    {
        result.Append("delivering a gumball");
    }
    result.Append("\n");

    string ret = this._logMsg.ToString() + "\n" + result.ToString();
    this._logMsg = new StringBuilder();

    return ret.ToString();
}

    接着我们通过WebService把以上方法通过网络形式发布,然后我们在客户端添加web引用来间接调用GumballState.Machine类中方法。

clip_image001

proxy7

图6客户端添加web引用

   现在我们完成了服务器端的功能了,接下来我们通过控制台应用程序作为客户端调用服务器端公开的方法。

class Program
{
    static void Main()
    {
        // Create proxy object
        GumballMachineClient proxy = new GumballMachineClient();

        proxy.StartWithQuarters(5);
        proxy.InsertQuarter();
        proxy.TurnCrank();

        Console.WriteLine(proxy.GetReport());

        proxy.InsertQuarter();
        proxy.EjectQuarter();
        proxy.TurnCrank();

        Console.WriteLine(proxy.GetReport());

        proxy.InsertQuarter();
        proxy.TurnCrank();
        proxy.InsertQuarter();
        proxy.TurnCrank();
        proxy.EjectQuarter();

        Console.WriteLine(proxy.GetReport());

        proxy.InsertQuarter();
        proxy.InsertQuarter();
        proxy.TurnCrank();
        proxy.InsertQuarter();
        proxy.TurnCrank();
        proxy.InsertQuarter();
        proxy.TurnCrank();

        Console.WriteLine(proxy.GetReport());

        // Wait for user
        Console.ReadKey();
    }
}

图7客户端调用web方法

      现在我们通过控制台应用程序添加web引用之后,来调用GumballState.Machine类中方法,我们终于正在实现了一个跨网络的应用程序调用,WebService充当了proxy的角色,而且帮我们解决了很多网络的问题使得我们实现跨网络程序十分方便。

1.1.3 总结

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

  • 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个局域代表对象。这个不同的地址空间可以是在本机器中,也可是在另一台机器中。远程代理又叫做大使(Ambassador)。好处是系统可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的对象是局域的而不是远程的,而代理对象承担了大部份的网络通讯工作。由于客户可能没有意识到会启动一个耗费时间的远程调用,因此客户没有必要的思想准备。
  • 虚拟(Virtual)代理(图片延迟加载的例子):根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。使用虚拟代理模式的好处就是代理对象可以在必要的时候才将被代理的对象加载;代理可以对加载的过程加以必要的优化。当一个模块的加载十分耗费资源的情况下,虚拟代理的好处就非常明显。
  • Copy-on-Write代理:虚拟代理的一种。把复制(克隆)拖延到只有在客户端需要时,才真正采取行动。
  • 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等。

 

     代理模式(Proxy)VS 装饰者(Decorator)

     意图:它们都提供间接访问对象层,都保存被调用对象的引用。

     代理模式(Proxy):为另一个对象提供一个替身或占位符以控制对这个对象的访问,简而言之就是用一个对象来代表另一个对象。

    装饰者(Decorator):动态地给一个对象添加一些额外的职责,就增加功能来说,Decorator模式比生成子类更为灵活,它避免了类爆炸问题,像装饰者(Decorator),代理模式(Proxy)组成一个对象并提供相同的接口,但代理模式并不关心对象动态职能的增减。

    在代理模式(Proxy)中Subject定义了主要的功能,而且Proxy根据Subject提供功能控制对象的访问权限。在装饰者(Decorator)中Component只是提供了其中的一些功能,需要通过装饰链动态给对象增加职能。

 

代理模式(Proxy)例子源程序

posted @ 2011-06-19 20:43  JK_Rush  阅读(29037)  评论(11编辑  收藏  举报