读书笔记之 - javascript 设计模式 - 工厂模式
2014-08-12 10:58 sai.zhao 阅读(284) 评论(0) 编辑 收藏 举报一个类或者对象中,往往会包含别的对象。在创建这种对象的时候,你可能习惯于使用常规方式,即用 new 关键字和类构造函数。
这会导致相关的俩个类之间产生依赖。
工厂模式,就是消除这俩个类之间的依赖性的一种模式,它使用一种方法来决定究竟实例化那个具体的类。
简单工厂模式
假设你想开几个自行车商店,每个商店都有几种型号的自行车出售,可以用这样一个类来表示:
var BicycleShop = function(){} BicycleShop.prototype = { sellBicycle:function(model){ var bicycle; switvh(model){ case 'The Speedster': bicycle = new Speedster(); break; case 'The Lowrider': bicycle = new Lowrider(); break; case 'The Comfort Cruiser': default: bicycle = new ComfortCruiser(); } Interface.ensureImplements(bicycle,Bicycle); bicycle.assemble(); bicycle.wash(); return bicycle; } }
sellBicycle 方法根据所要求的自行车型号用 switch 来创建实例。
Bicycle接口:
var Bicycle = new Interface('Bicycle',['assemble','wash','ride','repair']);
Speedster 类:
var Speedster = function(){ ... }; Speedster.prototype = { assemble:fucntion(){ ... }, wash:fucntion(){ ... }, ride:fucntion(){ ... }, repair:fucntion(){ ... } } var californiaCruisers = new BicycleShop(); var yourNewBike = californiaCruisers.sellBicycle('The Speedster');
如果想加入一款新型号的车,只能修改BicycleShop的代码了。最好的办法就是把sellBicycle方法中“创建新实例”这部分工作转交给一个简单工厂对象:
var BicycleFactory = { createBicycle:function(model){ var bicycle; switch(model){ case 'The Speedster': bicycle = new Speedster(); break; case 'The Lowrider': bicycle = new Lowrider(); break; case 'The Comfort Cruiser': default: bicycle = new ComfortCruiser(); } Interface.ensureImplements(bicycle,Bicycle); return bicycle; } }
BicycleFactory是一个单体,用来把createBicycle封装在一个命名空间中,这个方法返回一个实现了Bicycle接口的对象。然后你可以照常对其进行组装和清洗。
var BicycleShop = function(){} BicycleShop.prototype = { sellBicycle:function(model){ var bicycle = BicycleFactory.createBicycle(model); bicycle.assemble(); bicycle.wash(); return bicycle; } }
这样,有关提供车型的所有信息都集中到一个地方管理,所以添加更多车型很容易。
BicycleFactory 就是简单工厂的一个很好的例子,这种模式把成员对象的创建工作转交给一个外部对象。这个外部对象可以像本例一样是一个简单的命名空间,也可以是一个类的实例。
真正的工厂模式
真正的工厂模式与简单的工厂模式区别在于,它不是另外使用一个类或者对象来创建自行车,而是使用一个子类。
按照正常定义,工厂是一个将其成员对象的实例化推迟到子类中进行的类。
我们打算让自行车商店自己决定从那个生产厂家进货。出于这个原因,单单一个BicycleFactory对象将无法提供需要的所有自行车实例。我们可以把BicycleShop设计为抽象类,然后让子类根据各自的进货渠道实现其createBicycle方法:
var BicycleShop = function(){} BicycleShop.prototype = { sellBicycle:function(model){ var bicycle = this.createBicycle(model); bicycle.assemble(); bicycle.wash(); return bicycle; }, createBicycle:function(model){ throw new Error('Unsupported operation on an abstract class.'); } }
以上类中定义了createBicycle方法,但是调用这个方法,会抛出一个错误,现在BicycleShop是一个抽象类,它不能被实例化,只能用来派生子类。
设计一个经销特定自行车生产厂家产品的子类需要扩展 BicycleShop 方法,下面是扩展方法。
var AcmeBicycleShop = function(){}; extend(AcmeBicycleShop,BicycleShop); AcmeBicycleShop.prototype.createBicycle = function(model){ var bicycle; switch(model){ case 'The Speedster': bicycle = new AcmSpeedster(); break; case 'The Lowrider': bicycle = new AcmLowrider(); break; case 'The Comfort Cruiser': default: bicycle = new AcmComfortCruiser(); } Interface.ensureImplements(bicycle,Bicycle); return bicycle; } var alecsCruisers = new AcmeBicycleShop(); var yourNewBike = alecsCruisers.sellBicycle('The Lowrider');
对Bicycle进行的一般性操作的代码完全可以放在父类BicycleShop中,而对具体的Bicycle对象进行实例化的工作则被留到子类中。
这样,一般性的代码被集中在一个位置,而个体性的代码则被封装在子类中。
如果需要像前面那样,创建一些用不同方法实现同一接口的对象,可以使用简单工厂方法来简化-选择实现-的过程。这种选择可以是明确的,也可以是隐含的,明确如自行车例子,隐含的如下面的HXR实现:
在有些场合下,你通常要与一系列实现了同一接口,可以被同等对待的类打交道,这是js中使用工厂模式最常见的原因。
XHR 工厂实例:
用于发起请求的对象是某种类的实例,具体是哪种类取决于用户的浏览器。如果代码中需要多次执行Ajax请求,那么明智的做法就是把创建这种对象提取到一个类中,并创建一个包装器来包装在实际发起请求时所要经历的一系列步骤。简单工厂非常适合这种场合,它可以根据浏览器能力的不同生成一个XMLHttpRequest或 ActiveXObject 实例。
var AjaxHandler = new Interface('AjaxHandler',['request','createXhrObject']); var SimpleHandler = function(){}; SimpleHandler.prototype = { request:function(method,url,callback,postVars){ var xhr = this.createXhrObject(); xhr.onreadystatechange = function(){ if(xhr.readyState!==4) return; (xhr.status===200)? callback.success(xhr.responseText,xhr.responseXML): callback.failure(xhr.status); } xhr.open(method,url,true); if(method!=='POST') postVars=null; xhr.send(postVars); }, createXhrObject:function(){ var methods = [ function(){ return new XMLHttpRequest();}, function(){ return new ActiveXObject('Msxml2.XMLHTTP');}, function(){ return new ActiveXObject('Microsoft.XMLHTTP');} ]; for(var i=0,len=methods.length;i<len;i++){ try{ methods[i](); }catch(e){ continue; } //这里比较有意思,是一种记忆方式,代码执行一次之后,createXhrObject函数改变。 this.createXhrObject = methods[i]; return methods[i]; } throw new Error('SimpleHandler:could not create xhr object'); } }
上面这例子可以进一步扩展,把工厂模式用在俩个地方,以便根据网络条件创建专门的请求对象。在创建XHR对象的时候已经使用过了简单工厂模式。另一个工厂则用来返回各种处理器类,他们都派生自SimpleHandler。
首先要做的是创建俩个新的处理器类
QueueHandler 会在发起新请求之前确保所有的请求都成功处理。
OfflineHandler 则会在用户处于离线状态是把请求缓存起来。
var QueuedHandler = function(){ this.queue = []; this.requestInProgress = false; this.retryDelay = 5; } extend(QueuedHandler,SimpleHandler); QueuedHandler.prototype.request = function(method,url,callback,postVars,override){ //如果前面的请求还在处理,则直接把这些参数推入数组 if(this.requestInProgress && !override){ this.queue.push({ method:method, url:url, callback:callback, postVars:postVars }); }else{ //如果处理完成,则执行该操作 //把状态设置为处理中。。。。 //创建对象 this.requestInProgress = true; var xhr = this.createXhrObject(); var _this = this; xhr.onreadystatechange = function(){ if(xhr.readyState!==4) return; if(xhr.status===200){ //请求成功,执行callback函数 callback.success(xhr.responseText,xhr.responseXML); //继续处理队列中的请求 _this.advanceQueue(); }else{ //请求失败,则每隔5秒进行一次请求 callback.failure(xhr.status); setTimeout(function(){ _this.request(method,url,callback,postVars,override); },_this.retryDelay*1000); }; }; xhr.open(method,url,true); if(method!=='POST') postVars=null; xhr.send(postVars); } }; QueuedHandler.prototype.advanceQueue = function(){ if(this.queue.length===0){ this.requestInProgress = false; return; } var req = this.queue.shift(); this.request(req.method,req.url,req.callback,req.postVars,true); }
QueuedHandler 的 request 方法与SimpleHandler的看上去差不多,但是允许发起新的请求之前先检查一下,以确保当前没有别的请求正在处理。
OfflineHandler要更简单一点:
var OfflineHandler = function(){ this.storedRequests = []; } extend(OfflineHandler,SimpleHandler); OfflineHandler.prototype.request = function(method,url,callback,postVars){ if(xhrManager.isOffline()){ this.storedRequests.push({ method:method, url:url, callback:callback, postVars:postVars }); }else{ this.flushStoredRequests(); OfflineHandler.superclass.request(method,url,callback,postVars); }; OfflineHandler.prototype.flushStoredRequests = function(){ for(var i=0,len=storedRequests.length;i<len;i++){ var req = storedRequests[i]; OfflineHandler.superclass.request(req.method,req.url,req.callback,req.postVars); } } }
xhrManager.isOffline 方法的作用在于判断用户是否处于在线状态。
现在用到工厂模式了,因为程序员根本根本不肯知道各个最终用户实际面临的网络条件,所以不可能要求他们在开发过程中选择使用哪个处理器类,而是应该用一个工厂在运行时选择最合适的类。
var xhrManager = { createXhrHandler:function(){ var xhr; if (this.isOffline()) { xhr = new offlineHandler(); }else if(this.isHighLatency()){ xhr = new QueuedHandler(); }else{ xhr = new SimpleHandler(); }; Interface.ensureImplements(xhr,AjaxHandler); return xhr; }, isOffline:function(){}, isHighLatency:function(){} }
现在程序员就可以使用这个工厂方法,而不必实例化一个特定的类了:
var myHandler = xhrManager.createXhrHandler(); var callback = { success:function(responseText){}, failure:function(statusCode){} } myHandler.request('GET','script.php',callback);
示例:RSS阅读器:
RSS 阅读器对象,它的成员对象包括一个XHR处理器对象,一个显示对象,一个配置对象,XHR处理器类我们使用上面的xhrManager.createXhrHandler方法所创建的处理器对象,下面是一个显示类(显示对象):
var DisplayModule = new Interface('DisplayModule',['append','remove','clear']); var ListDisplay = function(id,parent){ this.list = document.createElement('ul'); this.list.id = id; parent.appendChild(this.list); } ListDisplay.prototype = { append:function(text){ var newEl = document.createElement('li'); this.list.appendChild(newEl); newEl.innerHTML = text; return newEl; }, remove:function(el){ this.list.removeChild(el); }, clear:function(){ this.list.innerHTML = ''; } }
下面是一个配置对象,这只是一个对象字面量,包含一些供阅读器以及成员对象使用的设置:
var conf = { id:'cnn-top-stories', feedUrl:'http://.....rss', updateInterval:60, parent:$('feed-readers') }
这些类由FeedReader组合使用。它使用XHR获取数据,并解析,最后显示模块将信息输出到网页:
var FeedReader = function(display,xhrHandler,conf){ this.display = display; this.xhrHandler = xhrHandler; this.conf = conf; this.startUpdates(); } FeedReader.prototype = { fetchFeed:function(){ var _this = this; var callback = { success:function(text,xml){ _this.parseFeed(text,xml); }, failure:function(status){ _this.showError(status); } }; this.xhrHandler.request('GET',this.conf.feedUrl,callback); }, parseFeed:function(responseText,responseXML){ this.display.clear(); var items = responseXML.getElementsByTagName('item'); for(var i= 0,len=items.length;i<len;i++){ var title = items[i].getElementsByTagName('title')[0]; var link = items[i].getElementsByTagName('link')[0]; this.display.append('<a href="'+link.firstChild.data+'">'+title.firstChild.data+'</a>'); } }, showError:function(status){ this.display.clear(); this.display.append('Error fetching feed.'); }, stopUpdates:function(){ clearInterval(this.interval); }, startUpdates:function(){ this.fetchFeed(); var _this = this; this.interval = setInterval(function(){_this.fetchFeed();}, this.conf.updateInterval*1000); } }
现在还差一个部分,即把所有这些类和对象拼装起来的那个工厂方法,他被实现为一个简单的工厂:
var FeedManager = { createFeedReader:function(conf){ var displayModule = new ListDisplay(conf.id+'-display',conf.parent); Interface.ensureImplements(displayModule,DisplayModule); var xhrHandler = xhrManager.createXhrHandler(); Interface.ensureImplements(xhrHandler,AjaxHandler); return new FeedReader(displayModule,xhrHandler,conf); } }
使用API的程序员当然可以手工创建一个FeedReader对象,而不必借助FeedManager.createFeedReader方法,但是使用这个工厂方法,可以把FeedReader类所需要的复杂设置封装取来,并且可以确保其成员对象都实现了所需接口。
总结:
工厂模式主要用于消除对象间的耦合。通过使用工厂方法,而不是new关键字以及具体类,你可以把所有实例化代码集中在一个位置。
使用工厂模式,你可以先创建一个抽象的父类,然后在子类中创建工厂方法,从而把成员对象的实例化推迟到更专门化的子类中进行。
有人不禁把工厂方法当做万金油,把构造函数扔在一边。这并不值得提倡。
如果根本不可能另外换用一个类,或者不需要在运行期间在一系列可互换的类中进行选择,那就不应该使用工厂方法。
它主要用于所实例化的类型不能在开发期确定,而只能在运行期间确定的情况,此外,如果存在许多具有复杂的设置开销的相关对象,或者想创建一个包含了一些成员对象的类但是又想避免它们紧密的偶合在一起的话,就应该使用工厂模式。