Strategy(別名 Policy)
説明
Strategy デザインパターンは、 関心の分離を可能にする手法です。また、 依存関係逆転を通じて ソフトウェアモジュールを疎結合にすることもできます。
Strategy パターンの基本的な考え方は、特定の問題を解決するアルゴリズムがある場合に、 アルゴリズムの骨組みだけを抽象レベルで定義し、具体的なアルゴリズムの実装を 異なる部分に分離するというものです。
このようにすることで、アルゴリズムを使用するクライアントは特定の実装を選択でき、 一方で一般的なアルゴリズムのワークフローは同じままになります。言い換えると、 クラスの抽象仕様は派生クラスの具体的な実装に依存しませんが、具体的な実装は 抽象仕様に従わなければなりません。これが「依存関係逆転」と呼ばれる理由です。
動機
毎月レポートを生成するプロジェクトに取り組んでいると想像してください。レポートは、
たとえば JSON や Plain Text 形式など、異なる形式(戦略)で生成する必要があります。
しかし状況は時間とともに変化し、将来どのような要件が出てくるかはわかりません。
たとえば、まったく新しい形式でレポートを生成する必要があるかもしれませんし、
既存の形式のいずれかを変更するだけでよいかもしれません。
例
この例では、不変要素(または抽象化)は Formatter と Report であり、
Text と Json が戦略構造体です。これらの戦略は Formatter トレイトを
実装しなければなりません。
use std::collections::HashMap;
type Data = HashMap<String, u32>;
trait Formatter {
fn format(&self, data: &Data, buf: &mut String);
}
struct Report;
impl Report {
// Write を使用すべきですが、エラー処理を無視するために String のままにしています
fn generate<T: Formatter>(g: T, s: &mut String) {
// バックエンド操作...
let mut data = HashMap::new();
data.insert("one".to_string(), 1);
data.insert("two".to_string(), 2);
// レポートを生成
g.format(&data, s);
}
}
struct Text;
impl Formatter for Text {
fn format(&self, data: &Data, buf: &mut String) {
for (k, v) in data {
let entry = format!("{k} {v}\n");
buf.push_str(&entry);
}
}
}
struct Json;
impl Formatter for Json {
fn format(&self, data: &Data, buf: &mut String) {
buf.push('[');
for (k, v) in data.into_iter() {
let entry = format!(r#"{{"{}":"{}"}}"#, k, v);
buf.push_str(&entry);
buf.push(',');
}
if !data.is_empty() {
buf.pop(); // 末尾の余分な , を削除
}
buf.push(']');
}
}
fn main() {
let mut s = String::from("");
Report::generate(Text, &mut s);
assert!(s.contains("one 1"));
assert!(s.contains("two 2"));
s.clear(); // 同じバッファを再利用
Report::generate(Json, &mut s);
assert!(s.contains(r#"{"one":"1"}"#));
assert!(s.contains(r#"{"two":"2"}"#));
}
利点
主な利点は関心の分離です。たとえば、この場合 Report は Json と Text の
具体的な実装について何も知りません。一方で出力の実装は、データがどのように
前処理され、保存され、取得されるかを気にしません。それらが知る必要があるのは、
実装すべき特定のトレイトと、結果を処理する具体的なアルゴリズム実装を定義する
そのメソッド、すなわち Formatter と format(...) だけです。
欠点
各戦略について少なくとも 1 つのモジュールを実装する必要があるため、戦略の数に 応じてモジュール数が増加します。選択可能な戦略が多数ある場合、ユーザーは 各戦略が互いにどのように異なるかを知っている必要があります。
議論
前の例では、すべての戦略が 1 つのファイルに実装されています。異なる戦略を 提供する方法には、次のようなものがあります。
- すべてを 1 つのファイルに入れる(この例で示したように、モジュールとして 分離するのに似ています)
- モジュールとして分離する。例:
formatter::jsonモジュール、formatter::textモジュール - コンパイラの機能フラグを使用する。例:
json機能、text機能 - クレートとして分離する。例:
jsonクレート、textクレート
Serde クレートは、Strategy パターンが実際に使われている良い例です。Serde では、
自分たちの型に対して Serialize と Deserialize トレイトを手動で実装することで、
シリアライズ動作を完全にカスタマイズできます。
たとえば、serde_json と serde_cbor は似たメソッドを公開しているため、
簡単に置き換えることができます。これにより、ヘルパークレートである
serde_transcode ははるかに便利で使いやすくなります。
ただし、Rust でこのパターンを設計するためにトレイトを使用する必要はありません。
次のおもちゃの例は、Rust の closures を使用して Strategy パターンの考え方を
示しています。
struct Adder;
impl Adder {
pub fn add<F>(x: u8, y: u8, f: F) -> u8
where
F: Fn(u8, u8) -> u8,
{
f(x, y)
}
}
fn main() {
let arith_adder = |x, y| x + y;
let bool_adder = |x, y| {
if x == 1 || y == 1 {
1
} else {
0
}
};
let custom_adder = |x, y| 2 * x + y;
assert_eq!(9, Adder::add(4, 5, arith_adder));
assert_eq!(0, Adder::add(0, 0, bool_adder));
assert_eq!(5, Adder::add(1, 3, custom_adder));
}
実際、Rust はすでに Options の map メソッドでこの考え方を使用しています。
fn main() {
let val = Some("Rust");
let len_strategy = |s: &str| s.len();
assert_eq!(4, val.map(len_strategy).unwrap());
let first_byte_strategy = |s: &str| s.bytes().next().unwrap();
assert_eq!(82, val.map(first_byte_strategy).unwrap());
}