戦略のデザインパターンが好きです。できるだけ使うようにしています。 Strategy パターンの中核では、デリゲートを使用して、デリゲートを使用するアルゴリズム クラスを分離します。
これを行うことにはいくつかの利点があります。これにより、特定の種類のオブジェクトにどのアルゴリズムを使用するかを決定するために大規模な条件ステートメントを使用することがなくなります。したがって、関心事を分離すると、クライアントの複雑さが軽減されると同時に、サブクラス化も容易になります。モジュール性とテスト容易性が向上します。各アルゴリズムは個別にテストできます。すべてのクライアントはアルゴリズムをシミュレートできます。どのクライアントでも任意のアルゴリズムを使用できます。それらは相互変調することができます。まるでレゴのようだ。
戦略パターンを実装するには、通常 2 人の参加者が必要です:
この戦略のオブジェクトはアルゴリズムをカプセル化します。
プラグアンドプレイ方式で任意の戦略を使用できるクライアント (コンテキスト) オブジェクト。
ここでは、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 がクライアントであり、3 つの戦略があります。ご覧のとおり、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.'
クラスを使用して、executemethod オブジェクトでストラテジを定義します。クライアントは、任意の戦略を使用してこのインターフェイスを実装できます。
GreetingStrategy をどのように作成したかにも注目してください。興味深い部分は、methodexecute のオーバーロードです。他の関数の形式で定義されます。クラスの後続のサブクラスは、一般的なアルゴリズムを変更せずに、sayHiorsayByemethod などの特定の動作を変更できるようになりました。このパターンはテンプレート手法と呼ばれ、戦略パターンに非常に適しています。
何が起こるか見てみましょう。
// 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(); });
複数環境での戦略モード
戦略パターンの私のお気に入りの例の 1 つは、Passport.js ライブラリにあります。 Passport.js は、Node で認証を処理する簡単な方法を提供します。幅広いプロバイダー (Facebook、Twitter、Google など) がサポートしており、それぞれがポリシーとして実装されています。
ライブラリは、そのすべての戦略と同様に、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 ライブラリには、1 つまたは 2 つの単純な認証メカニズムのみが付属しています。それ以外には、コンテキスト オブジェクトに準拠するポリシー クラスを超えるインターフェイスはありません。このメカニズムにより、ユーザーはプロジェクトに悪影響を与えることなく、独自の認証メカニズムを簡単に実装できます。
反省
Strategy パターンは、コードのモジュール性とテスト容易性を高める方法を提供します。これは、(戦略パターン)が常に機能するという意味ではありません。ミックスインを使用して、実行時にアルゴリズムなどの機能をオブジェクトに挿入することもできます。フラットで古いダックタイピングの多態性は、場合によっては十分に単純である場合があります。
ただし、Strategy パターンを使用すると、最初から大規模なアーキテクチャを導入することなく、ワークロードの増大に応じてコードを拡張できます。 Passport.js の例で見たように、将来的にはメンテナーが戦略を追加することが容易になるでしょう。