详解JavaScript的策略模式编程
我喜欢策略设计模式。我尽可能多的试着去使用它。究其本质,策略模式使用委托去解耦使用它们的算法类。
这样做有几个好处。他可以防止使用大条件语句来决定哪些算法用于特定类型的对象。将关注点分离开来,因此降低了客户端的复杂度,同时还可以促进子类化的组成。它提高了模块化和可测性。每一个算法都可以单独测试。每一个客户端都可以模拟算法。任意的客户端都能使用任何算法。他们可以互调。就像乐高积木一样。
为了实现策略模式,通常有两个参与者:
该策略的对象,封装了算法。
客户端(上下文)对象,以即插即用的方式能使用任何策略。
这里介绍了我在Javascrip里,怎样使用策略模式,在混乱无序的环境中怎样使用它将库拆成小插件,以及即插即用包的。
函数作为策略
一个函数提供了一种封装算法的绝佳方式,同时可以作为一种策略来使用。只需通过一个到客户端的函数并确保你的客户端能调用该策略。
我们用一个例子来证明。假设我们想创建一个Greeter类。它所要做的就是和人打招呼。我们希望Greeter类能知道跟人打招呼的不同方式。为了实现这一想法,我们为打招呼创建不同的策略。
//Greeterisaclassofobjectthatcangreetpeople.
//Itcanlearndifferentwaysofgreetingpeoplethrough
//'Strategies.'
//
//ThisistheGreeterconstructor.
varGreeter=function(strategy){
this.strategy=strategy;
};
//Greeterprovidesagreetfunctionthatisgoingto
//greetpeopleusingtheStrategypassedtotheconstructor.
Greeter.prototype.greet=function(){
returnthis.strategy();
};
//Sinceafunctionencapsulatesanalgorithm,itmakesaperfect
//candidateforaStrategy.
//
//HereareacoupleofStrategiestousewithourGreeter.
varpoliteGreetingStrategy=function(){
console.log("Hello.");
};
varfriendlyGreetingStrategy=function(){
console.log("Hey!");
};
varboredGreetingStrategy=function(){
console.log("sup.");
};
//Let'susethesestrategies!
varpoliteGreeter=newGreeter(politeGreetingStrategy);
varfriendlyGreeter=newGreeter(friendlyGreetingStrategy);
varboredGreeter=newGreeter(boredGreetingStrategy);
console.log(politeGreeter.greet());//=>Hello.
console.log(friendlyGreeter.greet());//=>Hey!
console.log(boredGreeter.greet());//=>sup.
在上面的例子中,Greeter是客户端,并有三种策略。正如你所看到的,Greeter知道怎样使用算法,但对于算法的细节却一无所知。
对于复杂的算法,一个简单的函数往往不能满足。在这种情况下,对好的方式就是按照对象来定义。
类作为策略
策略同样可以是类,特别是当算比上述例子中使用的人为的(策略/算法)更复杂的时候。使用类的话,允许你为每一种策略定义一个接口。
在下面的例子中,证实了这一点。
//WecanalsoleveragethepowerofPrototypesinJavascripttocreate
//classesthatactasstrategies.
//
//Here,wecreateanabstractclassthatwillserveastheinterface
//forallourstrategies.Itisn'tneeded,butit'sgoodfordocumenting
//purposes.
varStrategy=function(){};
Strategy.prototype.execute=function(){
thrownewError('Strategy#executeneedstobeoverridden.')
};
//Likeabove,wewanttocreateGreetingstrategies.Let'ssubclass
//ourStrategyclasstodefinethem.Noticethattheparentclass
//requiresitschildrentooverridetheexecutemethod.
varGreetingStrategy=function(){};
GreetingStrategy.prototype=Object.create(Strategy.prototype);
//Hereisthe`execute`method,whichispartofthepublicinterfaceof
//ourStrategy-basedobjects.NoticehowIimplementedthismethodintermof
//ofothermethods.ThispatterniscalledaTemplateMethod,andyou'llsee
//thebenefitslateron.
GreetingStrategy.prototype.execute=function(){
returnthis.sayHi()+this.sayBye();
};
GreetingStrategy.prototype.sayHi=function(){
return"Hello,";
};
GreetingStrategy.prototype.sayBye=function(){
return"Goodbye.";
};
//WecanalreadytryoutourStrategy.Itrequiresalittletweakinthe
//Greeterclassbefore,though.
Greeter.prototype.greet=function(){
returnthis.strategy.execute();
};
vargreeter=newGreeter(newGreetingStrategy());
greeter.greet()//=>'Hello,Goodbye.'
通过使用类,我们与anexecutemethod对象定义了一个策略。客户端可以使用任何策略实现该接口。
同样注意我又是怎样创建GreetingStrategy的。有趣的部分是对methodexecute的重载。它以其他函数的形式定义。现在类的后继子类可以改变特定的行为,如thesayHiorsayByemethod,并不改变常规的算法。这种模式叫做模板方法,非常适合策略模式。
让我们看个究竟。
//SincetheGreetingStrategy#executemethodusesmethodstodefineitsalgorithm,
//theTemplateMethodpattern,wecansubclassitandsimplyoverrideoneofthose
//methodstoalterthebehaviorwithoutchangingthealgorithm.
varPoliteGreetingStrategy=function(){};
PoliteGreetingStrategy.prototype=Object.create(GreetingStrategy.prototype);
PoliteGreetingStrategy.prototype.sayHi=function(){
return"Welcomesir,";
};
varFriendlyGreetingStrategy=function(){};
FriendlyGreetingStrategy.prototype=Object.create(GreetingStrategy.prototype);
FriendlyGreetingStrategy.prototype.sayHi=function(){
return"Hey,";
};
varBoredGreetingStrategy=function(){};
BoredGreetingStrategy.prototype=Object.create(GreetingStrategy.prototype);
BoredGreetingStrategy.prototype.sayHi=function(){
return"sup,";
};
varpoliteGreeter=newGreeter(newPoliteGreetingStrategy());
varfriendlyGreeter=newGreeter(newFriendlyGreetingStrategy());
varboredGreeter=newGreeter(newBoredGreetingStrategy());
politeGreeter.greet();//=>'Welcomesir,Goodbye.'
friendlyGreeter.greet();//=>'Hey,Goodbye.'
boredGreeter.greet();//=>'sup,Goodbye.'
GreetingStrategy通过指定theexecutemethod的步骤,创建了一个类的算法。在上面的代码片段中,我们通过创建专门的算法从而利用了这一点。
没有使用子类,我们的Greeter依然展示出一种多态行为。没有必要在Greeter的不同类型上进行切换来触发正确的算法。这一切都绑定到每一个Greeter对象上。
vargreeters=[
newGreeter(newBoredGreetingStrategy()),
newGreeter(newPoliteGreetingStrategy()),
newGreeter(newFriendlyGreetingStrategy()),
];
greeters.forEach(function(greeter){
//Sinceeachgreeterknowsitsstrategy,there'snoneed
//todoanytypechecking.Wejustgreet,andtheobject
//knowshowtohandleit.
greeter.greet();
});
多环境下的策略模式
我最喜欢的有关策略模式的例子之一,实在Passport.js库中。Passport.js提供了一种在Node中处理身份验证的简单方式。大范围内的供应商都支持(Facebook,Twitter,Google等等),每一个都作为一种策略实现。
该库作为一个npm包是可行的,其所有的策略也一样。库的用户可以决定为他们特有的用例安装哪一个npm包。下面是展示其如何实现的代码片段:
//Takenfromhttp://passportjs.org
varpassport=require('passport')
//Eachauthenticationmechanismisprovidedasannpmpackage.
//ThesepackagesexposeaStrategyobject.
,LocalStrategy=require('passport-local').Strategy
,FacebookStrategy=require('passport-facebook').Strategy;
//PassportcanbeinstanciatedusinganyStrategy.
passport.use(newLocalStrategy(
function(username,password,done){
User.findOne({username:username},function(err,user){
if(err){returndone(err);}
if(!user){
returndone(null,false,{message:'Incorrectusername.'});
}
if(!user.validPassword(password)){
returndone(null,false,{message:'Incorrectpassword.'});
}
returndone(null,user);
});
}
));
//Inthiscase,weinstanciateaFacebookStrategy
passport.use(newFacebookStrategy({
clientID:FACEBOOK_APP_ID,
clientSecret:FACEBOOK_APP_SECRET,
callbackURL:"http://www.example.com/auth/facebook/callback"
},
function(accessToken,refreshToken,profile,done){
User.findOrCreate(...,function(err,user){
if(err){returndone(err);}
done(null,user);
});
}
));
Passport.js库只配备了一两个简单的身份验证机制。除此之外,它没有超过一个符合上下文对象的一个策略类的接口。这种机制让他的使用者,很容易的实现他们自己的身份验证机制,而对项目不产生不利的影响。
反思
策略模式为你的代码提供了一种增加模块化和可测性的方式。这并不意味着(策略模式)总是有效。Mixins同样可以被用来进行功能性注入,如在运行时的一个对象的算法。扁平的老式duck-typing多态有时候也可以足够简单。
然而,使用策略模式允许你在一开始没有引入大型体系的情况下,随着负载型的增长,扩大你的代码的规模。正如我们在Passport.js例子中看到的一样,对于维护人员在将来增加另外的策略,将变得更加方便。