本文深入解析JavaScript中的策略设计模式,阐述如何通过委托解耦算法与客户端。内容涵盖以函数和类作为策略的实现方式,结合Greeter示例演示模板方法的应用,并分析Passport.js库中利用策略模式实现即插即用身份验证机制的最佳实践,旨在提升代码的模块化、可测性与扩展性。
策略模式概述
我非常推崇策略设计模式,并尽可能在实际开发中广泛使用它。从本质上看,策略模式利用委托机制,有效解耦了调用算法的类与具体算法实现。
这种设计带来了多重优势。它避免了使用冗长的条件语句来为特定对象选择算法,实现了关注点的分离。这不仅降低了客户端的复杂度,还促进了通过组合而非继承来实现子类化。此外,它显著提升了模块化和可测试性:每个算法都可以独立测试,客户端可以模拟任意算法,且不同算法之间可以相互调用,如同乐高积木般灵活拼接。
要实现策略模式,通常涉及两个核心参与者:
策略对象,负责封装具体的算法逻辑。
客户端(或称上下文)对象,能够以即插即用方式使用任何符合接口的策略。
接下来,我将介绍在JavaScript中如何应用策略模式,展示如何在复杂环境中将库拆分为小型插件,并实现即插即用的功能包。
函数作为策略
函数作为策略
函数是封装算法并作为策略使用的绝佳方式。只需向客户端传递一个函数,并确保客户端能够调用该策略即可。
我们通过一个示例来验证。假设我们要创建一个Greeter类,其功能是与人打招呼。我们希望Greeter类能掌握多种打招呼的方式。为此,我们为不同的打招呼方式创建相应的策略。
// Greeter is a class of object that can greet people.
// It can learn different ways of greeting people through
// 'Strategies.'
//
// This is the Greeter constructor.
var Greeter = function(strategy) {
this.strategy = strategy;
};
// Greeter provides a greet function that is going to
// greet people using the Strategy passed to the constructor.
Greeter.prototype.greet = function() {
return this.strategy();
};
// Since a function encapsulates an algorithm, it makes a perfect
// candidate for a Strategy.
//
// Here are a couple of Strategies to use with our Greeter.
var politeGreetingStrategy = function() {
console.log("Hello.");
};
var friendlyGreetingStrategy = function() {
console.log("Hey!");
};
var boredGreetingStrategy = function() {
console.log("sup.");
};
// Let's use these strategies!
var politeGreeter = new Greeter(politeGreetingStrategy);
var friendlyGreeter = new Greeter(friendlyGreetingStrategy);
var boredGreeter = new Greeter(boredGreetingStrategy);
console.log(politeGreeter.greet()); //=> Hello.
console.log(friendlyGreeter.greet()); //=> Hey!
console.log(boredGreeter.greet()); //=> sup.
在上述示例中,Greeter充当客户端角色,并拥有三种不同的策略。可以看到,Greeter知道如何调用算法,但并不了解算法的具体细节。
对于复杂的算法,简单的函数往往难以胜任。在这种情况下,通过对象来定义策略是更好的选择。
类作为策略与模板方法
类作为策略
策略同样可以是类,特别是当算法比上述示例中的人为(策略/算法)更复杂时。使用类允许我们为每种策略定义一个接口。
// We can also leverage the power of Prototypes in Javascript to create
// classes that act as strategies.
//
// Here, we create an abstract class that will serve as the interface
// for all our strategies. It isn't needed, but it's good for documenting
// purposes.
var Strategy = function() {};
Strategy.prototype.execute = function() {
throw new Error('Strategy#execute needs to be overridden.')
};
// Like above, we want to create Greeting strategies. Let's subclass
// our Strategy class to define them. Notice that the parent class
// requires its children to override the execute method.
var GreetingStrategy = function() {};
GreetingStrategy.prototype = Object.create(Strategy.prototype);
// Here is the `execute` method, which is part of the public interface of
// our Strategy-based objects. Notice how I implemented this method in term of
// of other methods. This pattern is called a Template Method, and you'll see
// the benefits later on.
GreetingStrategy.prototype.execute = function() {
return this.sayHi() + this.sayBye();
};
GreetingStrategy.prototype.sayHi = function() {
return "Hello, ";
};
GreetingStrategy.prototype.sayBye = function() {
return "Goodbye.";
};
// We can already try out our Strategy. It requires a little tweak in the
// Greeter class before, though.
Greeter.prototype.greet = function() {
return this.strategy.execute();
};
var greeter = new Greeter(new GreetingStrategy());
greeter.greet() //=> 'Hello, Goodbye.'
下面的示例证实了这一点。
通过使用类,我们定义了一个包含execute方法的策略对象。客户端可以使用任何实现了该接口的策略。同时,请注意GreetingStrategy的创建方式。有趣的部分在于对execute方法的重载,它以其他函数的形式定义。现在,类的后续子类可以改变特定行为(如sayHi或sayBye方法),而不改变常规算法。这种模式称为模板方法,非常适合与策略模式结合使用。
让我们深入探讨一下。
// Since the GreetingStrategy#execute method uses methods to define its algorithm,
// the Template Method pattern, we can subclass it and simply override one of those
// methods to alter the behavior without changing the algorithm.
var PoliteGreetingStrategy = function() {};
PoliteGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
PoliteGreetingStrategy.prototype.sayHi = function() {
return "Welcome sir, ";
};
var FriendlyGreetingStrategy = function() {};
FriendlyGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
FriendlyGreetingStrategy.prototype.sayHi = function() {
return "Hey, ";
};
var BoredGreetingStrategy = function() {};
BoredGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
BoredGreetingStrategy.prototype.sayHi = function() {
return "sup, ";
};
var politeGreeter = new Greeter(new PoliteGreetingStrategy());
var friendlyGreeter = new Greeter(new FriendlyGreetingStrategy());
var boredGreeter = new Greeter(new BoredGreetingStrategy());
politeGreeter.greet(); //=> 'Welcome sir, Goodbye.'
friendlyGreeter.greet(); //=> 'Hey, Goodbye.'
boredGreeter.greet(); //=> 'sup, Goodbye.'
GreetingStrategy通过指定execute方法的步骤来创建类的算法。在上面的代码片段中,我们通过创建专门的算法利用了这一点。
即使不使用子类,我们的Greeter依然表现出多态行为。无需在Greeter的不同类型之间进行切换来触发正确的算法。这一切都绑定在每一个Greeter对象上。
var greeters = [
new Greeter(new BoredGreetingStrategy()),
new Greeter(new PoliteGreetingStrategy()),
new Greeter(new FriendlyGreetingStrategy()),
];
greeters.forEach(function(greeter) {
// Since each greeter knows its strategy, there's no need
// to do any type checking. We just greet, and the object
// knows how to handle it.
greeter.greet();
});
多环境下的策略模式应用
多环境下的策略模式
我最喜欢的策略模式示例之一是Passport.js库。Passport.js提供了一种在Node.js中处理身份验证的简便方式。它支持广泛的服务提供商(如Facebook、Twitter、Google等),每个提供商都作为一种策略实现。
该库作为一个npm包存在,其所有策略也各自独立为npm包。库的用户可以根据特定用例决定安装哪个npm包。以下是展示其实现方式的代码片段:
// Taken from http://passportjs.org
var passport = require('passport')
// Each authentication mechanism is provided as an npm package.
// These packages expose a Strategy object.
, LocalStrategy = require('passport-local').Strategy
, FacebookStrategy = require('passport-facebook').Strategy;
// Passport can be instanciated using any Strategy.
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
// In this case, we instanciate a Facebook Strategy
passport.use(new FacebookStrategy({
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) { return done(err); }
done(null, user);
});
}
));
Passport.js库仅配备了一两个简单的身份验证机制。除此之外,它没有超过一个符合上下文对象的一个策略类的接口。这种机制让用户很容易地实现自己的身份验证机制,而不对项目产生不利影响。
策略模式反思与总结
策略模式为你的代码提供了一种增加模块化和可测性的方式。这并不意味着(策略模式)总是有效。Mixins 同样可以被用来进行功能性注入,如在运行时的一个对象的算法。扁平的老式 duck-typing多态有时候也可以足够简单。
策略模式为你的代码提供了一种增加模块化和可测性的方式。但这并不意味着(策略模式)总是有效。Mixins同样可用于进行功能性注入,例如在运行时向对象注入算法。扁平的老式duck-typing多态有时也可以足够简单。然而,使用策略模式允许你在不引入大型体系结构的情况下,随着负载增长而扩大代码规模。正如我们在Passport.js示例中看到的那样,这使得维护人员在将来增加其他策略变得更加方便。