今日の個人開発5~Factoryパターンに基づくCodeの改良~
はじめに
この記事は、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を作成します。
pub mod code_factory;
pub use code_factory::CodeFactory;
pub enum CodeFactory {
Hex,
Dec,
}
CodeFactoryのコンストラクタ
CodeFactoryはradixというu8の値から生成するのですが、せっかくなのでTryFrom<u8>
トレイトで実装してみます。
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を追加します。
struct Args {
...
/// Number of radix [10, 16]
#[arg(short, long, default_value_t = 10)]
radix: u8,
}
mainで生成
factoriesからCodeFactoryを参照できるようにし、factoryを生成します。
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の配下に設置します。
pub mod code;
pub use code::Code;
#[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から持ってきて実装できました。
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
を通して公開します。
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)
}
}
pub mod diff_result;
pub use diff_result::DiffResult;
...
Codeのdiff関数を実装
Codeのdiff関数を実装します。
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を引数に取るのが気になってしまうので、以下のような修正を加えました。
pub fn diff(&self, guess: &Code) -> Result<(DiffResult, bool), String> {
...
let is_correct = result.correct(self.len());
Ok((result, is_correct))
}
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の改良を行いました。
次回はさらに英語モードの追加をやってみたいです。
続きはこちら:
記事については、もっと読みやすくなるような修正を適宜していくつもりです。
実装こっちの方がいいよ!などあればコメントください。
Discussion