🙌

今日の個人開発5~Factoryパターンに基づくCodeの改良~

2023/02/23に公開

はじめに

この記事は、coloname33が不定期で行う小規模個人開発の記録です。

本日のテーマ

前回の続きです。

Codeの改良について再考してみます。

環境

Rust

% cargo --version
cargo 1.67.1 (8ecd4f20a 2023-01-10)

作るもの

  • Hexモードの追加
  • CodeFactoryの実装
  • CheckResultの扱いを変更

Hexモードについて

現在、このゲームは10進法の数字列に対応しています。

今回は、既存のモードをDecモードと名付け、そこに16進法を使うHexモードを追加してみます。

Codeの再考

Codeという構造体について、少し整理してみます。

モードによる違いを吸収するために、外から見た振る舞いを同じにしたいと考えているので、そのためにCodeの外から見た振る舞いを規定してみましょう。

まずは生成について。

乱数による答えの自動生成と、文字列からの質問の生成の2通りの生成方法があります。

この生成方法は、DecモードとHexモードで処理の大部分は一緒であるものの、一部に違いがあります。

一方、生成後のCodeやcheck関数内の処理は共通となります。

現在の実装のまま拡張してこれを実装する場合は、from_randやfrom_stringが引数としてradixを取るようにすれば良い、のですが、それではモードを常にmain関数内で数字で管理することになり、両方でエラー処理も必要になってしまうので、避けたいです。

そこで、今回は、Codeの生成部分をfactories/code_factory.rsとして切り出し、Codeはmodels/code.rsとして分離してみようと思います。

自分は全くの素人ですが、デザインパターンでいう「factoryパターン」に該当する構成、だと思います。

参考:https://qiita.com/shoheiyokoyama/items/d752834a6a2e208b90ca

CheckResultについて

現在、CheckResultはcheckというメソッドをもち、その中でCodeの比較を行っています。

これは、check関数がCheckResultのコンストラクタであるという解釈によるものです

しかし、Codeは生成をCodeFactoryに任せたのと同様、生成はその下のstructに任せる形とするのが見通しも良くなっていい気がします。

また、checkというメソッド名も分かりにくいので、diffにリネームし、結果を表すオブジェクトもDiffResultと名付けます。

実装

CodeFactoryの実装

factoriesの作成

まずは、factories.rsとfactories/code_factory.rsを作成します。

factories.rs
pub mod code_factory;
pub use code_factory::CodeFactory;
factories/code_factory.rs
pub enum CodeFactory {
    Hex,
    Dec,
}

CodeFactoryのコンストラクタ

CodeFactoryはradixというu8の値から生成するのですが、せっかくなのでTryFrom<u8>トレイトで実装してみます。

factories/code_factory.rs
impl TryFrom<u8> for CodeFactory {
    type Error = String;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            10 => Ok(CodeFactory::Dec),
            16 => Ok(CodeFactory::Hex),
            _ => {
                return Err(format!(
                    "radixには[10, 16]のいずれかを指定してください。 r={}",
                    value
                ))
            }
        }
    }
}

コマンド引数の追加

Argsにradixを追加します。

main.rs
struct Args {
    ...
    /// Number of radix [10, 16]
    #[arg(short, long, default_value_t = 10)]
    radix: u8,
}

mainで生成

factoriesからCodeFactoryを参照できるようにし、factoryを生成します。

main.rs
mod factories;
use factories::CodeFactory;

fn main() {
    let args = Args::parse();
    let factory = match CodeFactory::try_from(args.radix) {
        Ok(factory) => factory,
        Err(e) => {
            println!("{}", e);
            return;
        }
    };
    ...
}

## Codeの生成

modelsの作成

Codeはmodelsの配下に設置します。

models.rs
pub mod code;
pub use code::Code;
models/code.rs
#[derive(Debug, PartialEq, Eq)]
pub struct Code(HashMap<u8, usize>);

impl Code {
    pub fn new(init: HashMap<u8, usize>) -> Self {
        Self(init)
    }
}

CodeFactoryでCodeを生成

sizeを指定してランダムに自動生成するgenerate関数と、文字列から生成するgenerate_from_string関数を実装します。

大部分を元のCodeのfrom_rand, from_stringから持ってきて実装できました。

factories/code_factory.rs
impl CodeFactory {
    pub fn generate(&self, len: usize) -> Code {
        match self {
            Self::Hex | Self::Dec => {
                let mut rng = rand::thread_rng();
                let choices: Vec<u8> = (0..(self.to_radix() as u8)).collect();

                let code = HashMap::from_iter(
                    choices
                        .choose_multiple(&mut rng, len)
                        .cloned()
                        .enumerate()
                        .map(|(i, d)| (d, i)),
                );

                Code::new(code)
            }
        }
    }

    pub fn generate_from_str(&self, s: &str) -> Result<Code, String> {
        let mut code = HashMap::new();

        match self {
            Self::Dec | Self::Hex => {
                for (i, c) in s.trim().chars().enumerate() {
                    let d = match c.to_digit(self.to_radix()) {
                        Some(d) => d as u8,
                        None => {
                            return Err(format!("数字として解釈できない文字があります。c={}", c))
                        }
                    };

                    if let Some(j) = code.insert(d, i) {
                        return Err(format!(
                            "{}つ目と{}つ目の数字が重複しています。d={}",
                            j + 1,
                            i + 1,
                            d
                        ));
                    }
                }
            }
        }

        Ok(Code::new(code))
    }

    fn to_radix(&self) -> u32 {
        match self {
            CodeFactory::Dec => 10,
            CodeFactory::Hex => 16,
        }
    }
}

モードの違いによってそれぞれ別の関数を定義しても良いのですが、かなり共通部分が多いので、今回は一つの処理で全部やることにします。

ネストがかなり深くなっているので、いい構成を考えたいところです。(参考記事のFactoryMethodパターンに変更するなど)

Codeの比較を実装

DiffResultの定義

まず、DiffResultを定義し、code.rsを通して公開します。

models/code/diff_result.rs
use std::fmt;

#[derive(Debug, PartialEq, Eq)]
pub struct DiffResult {
    pub hit: usize,
    pub blow: usize,
}

impl DiffResult {
    pub fn new() -> Self {
        Self { hit: 0, blow: 0 }
    }

    pub fn hit(&mut self) {
        self.hit += 1;
    }

    pub fn blow(&mut self) {
        self.blow += 1;
    }

    pub fn correct(&self, len: usize) -> bool {
        self.hit == len && self.blow == 0
    }
}

impl fmt::Display for DiffResult {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Result: {}hit, {}blow", self.hit, self.blow)
    }
}
models/code.rs
pub mod diff_result;
pub use diff_result::DiffResult;
...

Codeのdiff関数を実装

Codeのdiff関数を実装します。

models/code.rs
impl Code {
    ...
    pub fn diff(&self, guess: &Code) -> Result<DiffResult, String> {
        if self.0.len() != guess.0.len() {
            return Err(format!(
                "長さが間違っています。ans={}, guess={}",
                self.0.len(),
                guess.0.len()
            ));
        }

        let mut result = DiffResult::new();

        for (val, i) in guess.0.iter() {
            if let Some(j) = self.0.get(val) {
                if i == j {
                    result.hit();
                } else {
                    result.blow();
                }
            }
        }

        Ok(result)
    }
}

DiffResultのhit関数、blow関数を使って、できるだけ生の数字を触る部分の表出を抑えようとしています。

テストの実装

基本的に、既存コードを該当する場所に移動させるだけで実現できました。

詳しくは、完成したコードを確認してください。

おまけ

DiffResultのcorrect関数がlenを引数に取るのが気になってしまうので、以下のような修正を加えました。

models/code.rs
pub fn diff(&self, guess: &Code) -> Result<(DiffResult, bool), String> {
    ...
    
    let is_correct = result.correct(self.len());

    Ok((result, is_correct))
}
main.rs
    let (result, is_correct) = match answer.diff(&guess) {
        Ok(d) => d,
        Err(e) => {
            println!("{}\nもう一度入力してください。", e);
            continue;
        }
    };

    if is_correct {
        ...
    }

実装したコード

実際のコードが以下になります。

https://github.com/kyuki3rain/hit-and-blow-rust/tree/release/f94807700332e5

最後に

今回はFactoryパターンに基づいて(?)、Codeの改良を行いました。

次回はさらに英語モードの追加をやってみたいです。

続きはこちら:
https://zenn.dev/coloname33/articles/73c9e6b7b3dc60

記事については、もっと読みやすくなるような修正を適宜していくつもりです。

実装こっちの方がいいよ!などあればコメントください。

Discussion