Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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());
}

関連項目