Closed12

『ちょうぜつソフトウェア設計入門』を読むぞ:依存性注入編

(love cat)(love cat)

テスト駆動開発の章で作ったFizzBuzzのNumberConverterは、与えられた数値を変換して文字列を返してくれるところまでしてくれる。
一般的にFizzBuzzといったら、一つの数値を変換するのでなく1から100までとかを指定して動かすような感じなので、そこの部分を作っていく。

FizzBuzzSequencePrinterを作り、1から100までのFizzBuzz結果をコンソールに表示するようにする。そこの数値の範囲は「仕様」なので、外部から与えるパラメータにしましょうということ。

(love cat)(love cat)

ヨイショと実装。print_rangeを呼ばれる度にrulesを作るのが気になるが、まあ動く。

use crate::{
    core::{number_converter::NumberConverter, replace_rule_interface::ReplaceRuleInterface},
    spec::{cyclic_number_rule::CyclicNumberRule, pass_through_rule::PassThroughRule},
};

pub struct FizzBuzzSequencePrinter {}

impl FizzBuzzSequencePrinter {
    pub fn new() -> Self {
        FizzBuzzSequencePrinter {}
    }

    pub fn print_range(&self, begin: i32, end: i32) {
        let rules: Vec<Box<dyn ReplaceRuleInterface>> = vec![
            Box::new(CyclicNumberRule::new(3, "Fizz")),
            Box::new(CyclicNumberRule::new(5, "Buzz")),
            Box::new(PassThroughRule::new()),
        ];
        let fizzbuzz = NumberConverter::new(rules);
        for i in begin..end {
            println!("{}", fizzbuzz.convert(i));
        }
    }
}
(love cat)(love cat)

オブジェクト指向プログラミングの観点でよくないところ。

  • サブモジュールの変更の影響を直接受ける上位構造
  • 文字を出力するような機能は、単体テストが困難
(love cat)(love cat)

それらの課題を内部から追い出す。
必要な動作をするためのインスタンスやインターフェースを外から受け取るようにする。
そうしておけば、それらの詳細の変更影響を受けなくなる。

(love cat)(love cat)

これで構造体のモック化はできているのか?とりあえず(モック関連では)エラー吐かれていない気がする。

pub struct NumberConverter {
    rules: Vec<Box<dyn ReplaceRuleInterface>>,
}

#[cfg_attr(test, automock)]
impl NumberConverter {
    pub fn new(rules: Vec<Box<dyn ReplaceRuleInterface>>) -> Self {
        Self { rules }
    }

    pub fn convert(&self, n: i32) -> String {
        let mut carry = "".to_string();
        for rule in self.rules.iter() {
            if rule.match_check(&carry, n) {
                carry = rule.apply(carry, n);
            }
        }
        carry
    }
}
#[cfg(test)]
use mockall::automock;
use mockall_double::double;

#[double]
use crate::core::number_converter::NumberConverter;

#[cfg_attr(test, automock)]
pub trait OutPutInterface {
    fn write(&self, data: String);
}

pub struct FizzBuzzSequencePrinter<T: OutPutInterface> {
    fizzbuzz: NumberConverter,
    output: T,
}

impl<T: OutPutInterface> FizzBuzzSequencePrinter<T> {
    pub fn new(fizzbuzz: NumberConverter, output: T) -> Self {
        FizzBuzzSequencePrinter { fizzbuzz, output }
    }

    pub fn print_range(&self, begin: i32, end: i32) {
        todo!()
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use mockall::predicate::*;

    #[test]
    fn test_print_none() {
        let mut converter = NumberConverter::default();
        converter.expect_convert().times(0);

        let mut output = MockOutPutInterface::new();
        output.expect_write().times(0);

        let printer = FizzBuzzSequencePrinter::new(converter, output);
        printer.print_range(0, -1);
    }

    #[test]
    fn test_print_1to3() {
        let mut converter = NumberConverter::default();
        converter.expect_convert().times(3);
        converter.expect_convert().with(eq(1)).return_const("1");
        converter.expect_convert().with(eq(2)).return_const("2");
        converter.expect_convert().with(eq(3)).return_const("Fizz");

        let mut output = MockOutPutInterface::new();
        output.expect_write().times(3);
        output.expect_write().with(eq("1 1\n".to_string()));
        output.expect_write().with(eq("2 2\n".to_string()));
        output.expect_write().with(eq("3 Fizz\n".to_string()));

        let printer = FizzBuzzSequencePrinter::new(converter, output);
        printer.print_range(1, 3);
    }
}
(love cat)(love cat)

「依存を意図通りコールしていればよい」というテストシナリオ。およびそれにパスする実装。
なるほど、そうすればこういう感じでテストが書けるのか。

#[cfg(test)]
use mockall::automock;
use mockall_double::double;

#[double]
use crate::core::number_converter::NumberConverter;

#[cfg_attr(test, automock)]
pub trait OutPutInterface {
    fn write(&self, data: &str);
}

pub struct FizzBuzzSequencePrinter<T: OutPutInterface> {
    fizzbuzz: NumberConverter,
    output: T,
}

impl<T: OutPutInterface> FizzBuzzSequencePrinter<T> {
    pub fn new(fizzbuzz: NumberConverter, output: T) -> Self {
        FizzBuzzSequencePrinter { fizzbuzz, output }
    }

    pub fn print_range(&self, begin: i32, end: i32) {
        for i in begin..=end {
            let text = self.fizzbuzz.convert(i);
            let formatted_text = format!("{i} {text}\n");
            self.output.write(&formatted_text);
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use mockall::predicate::*;

    #[test]
    fn test_print_none() {
        let mut converter = NumberConverter::default();
        converter.expect_convert().times(0);

        let mut output = MockOutPutInterface::new();
        output.expect_write().times(0);

        let printer = FizzBuzzSequencePrinter::new(converter, output);
        printer.print_range(0, -1);
    }

    #[test]
    fn test_print_1to3() {
        let mut converter = NumberConverter::default();
        converter
            .expect_convert()
            .times(1)
            .with(eq(1))
            .return_const("1".to_string());
        converter
            .expect_convert()
            .times(1)
            .with(eq(2))
            .return_const("2".to_string());
        converter
            .expect_convert()
            .times(1)
            .with(eq(3))
            .return_const("Fizz".to_string());

        let mut output = MockOutPutInterface::new();
        output
            .expect_write()
            .times(1)
            .with(eq("1 1\n"))
            .return_const(());
        output
            .expect_write()
            .times(1)
            .with(eq("2 2\n"))
            .return_const(());
        output
            .expect_write()
            .times(1)
            .with(eq("3 Fizz\n"))
            .return_const(());

        let printer = FizzBuzzSequencePrinter::new(converter, output);
        printer.print_range(1, 3);
    }
}
(love cat)(love cat)

外部から具象を与えられるようにすれば、こいつの責務は与えられたモノを正しく扱うことだけになるって寸法。使い方(インターフェース仕様)さえ変えなければ、こっちを変える必要はないと。まさにオブジェクト指向って感じだ。いいことですね。

(love cat)(love cat)

依存性を注入する部分を作成…?

pub struct FizzBuzzAppFactory {}

impl FizzBuzzAppFactory {
    pub fn create(&self) -> FizzBuzzSequencePrinter<ConsoleOutput> {
        FizzBuzzSequencePrinter::new(self.create_fizzbuzz(), self.create_output())
    }

    fn create_fizzbuzz(&self) -> NumberConverter {
        NumberConverter::new(vec![
            Box::new(self.create_fizz_rule()),
            Box::new(self.create_buzz_rule()),
            Box::new(self.create_pass_through_rule()),
        ])
    }

    fn create_fizz_rule(&self) -> CyclicNumberRule {
        CyclicNumberRule::new(3, "Fizz")
    }

    fn create_buzz_rule(&self) -> CyclicNumberRule {
        CyclicNumberRule::new(5, "Buzz")
    }

    fn create_pass_through_rule(&self) -> PassThroughRule {
        PassThroughRule::new()
    }

    fn create_output(&self) -> ConsoleOutput {
        ConsoleOutput {}
    }
}
struct ConsoleOutput {}

impl OutPutInterface for ConsoleOutput {
    fn write(&self, data: &str) {
        print!("{data}");
    }
}
mismatched types
  --> src/fizzbuzz_app_factory.rs:11:38
   |
11 |         FizzBuzzSequencePrinter::new(self.create_fizzbuzz(), self.create_output())
   |         ---------------------------- ^^^^^^^^^^^^^^^^^^^^^^ expected struct `MockNumberConverter`, found struct `NumberConverter`
   |         |
   |         arguments to this function are incorrect
(love cat)(love cat)

テストから切り離した。これでいいような気はあまりしないが…。

use crate::{
    app::fizzbuzz_sequence_printer::{FizzBuzzSequencePrinter, OutPutInterface},
    core::number_converter::NumberConverter,
    spec::{cyclic_number_rule::CyclicNumberRule, pass_through_rule::PassThroughRule},
};

pub struct FizzBuzzAppFactory {}

#[cfg(not(test))]
impl FizzBuzzAppFactory {
    pub fn new() -> Self {
        Self {}
    }

    pub fn create(&self) -> FizzBuzzSequencePrinter<ConsoleOutput> {
        FizzBuzzSequencePrinter::new(self.create_fizzbuzz(), self.create_output())
    }

    fn create_fizzbuzz(&self) -> NumberConverter {
        NumberConverter::new(vec![
            Box::new(self.create_fizz_rule()),
            Box::new(self.create_buzz_rule()),
            Box::new(self.create_pass_through_rule()),
        ])
    }

    fn create_fizz_rule(&self) -> CyclicNumberRule {
        CyclicNumberRule::new(3, "Fizz")
    }

    fn create_buzz_rule(&self) -> CyclicNumberRule {
        CyclicNumberRule::new(5, "Buzz")
    }

    fn create_pass_through_rule(&self) -> PassThroughRule {
        PassThroughRule::new()
    }

    fn create_output(&self) -> ConsoleOutput {
        ConsoleOutput {}
    }
}
pub struct ConsoleOutput {}

impl OutPutInterface for ConsoleOutput {
    fn write(&self, data: &str) {
        print!("{data}");
    }
}
このスクラップは2023/07/02にクローズされました