メインコンテンツまでスキップ

Strategy — 関数が第一級であること

関数で足りる場合と、オブジェクトとして束ねる価値がある場合を分けます。

このパターンが解こうとした問題

GoF は Strategy を、一群のアルゴリズムから 1 つを実行時に選べるようにするパターンとして収録しています1

ifswitch でアルゴリズムを選ぶコードは、種類が増えるたびに同じ場所を書き換えることになります。Strategy はアルゴリズムを別々のオブジェクトに切り出し、選択を「どのオブジェクトを渡すか」に変えます。

これも言語側の前提が効いているパターンです。アルゴリズムそのものを値として渡せない言語では、アルゴリズムを持ち回るための入れ物が要り、その入れ物がクラスでした。

クラスで素直に書く

ファイルの圧縮方式の切り替えを題材にします。

interface CompressionCodec {
compress(input: Uint8Array): Uint8Array;
}

class GzipCodec implements CompressionCodec {
compress(input: Uint8Array): Uint8Array {
return input; // 圧縮の中身は省略
}
}

class BrotliCodec implements CompressionCodec {
compress(input: Uint8Array): Uint8Array {
return input; // 圧縮の中身は省略
}
}

class AssetWriter {
constructor(private readonly codec: CompressionCodec) {}

write(input: Uint8Array): Uint8Array {
return this.codec.compress(input);
}
}

const writer = new AssetWriter(new BrotliCodec());

AssetWriter は圧縮方式を知りません。方式を足すときも AssetWriter には触れません。設計としては正しく動いています。

ただし GzipCodec を見ると、クラスの中身は compress 1 つだけです。状態を持たないクラスは、関数を 1 つ包んだだけの入れ物になっています。

言語機能で置き換える

TypeScript では関数そのものを値として渡せます。ハンドブックはこう書いています。

Functions are the basic building block of any application, whether they're local functions, imported from another module, or methods on a class. They're also values, and just like other values, TypeScript has many ways to describe how functions can be called.2

関数の型は矢印で書けます。

type Compress = (input: Uint8Array) => Uint8Array;

const gzip: Compress = (input) => input; // 圧縮の中身は省略
const brotli: Compress = (input) => input; // 圧縮の中身は省略

function writeAsset(input: Uint8Array, compress: Compress): Uint8Array {
return compress(input);
}

const output = writeAsset(new Uint8Array([1, 2, 3]), brotli);

interface も 2 つのクラスもなくなり、残ったのは型 1 つと関数 2 つです。差し替えられるという性質は保たれています。

writeAsset の引数を見れば、この関数が圧縮方式を外から受け取ることがわかります。クラスの版では、それを知るにはコンストラクタを見る必要がありました。依存が呼び出しの形に現れるのは、関数に寄せたときの副産物です。

戦略が状態や複数の操作を持つとき

圧縮と展開が対で必要なら、話が変わります。圧縮と展開を別々の関数として持ち回ると、対応しない組み合わせを渡せてしまいます。1 つの値にまとめておけば、その値を渡すかぎり対応が保たれます。

type Codec = {
compress: (input: Uint8Array) => Uint8Array;
decompress: (input: Uint8Array) => Uint8Array;
contentEncoding: string;
};

const gzipCodec: Codec = {
compress: (input) => input,
decompress: (input) => input,
contentEncoding: 'gzip',
};

function writeAsset(input: Uint8Array, codec: Codec): {body: Uint8Array; encoding: string} {
return {body: codec.compress(input), encoding: codec.contentEncoding};
}

// 保存したものを読み戻す。圧縮と展開に同じ codec が使われることが型で決まる
function readAsset(body: Uint8Array, codec: Codec): Uint8Array {
return codec.decompress(body);
}

const asset = writeAsset(new Uint8Array([1, 2, 3]), gzipCodec);
const restored = readAsset(asset.body, gzipCodec);

まとまりをオブジェクトにすると、圧縮と展開が別々の方式になる書き方が現れません。ここで使っているのは素のオブジェクトで、クラスではありません。

本連載の判断

判定: 条件付き。束ねる必要があるかで決まります。

  • 操作が 1 つで状態を持たないなら関数 — 引数として渡すだけです。型は関数型 1 つ、実装は関数です。interface もクラスも書きません。
  • 操作が複数あり、それらが揃っている必要があるならオブジェクト — 上の Codec の形です。まとまりの整合を型が保証します。
  • 戦略自身が状態を持つならクロージャかクラス — 呼び出しをまたいで持ち回るもの (接続、統計の蓄積、内部バッファ) がある場合です。状態を閉じ込めた関数を返すファクトリ関数でも表せるので、クラスは選択肢の 1 つにすぎません。

3 つのどれでも、AssetWriter のような受け取る側は変わりません。選択肢は戦略の側の形だけです。

「クラスにするか関数にするか」ではなく、**「束ねる必要があるか」「持ち回る状態があるか」**の 2 つを順に見るのが、判断の道筋として扱いやすくなります。

この判定が覆る条件

関数に寄せると、戦略に名前が付かないことがありますwriteAsset(input, (x) => x) のように無名関数を直接書けてしまうので、ログやエラーメッセージで「どの戦略だったか」を出せません。戦略の識別が運用上必要なら、名前を持つオブジェクトに寄せる理由になります。

もう 1 つ、戦略の集合を設定ファイルや外部入力から選ぶ場合は、文字列から戦略への対応表が要ります。この対応表は Factory Method で扱った Record の形になり、Strategy と Factory の議論が合流します。

既存記事との関係

本サイトには Strategy に相当する実例が複数ありますが、いずれもパターン名を出していません。本章はそれらと同じ構図を、パターンの側から扱っています。既存記事とは題材を変えてあります。

  • SOLID 原則 の開放閉鎖の原則の例が、文字列による分岐をインターフェースと複数実装に置き換える構図です。原則の説明が主題で、パターン名は出てきません。
  • クラスとインターフェース にも、振る舞いをコンストラクタで注入する例があります。
  • 条件分岐 が、switch をオブジェクトの対応表に置き換える書き方を扱っています。本章の「文字列から戦略を引く」話は、そこで扱われている手法と同じ形です。

原則としての「なぜそうするか」は上記の記事が扱い、本章は「TypeScript でどの形を選ぶか」を扱う、という分担になります。

Footnotes

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides『Design Patterns: Elements of Reusable Object-Oriented Software』(Addison-Wesley, 1994)。Strategy は振る舞いに関するパターンとして収録されています。意図の要約は本ガイドによるもので、原典とは照合していません。

  2. 出典: More on Functions(TypeScript Handbook)の Function Type Expressions。