读书笔记之 - javascript 设计模式 - 责任链模式

责任链模式可以用来消除请求的发送者和接收者之间的耦合。这是通过实现一个由隐式地对请求进行处理的对象组成的链而做到的。链中的每个对象可以处理请求,也可以将其传给下一个对象。

责任链的结构:

责任链由多个不同类型的对象组成,发送者是发出请求的对象,而接收者则是链中那些接收这种请求并且对其进行处理或者传递的对象。请求本身有时也是一个对象,它封装着操作有关的所有数据。其典型的运转流程大致如下:

发送者知道链中的第一个接收者。它向这个接收者发出请求。

每一个接收者都对请求进行分析,然后要么处理它,要么将其往下传。

每一个接收者知道的其它对象只有一个,即它在链中的下家。

如果没有任何接收者处理请求,那么请求将从链上离开。不同的实现对此有不同的反应,既可以无声无息,也可以抛出一个错误。

下面来回顾一下前面的图书馆示例:

复制代码
var Interface = function () {};

var Publication = new Interface('Publication',['getIsbn','setIsbn','getTitle','setTitle','getAuthor','setAuthor','getGenres','setGenres','display']);
var Library = new Interface('Library',['addBook','findBook','checkoutBook','returnBook']);
var Catalog = new Interface('Catalog',['handleFilingRequest','findBooks','setSuccessor']);
复制代码

与以前相比,Publication接口只是多了俩个新方法:getGenres 和 setGenres,Library接口新增了一个为图书馆添加藏书的方法,Catalog接口是新增的,他将用来创建保存图书对象的类。

Catalog 接口有三个方法:handleFilingRequest会根据传给它的图书是否符合特定标准而决定是否将其编入内部目录中;findBooks会根据某些参数对内部目录进行搜索;setSuccessor则会设定责任链的下一环。

现在来看一下Book和PublicLibrary这俩个将被重用的类。

var Book = function (isbn,title,author,genres) {
    //.... 
}

Book类现在多了一个用来说明图书所属类别的参数。

复制代码
PublicLibrary.prototype = {
    addBook: function (newBook) {
        this.catalog[newBook.getIsbn()] = {book: newBook, available: true};
    },
    findbooks: function () {
        var result = [];
        for (var isbn in this.catalog) {
            if (!this.catalog.hasOwnProperty(isbn)) {
                continue;
            }
            if (this.catalog[isbn].getTitle().match(searchString) || this.catalog[isbn].getAuthor().match(searchString)) {
                result.push(this.catalog[isbn]);
            }
        }
        return result;
    },
    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() + 'is 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() + 'is not found.');
        }
    }
};
复制代码

现在来实现目录对象,在为这些对象编写代码之前,所有用于判断一本书是否应该编入某个特定目录的代码都封装在Catalog类中。这意味着需要在 PublicLibrary 对象中把每一本书都提供给每一种分类目录进行处理。

复制代码
function PublicLibrary(books){
    this.catalog = {};
    this.biographyCatalog = new BiographyCatalog();
    this.fantasyCatalog = new FantasyCatalog();
    this.mysteryCatalog = new MysteryCatalog();
    this.nonFictionCatalog = new NonFictionCatalog();
    this.sciFiCatalog = new SciFiCatalog();

    for (var i = 0; i < books.length; i++) {
        this.addBook(books[i]);
    }
}
PublicLibrary.prototype = {
    findBooks: function (searchString){},
    checkoutBook:function(book){},
    returnBook:function(book){},
    addBook: function (newBook) {
        this.catalog[newBook.getIsbn()] = {book: newBook, available: true};
        this.biographyCatalog.handleFillingRequest(newBook);
        this.fantasyCatalog.handleFillingRequest(newBook);
        this.mysteryCatalog.handleFillingRequest(newBook);
        this.nonFictionCatalog.handleFillingRequest(newBook);
        this.sciFiCatalog.handleFillingRequest(newBook);
    }
}
复制代码

其中固化了对5个不同类的依赖。如果想增加更多的图书类别,那就需要修改构造函数和addBook方法这俩处代码。此外,这些目录类固化在构造函数中也没有什么意义,因为PublicLibrary的不同实例可能希望拥有完全不同的一套分类目录。而你不可能在对象实例化之后再修改其支持的类别。这些都充分说明了前面的方法并不可取。下面来看责任链模式能带来什么改进。

复制代码
var PublicLibrary = function (books,firstGenreCatalog) {
    this.catalog = {};
    this.firstGenreCatalog = firstGenreCatalog;

    for (var i = 0; i < books.length; i++) {
        this.addBook(books[i]);
    }
}
PublicLibrary.prototype = {
    findBooks: function (searchString){},
    checkoutBook:function(book){},
    returnBook:function(book){},
    addBook: function (newBook) {
        this.catalog[newBook.getIsbn()] = {book: newBook, available: true};
        this.firstGenreCatalog.handleFillingRequest(newBook);
    }
}
复制代码

这个改进很明显,现在需要保存的只是指向分类目录链中的第一个环节。如果想把一本新书编入各种分类目录中,只需将其传给链中的第一个目录对象即可,每个目录都会把请求往下传。

现在不再有固化在代码中的依赖。所有分类目录都在外部实例化,因此不同的PublicLibrary实例能够使用不同的分类。下面显示了其用法:

复制代码
var biographyCatalog = new BiographyCatalog();
var fantasyCatalog = new FantasyCatalog();
var mysteryCatalog = new MysteryCatalog();
var nonFictionCatalog = new NonFictionCatalog();
var sciFiCatalog = new SciFiCatalog();

biographyCatalog.setSuccessor(fantasyCatalog);
fantasyCatalog.setSuccessor(mysteryCatalog);
mysteryCatalog.setSuccessor(nonFictionCatalog);
nonFictionCatalog.setSuccessor(sciFiCatalog);

var myLibrary = new PublicLibrary(books, biographyCatalog);
// you can add links to the chain whenever you like.
var historyCatalog = new HistoryCatalog();
sciFiCatalog.setSuccessor(historyCatalog);
复制代码

这个例子中,原来的链上有5个环节,第六个是后来加的。这意味着图书馆每增加一本书都会通过调用链上第一个环节的handleFilingRequest方法发起对该书的编目请求。该请求将沿着目录逐一经过6个目录,最后从链尾离开。链上新增的任何目录都会被挂到链尾。

前面已经考察了使用责任链模式的动机以及与其使用相关的一般结构,但是还没有研究过链上的对象本身。这些链有一个共同的特性,他们都拥有一个指向链上下一个对象successor的引用。链上最后一个对象,这是一个空引用。链上的对象至少都要实现一个共同的方法,即负责处理请求的方法,这些对象不用像前面的例子中那样属于同一个类,但是他们必须实现同样的接口。通常它们分别属于一个类的各种子类,分类目录对象就是这样实现的:

复制代码
var GenreCatalog = function () {
    this.successor = null;
    this.catalog = [];
}
GenreCatalog.prototype = {
    _bookMatchesCriteria:function (book) {
        return false;
    },
    handleFilingRequest: function (book) {
        if (this._bookMatchesCriteria(book)) {
            this.catalog.push(book);
        }
        if (this.successor) {
            this.successor.handleFilingRequest(book);
        }
    },
    findBooks: function (request) {
        if (this.successor) {
            return this.successor.findbook(request);
        }
    },
    setSuccessor: function (successor) {
        if (Interface.ensureImplements(successor,Catalog)) {
            this.successor = successor;
        }
    }
}
复制代码

这个超类提供了所有必须方法的默认实现。他们可以被各种子类继承。子类只需要重写findBooks和_bookMatchesCriteria这俩个方法。其中后一个方法是一个伪私用方法,它负责判断一本书是否应该被编入相关分类目录。GenreCatalog类提供了这俩个方法最简单的实现,以防子类没有重写它们。

从这个超类派生一个分类目录子类很简单:

复制代码
var SciFiCatalog = function () {};
extend(SciFiCatalog, GenreCatalog);
SciFiCatalog.prototype._bookMatchesCriteria = function (book) {
    var genres = book.getGenres();
    if (book.getTitle().match(/space/i)) {
        return true;
    }
    for (var i = 0; i < Publication.length; i++) {
        var obj = Publication[i];
        var genre = genres[i].toLowerCase();
        if (genre === 'sci-fi'||genre==='scifi'||genre==='science fiction') {
            return true;
        }
    }
    return false;
}
复制代码

这段代码首先创建了一个空函数,让其继承GenreCatalog,然后实现了_bookMatchesCriteria方法。它这个方法对图书的书名和类别进行检查,判断是否二者中都有一个能够匹配某些搜索用词。

传递请求:

在链上传递请求有许多不同的方法可供选择。最常见的做法就是要么使用一个专门的请求对象,要么就是根本不使用参数,只依靠方法自身传递信息。不用参数调用方法是最简单的办法。在前面的例子中,我们使用了另一种常见技术,即把图书对象作为请求进行传递。图书对象封装了在判断链上那些环节应该将其编入他们的目录时需要的所有数据。这属于将现有对象作为请求对象进行重用的情况。在本节中,我们将实现分类目录的findBooks方法。并将考察如何使用专门的请求对象来在链上的各个环节之间传递数据。

首先,我们需要修改一下PublicLibrary的findBooks方法。以便可以根据类别来缩小搜索范围。如果调用该方法时提供了可选的genres参数,那么搜索将只属于其指定类别的图书中进行:

复制代码
function PublicLibrary(books){
    //...
}
PublicLibrary.prototype = {
    addBook: function (newBook) {
        //...
    },
    findbooks: function (searchString,genres) {
        if(typeof genres =='object' && genres.length>0) {
            var requestObject = {
                searchString:searchString,
                genres:genres,
                results:[]
            };
            var responseObject = this.firstGenreCatalog.findBooks(requestObject);
            return requestObject.results
        }else{
            var result = [];
            for (var isbn in this.catalog) {
                if (!this.catalog.hasOwnProperty(isbn)) {
                    continue;
                }
                if (this.catalog[isbn].getTitle().match(searchString) || this.catalog[isbn].getAuthor().match(searchString)) {
                    result.push(this.catalog[isbn]);
                }
            }
            return result;
        }

    },
    checkoutBook: function (book) {
        //...
    },
    returnBook: function (book) {
        //...
    }
};
复制代码

findBooks方法创建了一个用来封装与请求相关的所有信息的对象。这些信息包含将要搜索的一组类别,搜索用词和一个用来保存查找结果的空数组。

现在我要实现GenreCatalog这个超类中的findBooks方法。这个方法将被用在所有子类中,它不需要重写。下面详细说明。

复制代码
var GenreCatalog = function () {
    this.successor = null;
    this.catalog = [];
    this.genreName = [];
}
GenreCatalog.prototype = {
    _bookMatchesCriteria:function (book) {
        //...
    },
    handleFilingRequest: function (book) {
        //...
    },
    findBooks: function (request) {
        var found = false;
        for (var i = 0; i < request.genres.length; i++) {
            for (var j = 0; j < this.genreName.length; j++) {
                if (this.genreName[j]===request.genres[i]) {
                    found = true;
                    break;
                }

            }
        }

        if(found){
            outerloop:for (var i = 0; i < this.catalog.length; i++) {
                var book = this.catalog[i];
                if (book.getTitle().match(request.searchString)||book.getAuthor().match(request.searchString)) {
                    for (var j = 0; j < request.results.length; j++) {
                        if(request.results[j].getIsbn()===book.getIsbn()) {
                            continue outerloop;
                        }
                    }
                    request.results.push(book);
                }

            }
        }

        if (this.successor) {
            return this.setSuccessor.findBooks(request);
        }else{
            return request;
        }
    },
    setSuccessor: function (successor) {
        //...
    }
}
复制代码

这个方法可以分为三大部分。第一部分逐一检查请求对象中的每一个类别名称,看其是否与对象中保存的一组类别名称中的某一个匹配。如果匹配,那么代码的第二部分会逐一检查目录中的所有图书,看看其书名和作者名是否与搜索词相匹配,匹配的图书将被添加到请求对象results数组中,前提是该数组中没有这本书。最后一部分,如果当前目录对象不是链上的最后一环,那么请求将沿着目录继续下传,否则将返回请求对象。最终请求对象将从链尾开始沿着目录链逐环向上返回,直到返回给客户代码。

在超类GenreCatalog中,用于保存类别名称属性的genreName是一个空数组,在子类中必须为其填入一些具体的类别名称,下面是SciFiCatalog类的实现代码。

复制代码
var SciFiCatalog = function () {
    this.genreName = ['sci-fi', 'scifi', 'fiction'];
};
extend(SciFiCatalog, GenreCatalog);
SciFiCatalog.prototype._bookMatchesCriteria = function (book) {
    //...
};
复制代码

通过把请求封装为一个对象,可以使其更容易管理。在GenreCatalog类的findBooks方法这种复杂的代码中尤其如此。它有助于让搜索用词、类别和搜索结果在通过链上所有必须经过的环节烦人过程中保持完好无缺。

责任链模式的适用场合

适用责任链模式的场合有几种,在图书馆的例子中,我们想发出对某本图书进行分类的请求。我们事先不知道如果可以的话应该被分类到哪一个目录,也不知道可用目录的数目和类型,为了解决这些问题,我们使用了一个目录链,其中每一个目录都会把图像沿链传递给下家。

如果事先不知道在几个对象中有哪些能够处理请求,那么也就应该使用责任链模式。如果这批处理器对象在开发期间不可知,而是需要动态指定的话,那么也应该使用这种模式。该模式还可以用在对于每个请求不不止有一个对象可以对它进行处理这种情况,在图书馆例子中,每本图书都可以被分类到不止一个目录,该请求可以先被一个对象处理,然后进行往下传,在链上可能后面还有另一个对象会处理它。

使用这种模式,可以把特定的具体类与客户隔离开,并代之以一条弱耦合的对象组成的链,他将隐式的对请求进行处理。这有助于提高代码的模块化程度和可维护性。

前面的例子是一个用来显示如何用责任链优化组合对象简单的例子。我们将通过为图片添加标签来进一步阐明这个概念。

标签是一个描述性的标题,可以用来对图片分类。图片和图片库都可以添加标签。为图片库添加标签相当于让所有图片都使用这个标签。你可以在层次体系的任何层次上搜索具有指定标签的图像。这正是责任链的优化资源可用的地方,如果在搜索过程中遇到一个具有所有请求的标签的组合对象节点,那就可以停止请求并将该节点的所有叶节点作为搜索结果返回。

var Composite = new Interface('Composite',['add','remove','getChild','getAllLeaves']);
var GalleryItem = new Interface('', ['hide', 'show', 'addTag', 'getPhotoWithTag']);

我们在接口中添加了三个方法。addTag将为所有调用它的对象以及子对象添加一个标签。getPhotoWithTag返回一个具有特定标签的所有图片组成的数组,而对叶节点使用这个方法则返回一个由它自身组成的数组。

复制代码
var DynamicGallery = function (id) {
    this.children = [];
    this.tags = [];
    this.element = document.createElement('div');
    this.element.id = id;
    this.element.className = 'dynamic-gallery';
};
DynamicGallery.prototype = {
    addTag: function (tag) {
        this.tags.push(tag);
        for (var node, i = 0; node=this.getChild(i); i++) {
            node.addTag(tag);
        }
    }
}

var GalleryImage = function (src) {
    this.element = document.createElement('img');
    this.element.className = 'gallery-image';
    this.element.src = src;
    this.tags = [];
};

GalleryImage.prototype = {
    addTag: function (tag) {
        this.tags.push(tag);
    }
};
复制代码

我们在组合对象类和叶类中都添加了一个名为tags的数组,它保存着代表标签的字符串。在叶类的addTag方法中,只需要把作为参数的传入的字符串加入tags数组即可。

而在组合类的这个方法中,除了这样做以外,还要把请求在层次体系中往下传递。尽管把标签给予一个组合对象相当于把该标签给予其所有的子对象,但是我们还是的为每一个子对象添加该标签。这是因为搜索可能从层次体系中的任何层次开始,如果没有为每一个叶节点添加标签的话,那么从较低层次开始搜索可能会错过在层次体系中较高层次上分配的标签。

getPhotoWithTag 方法是责任链发挥优化作用的地方。我们将分别讲述每个类中的这个方法,先看组合对象类:

复制代码
var DynamicGallery = function (id) {
    this.children = [];
    this.element = document.createElement('div');
    this.element.id = id;
    this.element.className = 'dynamic-gallery';
};
DynamicGallery.prototype = {
    addTag: function (tag) {
        this.tags.push(tag);
        for (var node, i = 0; node=this.getChild(i); i++) {
            node.addTag(tag);
        }
    },
    getAllLeaves: function () {
        var leaves = [];
        for (var node, i = 0; node < this.getChild(i); i++) {
            leaves = leaves.concat(node.getAllLeaves());
        }
        return leaves;
    },
    getPhotosWithTag: function (tag) {
        for (var i = 0; i < this.tags.length; i++) {
            if (this.tags[i]===tag) {
                return this.getAllLeaves();
            }
        }
        for (var results = [],node,i=0;node=this.getChild(i);i++) {
            results = results.concat(node.getPhotosWithTag(tag));
        }
        return results;
    }
}
复制代码

这段代码实际上为DynamicGallery添加了俩个方法,getPhotosWithTag 方法是按责任链的风格实现的。它首先要判断当前对象是否能处理请求。其具体做法就是在当前对象的tags数组中检查指定的标签,如果能找到,那就表明层次体系中当前这个组合对象的所有子对象也都具有这个标签,此时即可停止搜索,然后在这个层次上处理请求。如果找不到指定标签,则将请求传递给每一个子对象,并返回结果。

getAllLeaves 方法用于获取特定组合对象的所有叶节点后代节点并将它们组织为一个数组返回,它作为一个普通的数组对象方法实现的,也就是说同样的方法调用会被传递到每一个子对象。

这些方法在叶类中的实现相对简单,它们返回的结果之中都只包含叶对象本身(但getPhotosWithTag方法在当前叶对象不匹配指定标签时返回一个空数组):

复制代码
var GalleryImage = function (src) {
    this.element = document.createElement('img');
    this.element.className = 'gallery-image';
    this.element.src = src;
    this.tags = [];
};

GalleryImage.prototype = {
    addTag: function (tag) {
        this.tags.push(tag);
    },
    getAllLeaves: function () {
        return [this];
    },
    getPhotosWithTag: function (tag) {
        for (var i = 0; i < this.tags.length; i++) {
            if (this.tags[i]===tag) {
                return [this];
            }
        }
        return [];
    }
};
复制代码

我们来分析一下本例中使用责任链有什么收获以及组合模式是如何为此提供帮助的。如果把getPhotosWithTag作为一个组合对象方法实现,让它只负责把方法调用传递给每个子对象,那么每个子对象又得把自己的所有标签逐一与所搜索的标签进行比较,我们的做法使用了责任链模式来确定是否可以早一点结束 搜索。在最差的情况下,最后仍然是叶节点在处理请求,但要是某个组合对象节点具有那个标签的话,那么请求可能在层次体系中往上几个层次的地方就能得到处理。

实际上,组合对象方法需要耗费的计算量越大,在层次体中较高的层次上处理请求并使用某种辅助方法(比如getAllLeaves)提高请求的处理效率所带来的好处也就越大。

责任链模式的利与弊:

借助责任链模式,可以动态选择由那个对象处理请求。这意味着你可以使用只有在运行期间才能知道的条件来把任务分派给最恰当的对象。你还可以使用这个模式消除发出请求的对象与处理请求的对象之间的耦合。藉此你可以在模块化的组织方法获得更大的灵活性,而且在重构和修改代码的时候也不用担心会把类名固化在算法中。

在已经有现成的链或者层次体系的情况下,责任链模式更加有效。与组合模式的结合使用就属于这种情况。你可以重用组合对象的结构来传递请求,直到找到一个可以处理请求的对象。在此情况下,不用编写粘合性代码来实例化那些对象或建立链,因为那些东西早已准备妥当。藉此可以实现把请求转给恰当的处理程序的方法。

弊端:

在责任链模式中,请求与具体处理程序被隔离开来。因此无法保证它一定会被处理,而不是直接从链尾离开。也无法得知由那个请求具体处理它。

责任链和组合对象的搭配使人困惑,人们原本期望认为,组合对象节点完全可以与叶节点互换使用,而且客户代码也看不出其中的差别,所有方法调用都被组合对象往层次体系的下层传递,但是责任链方法改变了这个约定。引入责任链之后,有些方法会在组合对象这里进行处理,而不会继续往下传。想要让这些方法可以与叶方法互换,其代码的编写会很棘手,他们的效率很高,但为此付出的代价是代码的复杂性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值