详解JavaScript的策略模式编程

作者:纸飞机

时间:2026-10-29

浏览:511

本文深入解析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示例中看到的那样,这使得维护人员在将来增加其他策略变得更加方便。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章