【读书笔记】读《JavaScript设计模式》之代理模式

一、定义

  代理是一个对象,它可以用来控制对另一个对象的访问。它与另外那个对象实现了同样的接口,并且会把任何方法调用传递给那个对象。另外那个对象通常称为本体。代理可以代替其实体被实例化,并使其可被远程访问。它还可以把本体的实例化推迟到真正需要的时候,对于实例化比较费时的本体,或者因尺寸较大以至于不用时不易保存在内存中的本体,这特别有用。在处理那些需要较长时间才能把数据载入用户界面的类时,代理也大有裨益。

  代理模式最基本的形式是对访问进行控制。代理对象和另一个对象(本体)实现的是同样的接口。实际上工作还是本体在做,它才是负责执行所分派的任务的那个对象或类。代理对象所做的不外乎节制对本体的访问。要注意,代理对象并不会在另一对象的基础上添加方法或修改方法(就像装饰者那样),也不会简化那个对象的接口(就像门面元素那样)。它实现的接口与本体完全相同,所有对它进行的方法调用都会被传递给本体。

二、代理如何控制对本体的访问

  2.1 直接代理

var Library = new Interface('Library', ['findBooks', 'checkoutBook', 'returnBook']);

var PublicLibrary = function(books) { // implements Library
    this.catalog = {};
    for(var i = 0, len = books.length; i < len; i++) {
        this.catalog[books[i].getIsbn()] = { book: books[i], available: true };
    }
};
PublicLibrary.prototype = {
    findBooks: function(searchString) {
        var results = [];
        for(var isbn in this.catalog) {
            if(!this.catalog.hasOwnProperty(isbn)) continue;
            if(searchString.match(this.catalog[isbn].getTitle()) ||
                    searchString.match(this.catalog[isbn].getAuthor())) {
                results.push(this.catalog[isbn]);
            }
        }
        return results;
    },
    checkoutBook: function(book) {
        var isbn = book.getIsbn();
        if(this.catalog[isbn]) {
            if(this.catalog[isbn].available) {
                this.catalog[isbn].available = false;
                return this.catalog[isbn];
            }
            else {
                throw new Error('PublicLibrary: book ' + book.getTitle() +
                        ' is not currently available.');
            }
        }
        else {
            throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.');
        }
    },
    returnBook: function(book) {
        var isbn = book.getIsbn();
        if(this.catalog[isbn]) {
            this.catalog[isbn].available = true;
        }
        else {
            throw new Error('PublicLibrary: book ' + book.getTitle() + ' not found.');
        }
    }
};
var PublicLibraryProxy = function(catalog) { // implements Library
    this.library = new PublicLibrary(catalog);
};
PublicLibraryProxy.prototype = {
    findBooks: function(searchString) {
        return this.library.findBooks(searchString);
    },
    checkoutBook: function(book) {
        return this.library.checkoutBook(book);
    },
    returnBook: function(book) {
        return this.library.returnBook(book);
    }
};

  PublicLibraryProxy和PublicLibrary实现了同样的接口和同一批方法。这个类在实例化时会创建一个PublicLibrary实例并将其作为属性保存。如果调用该类的某个方法,它会通过这个属性在其PublicLibrary实例上调用同名方法。这种类型的代理也可以通过检查本体的接口并为每一个方法创建对应方法这样一种方式动态地创建。

  2.2 虚拟代理

var PublicLibraryVirtualProxy = function(catalog) { // implements Library
    this.library = null;
    this.catalog = catalog; // Store the argument to the constructor.
};
PublicLibraryVirtualProxy.prototype = {
    _initializeLibrary: function() {
        if(this.library === null) {
            this.library = new PublicLibrary(this.catalog);
        }
    },
    findBooks: function(searchString) {
        this._initializeLibrary();
        return this.library.findBooks(searchString);
    },
    checkoutBook: function(book) {
        this._initializeLibrary();
        return this.library.checkoutBook(book);
    },
    returnBook: function(book) {
        this._initializeLibrary();
        return this.library.returnBook(book);
    }
};

  PublicLibraryVirtualProxy会把构造函数的参数保存起来,直到有方法被调用时才真正执行本体的实例化。这样一来,如果图书馆对象一直没有被调用到,那么它就不会被创建出来。虚拟代理通常具有某种能触发本体的实例化的事件。在本例中,方法调用就是触发元素。

三、与装饰者模式的区别

  共同点:两者都要对其他对象进行包装,都要事先与被包装对象相同的接口,而且都要把方法调用传递给被包装对象。

  不同点:

  1> 装饰者会对被包装对象的功能进行修改或扩充,而代理只不过是控制对它的访问。除了有时可能会添加一些控制代码之外,代理并不会对传递给本体的方法调用进行修改。而装饰者就是为修改方法而生的。

  2> 在装饰者模式中,被包装对象的实例化过程是完全独立的。这个对象创建出来之后,你可以随意为其包裹上一个或更多装饰者。而在代理模式中,被包装对象的实例化时代理的实例化过程的一部分。在某些类型的虚拟代理中,这种实例化受到严格控制,它必须在代理内部进行。

  3> 代理不会像装饰者那样相互包装。它们一次只使用一个。

四、使用场合

  虚拟代理是一个对象,用于控制对一个创建开销昂贵的资源的访问。虚拟代理是一种优化模式。如果有些对象需要使用大量内存保存其数据,而你并不需要在实例化完成之后访问这些数据,或者,其构造函数需要进行大量计算那就应该使用虚拟代理将设置开销的产生推迟到真正需要使用数据的时候。代理可以在设置的进行过程中提供类似于“正在加载.....”这样的消息,这可以形成一个反应积极的用户界面,以免让用户面对一个没有任何反馈的空白页面发呆,不知道究竟发生了什么事。

  远程代理则没有这样清楚的用例。如果需要访问某种远程资源的话,那么最好是用一个类或独享来包装它,而不是一遍又一遍地手工设置XMLHttpRequest对象。问题在于应该用什么类型的对象来包装这个资源呢?这主要是个命名问题。如果包装对象实现了远程资源的所有方法,那么它就是一个远程代理。如果它会在运行期间增添一些方法,那它就是一个装饰者。如果它简化了该远程资源(或多个远程资源)的接口,那它就是一个门面。远程代理是一种结构型模式,它提供了一个访问位于其他环境中的资源的原生JavaScript API。

  下面引入包装web服务的通用包装模式的代理——

var WebserviceProxy = function() {
    this.xhrHandler = XhrManager.createXhrHandler();
};
WebserviceProxy.prototype = {
    _xhrFailure: function(statusCode) {
        throw new Error('StatsProxy: Asynchronous request for stats failed.');
    },
    _fetchData: function(url, dataCallback, getVars) {
        var that = this;
        var callback = {
            success: function(responseText) {
                var obj = eval('(' + responseText + ')');
                dataCallback(obj);
            },
            failure: that._xhrFailure
        };

        var getVarArray = [];
        for(varName in getVars) {
            getVarArray.push(varName + '=' + getVars[varName]);
        }
        if(getVarArray.length > 0) {
            url = url + '?' + getVarArray.join('&');
        }

        xhrHandler.request('GET', url, callback);
    }
};

/* StatsProxy class, using WebserviceProxy. */

var StatsProxy = function() {}; // implements PageStats
extend(StatsProxy, WebserviceProxy);

/* Implement the needed methods. */

StatsProxy.prototype.getPageviews = function(callback, startDate, endDate, page) {
    this._fetchData('/stats/getPageviews/', callback, {
        'startDate': startDate,
        'endDate': endDate,
        'page': page
    });
};
StatsProxy.prototype.getUniques = function(callback, startDate, endDate, page) {
    this._fetchData('/stats/getUniques/', callback, {
        'startDate': startDate,
        'endDate': endDate,
        'page': page
    });
};
StatsProxy.prototype.getBrowserShare = function(callback, startDate, endDate, page) {
    this._fetchData('/stats/getBrowserShare/', callback, {
        'startDate': startDate,
        'endDate': endDate,
        'page': page
    });
};
StatsProxy.prototype.getTopSearchTerms = function(callback, startDate, endDate, page) {
    this._fetchData('/stats/getTopSearchTerms/', callback, {
        'startDate': startDate,
        'endDate': endDate,
        'page': page
    });
};
StatsProxy.prototype.getMostVisitedPages = function(callback, startDate,endDate) {
    this._fetchData('/stats/getMostVisitedPages/', callback, {
        'startDate': startDate,
        'endDate': endDate
    });
};

五、优势

  远程代理的好处在于,可以把远程资源当做本地JavaScript对象使用。它减少了为远程访问资源而不得不编写的粘合性代码的数量,并且为此提供了单一的接口。如果远程资源提供的API发生了改变,需要修改的代码只有一处。它还把与远程资源相关的所有数据统一保存在一个地方,其中包括资源的URL、数据格式、命令和相应的结构。如果需要访问多个WEB服务,那么可以先创建一个抽象的通用远程代理类,然后针对每一种要访问的web服务派生出一个子类。

  虚拟代理的好处在于:可以把大对象的实例化推迟到其他元素加载完毕之后。如果虚拟代理包装的资源没有被用到,那么根本就不会被加载。不用操心实例化开销的问题。

六、劣势

  代理可以掩盖了大量复杂行为。

  对于远程代理而言,其背后的复杂行为包括发出XHR请求、等待响应、对响应接口进行解析以及输出收到的数据。在使用远程代理的程序员眼里,它可能就像一个本地资源,但访问它花的时间却比访问本地资源要出几个数量级。而且,它需要和毁掉函数结合使用,因为让方法直接放回结果是行不通的,这给代码增加了一定的复杂性,并且进一步拆穿了其访问资源的假象。这里的问题也能通过精心编撰的程序文档来消除(至少也可以减轻其不利影响)。

  对于虚拟代理也是如此。它掩盖了推迟本体的实例化的逻辑。使用这种代理的程序员并不清楚又那些操作会触发对象的实例化。

  综上,因为代理与其本体完全可以互换,如果没有令人信服的理由使用代理的话(或者降低代码的冗余程度,或者提高其模块化的程度,或运行效率),最好还是选择直接访问本体这种简单得多的方法。

源自:JavaScript设计模式(人民邮电出版社)——第十二章,装饰者模式