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 パターンの基本的な考え方は、特定の問題を解決するアルゴリズムがある場合に、 アルゴリズムの骨組みだけを抽象レベルで定義し、具体的なアルゴリズムの実装を 異なる部分に分離するというものです。

このようにすることで、アルゴリズムを使用するクライアントは特定の実装を選択でき、 一方で一般的なアルゴリズムのワークフローは同じままになります。言い換えると、 クラスの抽象仕様は派生クラスの具体的な実装に依存しませんが、具体的な実装は 抽象仕様に従わなければなりません。これが「依存関係逆転」と呼ばれる理由です。

動機

毎月レポートを生成するプロジェクトに取り組んでいると想像してください。レポートは、 たとえば JSONPlain Text 形式など、異なる形式(戦略)で生成する必要があります。 しかし状況は時間とともに変化し、将来どのような要件が出てくるかはわかりません。 たとえば、まったく新しい形式でレポートを生成する必要があるかもしれませんし、 既存の形式のいずれかを変更するだけでよいかもしれません。

この例では、不変要素(または抽象化)は FormatterReport であり、 TextJson が戦略構造体です。これらの戦略は 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"}"#));
}

利点

主な利点は関心の分離です。たとえば、この場合 ReportJsonText の 具体的な実装について何も知りません。一方で出力の実装は、データがどのように 前処理され、保存され、取得されるかを気にしません。それらが知る必要があるのは、 実装すべき特定のトレイトと、結果を処理する具体的なアルゴリズム実装を定義する そのメソッド、すなわち Formatterformat(...) だけです。

欠点

各戦略について少なくとも 1 つのモジュールを実装する必要があるため、戦略の数に 応じてモジュール数が増加します。選択可能な戦略が多数ある場合、ユーザーは 各戦略が互いにどのように異なるかを知っている必要があります。

議論

前の例では、すべての戦略が 1 つのファイルに実装されています。異なる戦略を 提供する方法には、次のようなものがあります。

  • すべてを 1 つのファイルに入れる(この例で示したように、モジュールとして 分離するのに似ています)
  • モジュールとして分離する。例: formatter::json モジュール、formatter::text モジュール
  • コンパイラの機能フラグを使用する。例: json 機能、text 機能
  • クレートとして分離する。例: json クレート、text クレート

Serde クレートは、Strategy パターンが実際に使われている良い例です。Serde では、 自分たちの型に対して SerializeDeserialize トレイトを手動で実装することで、 シリアライズ動作を完全にカスタマイズできます。 たとえば、serde_jsonserde_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 はすでに Optionsmap メソッドでこの考え方を使用しています。

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

関連項目