『ちょうぜつソフトウェア設計入門』を読むぞ:依存性注入編
引き続きRustの練習も兼ねてやっていく。DI、今ちょっとだけわかる(そういうネタではなく)ような気がしているので理解を固めたい。
テスト駆動開発の章で作ったFizzBuzzのNumberConverterは、与えられた数値を変換して文字列を返してくれるところまでしてくれる。
一般的にFizzBuzzといったら、一つの数値を変換するのでなく1から100までとかを指定して動かすような感じなので、そこの部分を作っていく。
FizzBuzzSequencePrinterを作り、1から100までのFizzBuzz結果をコンソールに表示するようにする。そこの数値の範囲は「仕様」なので、外部から与えるパラメータにしましょうということ。
ヨイショと実装。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));
}
}
}
オブジェクト指向プログラミングの観点でよくないところ。
- サブモジュールの変更の影響を直接受ける上位構造
- 文字を出力するような機能は、単体テストが困難
それらの課題を内部から追い出す。
必要な動作をするためのインスタンスやインターフェースを外から受け取るようにする。
そうしておけば、それらの詳細の変更影響を受けなくなる。
これで構造体のモック化はできているのか?とりあえず(モック関連では)エラー吐かれていない気がする。
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);
}
}
「依存を意図通りコールしていればよい」というテストシナリオ。およびそれにパスする実装。
なるほど、そうすればこういう感じでテストが書けるのか。
#[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);
}
}
外部から具象を与えられるようにすれば、こいつの責務は与えられたモノを正しく扱うことだけになるって寸法。使い方(インターフェース仕様)さえ変えなければ、こっちを変える必要はないと。まさにオブジェクト指向って感じだ。いいことですね。
依存性を注入する部分を作成…?
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}");
}
}
--> 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
テストから切り離した。これでいいような気はあまりしないが…。
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}");
}
}
こちらの記事の、「rust-analyzerの設定」の部分の話っぽい。なるほど。
DIコンテナやオートワイヤリングのあたりはじっくり読み返しまくる。
RustでのDIについて参考になりそうなもの。