背景
- デザインパターンを学んでいた
- StrategyパターンとStateパターンとTemplateパターンの違いが明確にわからなかった
- 整理のためにまとめていく
Strategyパターン
コンテキストと呼ばれるアルゴリズムの共通部分と、Strategyと呼ばれる可変する処理を抜き出した部分からなるデザインパターンです。
- アルゴリズムのバリエーションを増やしたいけど単純に条件分岐でバリエーション増やそうとしたらロジックが複雑になってしまう場合
- 一連の処理の中で状況に応じて異なるアルゴリズムを使いたい場合
などに特に有効と言われています。
具体例
多数あるフォーマットのserialize, deserializeの処理にStrategyパターンを利用します。
実現したいことは
- ファイルの読み込み
- ファイルの保存
- ファイルのserialize, deserialize(フォーマットによって可変)
の三つです。
ファイルのserialize, deserialize
はjsonやiniなどファイルによってデータ形式が違うので、それぞれにあったserialize, deserializeの処理が必要である。今回はこの部分をStrategyとして実装を行います。
まず共通部分(コンテキスト)のロジックは
import fs from 'fs';
import objectPath from 'object-path';
export interface Strategy {
deserialize: (path: string) => void;
serialize: (data: unknown) => string | NodeJS.ArrayBufferView;
}
export class Config {
strategy: Strategy;
data: any;
constructor(strategy: Strategy) {
this.data = {};
this.strategy = strategy;
}
get(path: string) {
return objectPath.get(this.data, path);
}
set(path: string, value: unknown) {
return objectPath.set(this.data, path, value);
}
read(file: string) {
console.log(`Deserializing from ${file}`);
this.data = this.strategy.deserialize(fs.readFileSync(file, 'utf-8'));
}
save(file: string) {
console.log(`Serializing to ${file}`);
fs.writeFileSync(file, this.strategy.serialize(this.data));
this.data = this.strategy.deserialize(fs.readFileSync(file, 'utf-8'));
}
}
ここで行っていることは
- クラスの初期化時に、Strategyを受け取るようにしている
- get, set, read, saveといった関数を用意している
- read, saveの処理実行時には、Strategyのserialize, deserializeを呼ぶことでStrategyに合わせた処理を行う
です。次はStrategyの実装です。
import iniModule from 'ini';
import { Strategy } from './config';
const json: Strategy = {
deserialize: (data) => JSON.parse(data),
serialize: (data) => JSON.stringify(data, null, ' '),
};
const ini: Strategy = {
deserialize: (data) => iniModule.parse(data),
serialize: (data) => iniModule.stringify(data),
};
export { json, ini };
ここでは、read, saveで呼ばれることを想定して、serialize, deserialize の処理を定義しています。当然、ファイルフォーマット毎に処理内容は違います。
では最後に、これらを使用するファイルを作成します。
import { Config } from './config';
import * as strategies from './strategies';
const jsonConfig = new Config(strategies.json);
jsonConfig.read('./conf.json');
jsonConfig.set(
'book.nodejs',
'design patterns'
);
jsonConfig.save('./conf_mode.json');
const iniConfig = new Config(strategies.ini);
iniConfig.read('./conf.ini');
iniConfig.set(
'book.nodejs',
'design patterns'
);
iniConfig.save('./conf_mode.ini');
上記の通り、ファイルのフォーマットが違くてもStrategyパターンを使えば、条件分岐のロジックを書かなくてもとてもシンプルにserialize, deserializeの処理が書けることがわかったかと思います。
また拡張性という面でも、これから他に対応させたいデータ形式があれば、それに応じたStrategyを strategies.ts
に定義して、あとはコンテキストに適用してあげるのみです。
Node.jsの認証用フレームワークである Passport.js はこのStrategyパターンを使用しています。
「Twitterでログイン」や「Facebookでログイン」といったStrategyがたくさんサポートされていることがわかるかと思います。
いかに強力なパターンか、ということが伝わっていれば幸いです。
Stateパターン
これはStrategyパターンの変種です。
Strategyパターンは、一度Strategyを受け取ったら、そのインスタンスが消滅するまでずっと同じStrategyです。
Stateパターンは、動的なStrategyパターン。インスタンスが消滅しなくても、内部の状態(State)を変えることで振る舞いを変更することができます。
具体例
こちらのパターンに関しては、以下の記事がとてもわかりやすかったので共有いたします。
Templateパターン
アルゴリズムの骨格となる抽象擬似クラスとそれを継承した具象クラスを定義するデザインパターン。
この抽象擬似クラスを継承したサブクラスは、Templateメソッドと呼ばれるかけたステップを実装することでアルゴリズムの欠陥部を埋めます。
つまり、抽象擬似クラスだけでは動きません。サブクラスがあって初めて動作するものとなります。(Strategyパターンとの違いはここです)
図を以下に示します。
ある程度共通の処理が存在して、さらにそのサブクラス毎に関数を加えていきたい時にTemplateパターンは有効であると言われています。
具体例
先ほどの実装をTemplateパターンで実装していきます。まずは抽象擬似クラスからです。
import fs, { PathOrFileDescriptor } from 'fs';
import objectPath from 'object-path';
export abstract class ConfigTemplate {
data!: object;
read(file: PathOrFileDescriptor) {
console.log(`Deserializing form ${file}`);
this.data = this._deserialize(fs.readFileSync(file, 'utf-8'));
}
save(file: PathOrFileDescriptor) {
console.log(`Serializing form ${file}`);
fs.writeFileSync(file, this._serialize(this.data));
}
get(path: string) {
return objectPath.get(this.data, path);
}
set(path: string, value: unknown) {
return objectPath.set(this.data, path, value);
}
abstract _serialize(data: any): string | NodeJS.ArrayBufferView;
abstract _deserialize(path: string): object;
}
_serializeと_deserializeに注目してください。ここの実装の中身は具象クラスに必ず定義するために抽象メソッドとして追加します。こうすることで
抽象擬似クラスだけでは動きません
を表現しています。
それではサブクラスを定義していきます。jsonを例に使用します。
import { ConfigTemplate } from './configTemplate';
export class JsonConfig extends ConfigTemplate {
_serialize(data: any): string | NodeJS.ArrayBufferView {
return JSON.stringify(data, null, ' ');
}
_deserialize(path: string): object {
return JSON.parse(path);
}
}
サブクラスでは先ほど定義した抽象擬似クラスの ConfigTemplate
を継承しています。そして、 ConfigTemplate
ではエラーを返していた_serialize, _deserializeで正しく動作するように処理を記述しています。
では、最後に実行するファイルを定義していきます。
import { JsonConfig } from './jsonConfig';
const jsonConfig = new JsonConfig();
jsonConfig.read('./conf.json');
jsonConfig.set('nodejs', 'design patterns');
jsonConfig.save('./conf_mod.json');
ここでは JsonConfig
を呼び出して、処理を行っています。
Templateパターンはご覧のように、共通ロジックは抽象擬似クラスに、固有のロジックを具象クラスに定義することで完全に動作するパターンとなります。
このパターンを使えば、具象クラスは最小限の手間で完全に動作するモジュールとして成り立ちます。
まとめ
- Strategyパターンは、コンテキストと呼ばれるオブジェクトに、可変部分のロジックを抜き出したStrategyを提供することで、ロジックの変更を容易に実現する
- Strategyパターンは、Passport.jsに使用されている
- Stateパターンは、動的なStrategyパターンである(インスタンスが消滅しなくてもStrategyを変更して振る舞いを変えることができる)
- Templateパターンは、抽象擬似クラスとそれを継承した具象クラスを実装することで初めて動作する