- 投稿日:2020-02-10T23:29:43+09:00
Ruby の tilt-mustache パッケージを使って Mustache テンプレートを処理する
概要
- Ruby の tilt-mustache パッケージを使って Mustache テンプレートを処理する
今回の環境
- macOS Catalina
- Ruby 2.7.0
- tilt-mustache 0.0.2
- Mustache 1.1.1
- Tilt 2.0.10
- Sinatra 2.0.8.1
- Puma 4.3.1
Tilt とは
様々なテンプレートエンジンを統一して扱えるインターフェースを持ったライブラリ。
tilt | RubyGems.org | コミュニティのGemホスティングサービス
Generic interface to multiple Ruby template engines
tilt を導入することで、複数のテンプレートエンジンに対応したライブラリ (Web アプリケーションフレームワークや静的サイトジェネレーターなど) が作りやすくなる。
rtomayko/tilt: Generic interface to multiple Ruby template engines
Tilt is a thin interface over a bunch of different Ruby template engines in an attempt to make their usage as generic as possible. This is useful for web frameworks, static site generators, and other systems that support multiple template engines but don't want to code for each of them individually.
Tilt は一時期 Mustache をサポートしていたが今はしていない
Tilt と Mustache のコンセプト的なところで合わないということなのだろうか。
Tilt integration · Issue #72 · mustache/mustache
Tilt had a MustacheTemplate at one point but we ripped it out because of Mustache's inverted view/template relationship made it feel a little wonky. I actually prefer using Mustache's normal view-oriented interface in Sinatra as opposed to the render support. I think you miss out on a lot of the benefits of mustache when you try to shoehorn it into the single template file approach used by most other template systems.
Any Reason For Using Only Symbols in Locals Hash? · Issue #72 · rtomayko/tilt
That would require some more render-time code. I'm not sure if it's worth adding edge cases like this when they will both decrease performance and make the code more complex. Tilt doesn't have to be flexible when it comes to the locals-hash.
Anyway, we should still raise some exceptions if any of the keys are not a symbol.
Mustache support by Becojo · Pull Request #51 · rtomayko/tilt
Mustache isn't a regular template engine in the way that it requires two parts: a template and an implementation (in Ruby). Chris (of Mustache) closed the equivalent issue at Mustache's issue tracker because a Tilt implementation (at least the one provided here) wouldn't be able to take advantage of Mustache's core concept.
This patch also uses an interpreted mode even when Mustache is actually compiled behind the scenes.
tilt-mustache パッケージとは
Tilt に採用されなかった Pull Request のコードを元に、別にパッケージ化したものが tilt-mustache パッケージらしい。
tilt-mustache | RubyGems.org | コミュニティのGemホスティングサービス
Add Mustache to Tilt
DAddYE/tilt-mustache: Add Mustache to Tilt
This gem is a verbatim copy of this tilt pull request done by @Becojo
tilt-mustache パッケージのインストール
依存関係で mustache と tilt の現時点(2020年2月10日現在)での最新版もインストールされる。
$ gem install tilt-mustache Fetching mustache-1.1.1.gem Fetching tilt-mustache-0.0.2.gem Fetching tilt-2.0.10.gem (以下略)Mustache テンプレートの拡張子
tilt-mustache 0.0.2 では Mustache テンプレートは拡張子 mustache と ms に関連付けられている。
tilt-mustache/tilt-mustache.rb at v0.0.2 · DAddYE/tilt-mustache
register 'mustache', MustacheTemplate register 'ms', MustacheTemplateHello World
HTML を記述した Mustache テンプレートファイルを用意。
今回は hello.ms というファイル名で保存する。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>{{title}}</title> </head> <body> <!-- message を出力--> <p>{{message}}</p> </body> </html>ソースコード。
require 'tilt-mustache' # 変数を定義 title = 'Hello, world.<&>' message = 'こんにちは、世界。' # Tile に包まれた Mustache テンプレートエンジン template = Tilt.new('hello.ms') # self スコープでレンダリング output = template.render(self, :title => title, 'message' => message) # 結果を出力 puts output実行結果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>Hello, world.<&></title> </head> <body> <!-- message を出力--> <p>こんにちは、世界。</p> </body> </html>ループや条件分岐など
HTML を記述した Mustache テンプレートファイルを用意。
今回は my-template.mustache というファイル名で保存する。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>{{title}}</title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p>{{mydata.message}}</p> <p> <!-- ループ --> {{#mydata}} {{#list}} {{.}}<br> {{/list}} {{/mydata}} </p> <p> <!-- hoge が存在する場合に出力--> {{#mydata.hoge}} Hoge exists. {{/mydata.hoge}} </p> <p> <!-- fuga が存在しない場合に出力--> {{^mydata.fuga}} Fuga does not exists. {{/mydata.fuga}} </p> </body> </html>ソースコード。
require 'tilt-mustache' # Tile に包まれた Mustache テンプレートオブジェクト template = Tilt.new('my-template.mustache') # 第一引数に他に影響されないスコープとして Object.new を指定 # 第二引数以降にHashオブジェクト output = template.render( Object.new, :title => 'タイトル', :mydata => { :message => 'メッセージ', 'list' => ['foo', 'bar', 'baz'], 'hoge' => 'ほげ' }) # 結果を出力 puts output実行結果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>タイトル</title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p>メッセージ</p> <p> <!-- ループ --> foo<br> bar<br> baz<br> </p> <p> <!-- hoge が存在する場合に出力--> Hoge exists. </p> <p> <!-- fuga が存在しない場合に出力--> Fuga does not exists. </p> </body> </html>Sinatra で Mustache を使う
ソースコード一覧
├── app.rb └── views └── hello.mustacheapp.rb
Mustache は Sinatra でサポートされていない。
Sinatra にテンプレートエンジンを追加するには、Tilt でテンプレートエンジンを登録するのと、レンダリングメソッドを作る必要がある。
Tilt でテンプレートエンジンを登録するのは tilt-mustache パッケージがやってくれている。
レンダリングメソッドを作るのは自分でやる必要がある。
require 'sinatra' require 'tilt-mustache' # レンダリングメソッドを作る helpers do def mustache(*args) render(:mustache, *args) end end get '/hello/:message' do # HTML に埋め込む値 values = { :message => params['message'], 'ruby_desc' => RUBY_DESCRIPTION } # HTML を表示 mustache :hello, :locals => values endviews/hello.mustache
HTML を記述した Mustache テンプレートファイル。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, World!</title> </head> <body> <p>{{ message }}</p> <p>{{ ruby_desc }}</p> </body> </html>サーバを起動
$ ruby app.rb == Sinatra (v2.0.8.1) has taken the stage on 4567 for development with backup from Puma Puma starting in single mode... * Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://127.0.0.1:4567 * Listening on tcp://[::1]:4567 Use Ctrl-C to stopcurl でアクセス
必要な値が HTML に埋め込まれている。
$ curl -i "http://localhost:4567/hello/こんにちは&世界" HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Content-Length: 219 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, World!</title> </head> <body> <p>こんにちは&世界</p> <p>ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]</p> </body> </html>参考資料
- 投稿日:2020-02-10T22:17:44+09:00
スクリプト言語の比較しながらGoのお勉強 〜 標準入力編
前回からのおさらい
前回、Hello World編にて、標準出力に文字列を表示するという処理を確認しました。
今回はその対になります標準入力にてキーボードにて入力する文字列を受け取る処理を比較してみたいと思います。標準入力
簡単にキーボードから1行入力された文字列を表示するプログラムを見ていくことにしましょう。
Goの標準入力
Goでの標準入力のプログラムです。
input.gopackage main import ( "fmt" "os" "bufio" ) func main() { stdin := bufio.NewScanner(os.Stdin) stdin.Scan() text := stdin.Text() fmt.Printf(text + "\n") }実行してみましょう。
$ go run input.go Hello, Go Hello, Goただ単に鸚鵡返しするだけですが、思い通りの処理はできています。
プログラムの中身ですが、
importで3つのパッケージを読み込んでいます。fmt
パッケージは標準出力でも使用したものですね。os
パッケージとbufio
パッケージは今回初めて使用するライブラリです。
os
パッケージは公式サイトによると、オペレーティングシステムの機能へのプラットフォームに依存しないインタフェースを提供します。
Unixライクな設計です。ドキュメントを読んでると
syscall
パッケージをラップしていることがわかりました。os
パッケージはシステムのローレベルな処理から汎用的にレベルを上げたパッケージということですね。I/Oのバッファリング機能を提供します。io.Reader・io.Writerをラップし、別のオブジェクト(ReaderまたはWriter)を作成します。インタフェースは同じままでバッファリングやその他便利な機能を追加します。
とのことで
io
パッケージをラップして機能拡張しているものの様です。処理に関してですが、10行目のNewScannerはドキュメントによると1行ごと読み込む処理です。
またos.Stdinの処理はsyscall.Stdin
で/dev/stdin
を開いたファイルディスクリプたということで、標準入力を1行ごと読み込む処理の様です。
11行目で1行ごとバッファし、12行目で変数に文字列を格納、そして15行目で出力という流れになっています。簡単な標準入力の処理ですが、なかなか理解しなければならないことの多いGoです。
では、他の言語との比較です。
Python
Pythonでは組み込み関数の
input
関数で入力処理を実装できます。
組み込み関数input.pytext = input() print(text)実行結果
$ python input.py Hello, Python Hello, PythonRuby
Rubyでは組み込み関数の
gets
で実装可能です。
module Kernelinput.rbtext = gets print text実行結果
$ ruby input.rb Hello, Ruby Hello, RubyPerl
PerlではI/O演算子の
<>
で標準入力のファイルハンドルSTDIN
を指定して入力処理を実装します。
I/O 演算子input.pl$text = <STDIN>; print $text;実行結果
$ perl input.pl Hello, Perl Hello, PerlBash
Bashでは
read
コマンドで入力処理を実装します。
man readinput.shread text echo $text実行結果
$ bash input.sh Hello, Bash Hello, Bash入力処理を比較してみて
入力処理を比較してみて感じたことはHello World編と同様で、本体は簡素に作られているというところです。
今回は多少パッケージのソースにも目を通してみたところ、低レベルの処理を駆使して実装はできるのでしょうが、
車輪の再発明は無駄で何も旨味がないためパッケージの使用を学ぶべきと感じました。
(但し、実装方法を確認することは多くの知見を得られますので、できる限りソースには目を通すべきだと思います)
では、次回以降は変数に関しての演算子や文字列処理について勉強したいと思います。
- 投稿日:2020-02-10T22:00:16+09:00
if文 配列の中身があるかを判定する書き方
配列の中身が空か判定しtrue/falseを返すメソッドとして
if 変数 present?
が使える
presentは配列の中身があればtrueを返し
presentは配列の中身が無ければfalseを返します。
- 投稿日:2020-02-10T21:45:21+09:00
独学4ヶ月で大阪のWeb系自社開発企業に転職(アルバイトスタート)決定したので振り返りとアドバイス
独学4ヶ月で大阪のWeb系自社開発企業に転職(アルバイトスタート)決定したので振り返りとアドバイス
転職活動が終わりましたので振り返りのためとエンジニア転職目指されている方に少しでも参考になる情報を提供するために、約4ヶ月間やってきたことをまとめます。
来週から大阪のWeb系自社開発企業でアルバイトとして働くことになります。対象読者
- 未経験&独学でWeb系エンジニア就職目指している人(特に大阪で)
自己紹介
- 24歳
- 2019年に文系大学を内定無いまま卒業
- 6月に老舗メーカーに営業職として就職し、工場研修が2年間あることを知る
- 9月末プログラミング学習開始
- 2020年2月に大阪の自社開発企業にアルバイトとしての入社決定(デザインのコーディング中心にその他開発業務)
振り返り
9~10月 HTML/CSS復習、 RubyとRuby on Rails基礎(Dotinstall)
かつて2回ほどプログラミングに挑戦したときにHTML/CSSの基礎はやっていたので、その復習とRubyの基礎をDotinstallでやりました。Rubyを選択した一番の理由は、過去にPHPを学び始めて一瞬で挫折したからです。
11月 Railsチュートリアル、ポートフォリオ用Webアプリケーション開発
簡単にRubyとRuby on Railsを学んだ後、Railsチュートリアルを始めました。結局3、4周しました。1周目は全体像を把握するために流し読み。2周目は写経の様な形で手を動かして一通り。3周目は、作りたいWebアプリ(結局ボツになりました)に必要な機能を実装するのにチュートリアルを参考にしたのでその時に。
11月中旬に、ポートフォリオ用のWebアプリケーション開発を始めました。この時にもRailsチュートリアルを大いに参考にしました。
また、開発をスムーズに進めるためにMENTAでメンターさんと契約しました。12月 ポートフォリオ用Webアプリケーション開発、もくもく会初参加
継続して開発していましたが、集中力が持たなかったり、実装スピードが遅くなったりしたので、初めてもくもく会に参加しました。環境を変えるというのは本当に大事だなと実感しました。
結局1ヶ月半、120時間ほどでWebアプリケーションを形にすることができました。友人にURLを送り、フィードバックをもらいながら細かい修正を行うことが非常に楽しかったです。リンク貼っておきます。Rocklette | ユーザー投稿型の音楽レコメンドルーレット
使用技術 : Rails, jQuery, RSpec, Docker, S3, Heroku, Github1月 Webアプリケーションをデプロイ、転職活動開始
一応12月に完成していたものの、デプロイして宣伝するのが恥ずかしくてなかなかできませんでした。が、なんとかデプロイしました。Twitterの趣味アカウントと転職用アカウントでツイートした結果、3名の一般ユーザーさんに登録していただけました。ありがとうございました。
年始休み明けに退職することになり、転職活動を開始しました。Twitter、Wantedly、 Green、会社HPから応募しました。2月 アルバイトでの採用連絡
大阪の自社開発企業さんから採用の連絡をいただけました。
業界未経験ということで3ヶ月はアルバイトとして働き、その後は私の成長次第ということになります。2度の面談を経て採用していただきました。面談ではポートフォリオの説明や学習内容について話すことがありましたが、正直、技術力アピールはほとんどできませんでした。例えば「RubyやRailsはWebアプリ開発を通して基本的なことは学びましたが、完全に理解してるとは言えません。」など正直に話しました。結果的にポテンシャルを評価されての採用となりました。ちなみに募集要項の必須経験には「Webアプリケーション開発1年以上」など、私が経験したことのない項目がいくつかあったのですが、なんとかなりましたので皆さん気にせず応募しましょう。
最終的に転職活動の結果は
応募(Green, Wnatedly, 直応募): 37社
一次面接or面談:9社
二次or最終面接:3社
内定(アルバイトスタート):1社
となりました。
予想以上に苦戦して、第2次転職活動も覚悟していたところに内定が出たのでよかったです。書類添削、面接練習をすればよかったと後悔してます。
これからプログラミング学習、転職活動始める方へのアドバイスを3つ(主観)
大阪での就職を目標に、これから独学で学習を始める人はPHPが無難。
大阪はPHPを使っている企業が圧倒的に多いです。Ruby, Ruby on Railsをメインに使っている企業さんは有名企業か少数精鋭の自社開発企業が大半だと感じました。
そして未経験、独学でそのような企業から内定をもらうことは非常に困難だと思います。私は5社ほどRailsメインの自社開発企業に応募しましたが、全て書類で落選しました。
あくまで「Web業界に入ること」を最初の目標に設定し、PHPとフレームワークを勉強するのが無難かなと思います。面接で「Rubyでもしっかり学習すれば他の言語の習得も容易になるのでPHP未経験でも問題ない」とおっしゃる方も多かったですが、PHP以外の言語に何か強い拘りがないのであればPHPがいいでしょう。独学者は外部との繋がりを作って、オフラインで会話する機会を作ろう。
引きこもって学習していると、びっくりするくらい面接で喋れないからです。書類が通らず面接の機会も少なかったので最後まで面接に慣れませんでした。一番軽視し、最も苦労したことです。
オフラインで面接練習の相手ぐらいはできますので、よければTwitterのDMで連絡ください。(条件 : 大阪市内、無料、土日のみetc)
Twitterアカウントこれから転職を目指す独学&未経験の方は、最低限のプログラミング学習、面接対策をした上でそれら以外の何かアピール材料が必要
つまり「スクール生などの他の転職希望者といかに差別化するか」ということです。これが最も大事かもしれません。複数の会社の面談で「未経験のエンジニア希望者からの応募が非常に多い」ということを聞きました。
チーム開発などを経験したスクール生、同じ様に独学で学習している他の転職希望者ではなく、あなたを採用するメリットはなんでしょうか?
それを説得力持って説明できるほどの行動と実績がないと、学習を継続しているだけでは埋もれてしまう状況になっていると思います。
(Twitterでのアウトプットやもくもく会の主催などされている方は転職も成功している方が多いです。)
今後について
来週から出社することになりました。エンジニアとして働く環境を掴み取り、ようやくスタートラインに立てました。
まずはフロントのコーディングが中心になると思います。CSSとJava Scriptから逃げてきたのでしっかり勉強してます。
アルバイトという形態ではありますが、どんどん仕事をもらい、よく学び、サービスをより良くすることに貢献していきたい思っています。独学されてる方、転職活動されている方、何か相談などあればDMお待ちしてます。https://twitter.com/nju5555
参考
【実体験をもとに】30歳未経験から独学4ヶ月でバックエンドエンジニアとしてWeb系自社開発企業へ転職するまでのロードマップ
- 投稿日:2020-02-10T21:20:38+09:00
RubyでAtcoderからテストを自動生成するライブラリを作りました
Atcoderのコンテストにチマチマでているのですが、最近Rustでテスト生成や提出ができるCLIツールを作っている方を見かけました。
https://github.com/tanakh/cargo-atcoder丁度コンテスト時に入出力例をいちいち試すのが面倒だと感じていたので、Rubyでもテストを自動生成するGemをつくってみました。
https://github.com/QWYNG/green_day
使い方
インストール
gem install green_day
なりでインストールしてください。ログイン
bundle exec green_day login
でユーザー名とパスワードを要求されるので入力してください。
Atcoderからのset-cookieの中身をcookie-storeというファイルに保存します。セッションを消したかったらこのファイルを削除してください。
セッションがないと開催中のコンテストにアクセスすることができません。作業用ディレクトリ及びテストの作成
このコマンドでコンテスト用のディレクトリ及びテストを生成できます。
bundle exec green_day new <contest-name>
例えばabc150を対象にしたいときは下のようになります。
bundle exec green_day new abc150
こうするとこんな感じのディレクトリ構成になります。abc150 ├── A.rb ├── B.rb ├── C.rb ├── D.rb ├── E.rb ├── F.rb └── spec ├── A_spec.rb ├── B_spec.rb ├── C_spec.rb ├── D_spec.rb ├── E_spec.rb └── F_spec.rb自動生成されるテストは例えばabc150のA問題ならこんな感じになります。
require 'rspec' RSpec.describe 'test' do it 'test with "2 900\n"' do io = IO.popen("ruby abc150/A.rb", "w+") io.puts("2 900\n") expect(io.gets).to eq("Yes\n") end it 'test with "1 501\n"' do io = IO.popen("ruby abc150/A.rb", "w+") io.puts("1 501\n") expect(io.gets).to eq("No\n") end it 'test with "4 2000\n"' do io = IO.popen("ruby abc150/A.rb", "w+") io.puts("4 2000\n") expect(io.gets).to eq("Yes\n") end end後は
abc150/A.rb
に提出用のコードをそのまま書けばOKです。n, m = gets.split.map(&:to_i) if 500 * n >= m puts "Yes" else puts "No" endいつものようにrspecでテストすることができます。
> rspec abc150/spec/A_spec.rb ... Finished in 0.15933 seconds (files took 0.08655 seconds to load) 3 examples, 0 failuresテスト名に入力例をそのまま入れているのでテスト結果はそれをみて判断してください。
3) test test with "4 2000\n" Failure/Error: expect(io.gets).to eq("Yes\n") expected: "Yes\n" got: "No\n" (compared using ==) Diff: @@ -1,2 +1,2 @@ -Yes +No後書きとか
一応CIではRuby 2.7.0とAtcoder用にRuby2.3.3でテストしています。
(ローカルでは2.3系は入れるのが面倒だったので未確認です)
入出力例のスクレイピングは全コンテスト試したわけではないので、未対応のケースがあるかもしれません。もし見つけたら教えてくれると嬉しいです。
提出まわりはつくっていないのですが気が向いたら作ります。
- 投稿日:2020-02-10T19:45:26+09:00
form_forでのデータ取得
form_forでのデータ取得方法
params.require(:モデル名).permit(:カラム名)
- 投稿日:2020-02-10T19:25:26+09:00
Ruby の tilt パッケージを使って ERB テンプレートを処理する
概要
- Ruby の tilt パッケージを使って ERB テンプレートを処理する
- 今回の環境: macOS Catalina + Ruby 2.7.0 + tilt 2.0.10
tilt とは
様々なテンプレートエンジンを統一して扱えるインターフェースを持ったライブラリ。
tilt | RubyGems.org | コミュニティのGemホスティングサービス
Generic interface to multiple Ruby template engines
tilt を導入することで、複数のテンプレートエンジンに対応したライブラリ (Web アプリケーションフレームワークや静的サイトジェネレーターなど) が作りやすくなる。
rtomayko/tilt: Generic interface to multiple Ruby template engines
Tilt is a thin interface over a bunch of different Ruby template engines in an attempt to make their usage as generic as possible. This is useful for web frameworks, static site generators, and other systems that support multiple template engines but don't want to code for each of them individually.
tilt パッケージのインストール
$ gem install tiltHello World
HTML を記述した ERB テンプレートファイルを用意。
今回は hello.erb というファイル名で保存する。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- @title を出力--> <title><%= @title %></title> </head> <body> <!-- @message を出力--> <p><%= @message %></p> </body> </html>ソースコード。
require 'tilt' # 変数を定義 @title = 'Hello, world.' @message = 'こんにちは、世界。' # Tile に包まれた ERB テンプレートオブジェクト template = Tilt.new('hello.erb') # self が指すスコープでレンダリング output = template.render(self) # 結果を出力 puts output実行結果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- @title を出力--> <title>Hello, world.</title> </head> <body> <!-- @message を出力--> <p>こんにちは、世界。</p> </body> </html>ループや条件分岐など
HTML を記述した ERB テンプレートファイルを用意。
今回は my-template.erb というファイル名で保存する。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title><%= title %></title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p><%= mydata[:message] %></p> <p> <!-- ループ --> <% mydata['list'].each do |item| %> <%= item %><br> <% end %> </p> <p> <!-- hoge が存在する場合に出力--> <%= 'Hoge exists.' if mydata['hoge'] %> </p> <p> <!-- fuga が存在しない場合に出力--> <%= 'Fuga does not exists.' if !mydata['fuga'] %> </p> </body> </html>ソースコード。
require 'tilt' # Tile に包まれた ERB テンプレートオブジェクト template = Tilt.new('my-template.erb') # 第一引数: 他に影響されないスコープとして Object.new を指定 # 第二引数以降: テンプレートに渡す Hash オブジェクト output = template.render( Object.new, :title => 'タイトル', :mydata => { :message => 'メッセージ', 'list' => ['foo', 'bar', 'baz'], 'hoge' => 'ほげ' }) # 結果を出力 puts output実行結果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>タイトル</title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p>メッセージ</p> <p> <!-- ループ --> foo<br> bar<br> baz<br> </p> <p> <!-- hoge が存在する場合に出力--> Hoge exists. </p> <p> <!-- fuga が存在しない場合に出力--> Fuga does not exists. </p> </body> </html>参考資料
- 投稿日:2020-02-10T18:58:11+09:00
Rails フォームで入力した値を取得する方法
フォームで入力した値を取得する方法
params[:keyword]
- 投稿日:2020-02-10T17:16:58+09:00
【残業時間】タイムカード の計算ができるGem「punch_time」の使い方
この記事は
タイムカード計算 punch_time を作ったメモ
やりたかったこと
Railsで深夜残業時間の計算をするのにSQLだと再利用ができなそうだったので
やり方
インストール方法
gem 'punch_time'コンフィグ
コンフィグでシフトの開始、終了時間、休憩時間、深夜時間、時差が設定できます
PunchTime.configure do |config| config.shift_in_time = Time.parse('10:00') config.shift_out_time = Time.parse('19:00') config.breaks = [ { start_time: Time.parse('12:00'), end_time: Time.parse('13:00') } ] config.night = { start_time: Time.parse('22:00'), end_time: Time.parse('05:00') } config.offset = '+0900' endタイムカードの記録
就業時間はシフトの開始時間からで計算
PunchTime.punch(DateTime.parse('20200101 10:10'), DateTime.parse('20200101 19:00'))就業時間の出力
p PunchTime.sum_work.hours例えば、business_time、holiday_jpを組み合わせれば祝日の勤務時間計算がこうなります
sum_works = [] Date.parse('20200101').upto(Date.parse('20200105')) do |x| PunchTime.punch(DateTime.parse(x.to_s + ' 10:10'), DateTime.parse(x.to_s + ' 19:00')) sum_works.append(PunchTime.sum_work.hours) unless x.workday? end p sum_works.inject(:+).to_iそのほか
PunchTime.sum_work PunchTime.sum_tardy PunchTime.sum_over_work PunchTime.sum_night_work PunchTime.tardy? PunchTime.overtime_work? PunchTime.night_overtime_work?技術的ポイント
なあなあだったタイムゾーンとか、Rationalの扱いに結構時間とられたので、これからはオープンソースは人々の協力によって支えられているのだ、ということを心に留めてGithubを利用したい
- 投稿日:2020-02-10T17:16:58+09:00
【残業計算】タイムカード の計算ができるGem「punch_time」の使い方
この記事は
タイムカード計算 punch_time を作ったメモ
やりたかったこと
Railsで深夜残業時間の計算をするのにSQLだと再利用ができなそうだったので
やり方
インストール方法
gem 'punch_time'コンフィグ
コンフィグでシフトの開始、終了時間、休憩時間、深夜時間、時差が設定できます
PunchTime.configure do |config| config.shift_in_time = Time.parse('10:00') config.shift_out_time = Time.parse('19:00') config.breaks = [ { start_time: Time.parse('12:00'), end_time: Time.parse('13:00') } ] config.night = { start_time: Time.parse('22:00'), end_time: Time.parse('05:00') } config.offset = '+0900' endタイムカードの記録
就業時間はシフトの開始時間からで計算
PunchTime.punch(DateTime.parse('20200101 10:10'), DateTime.parse('20200101 19:00'))就業時間の出力
p PunchTime.sum_work.hours例えば、business_time、holiday_jpを組み合わせれば祝日の勤務時間計算がこうなります
sum_works = [] Date.parse('20200101').upto(Date.parse('20200105')) do |x| PunchTime.punch(DateTime.parse(x.to_s + ' 10:10'), DateTime.parse(x.to_s + ' 19:00')) sum_works.append(PunchTime.sum_work.hours) unless x.workday? end p sum_works.inject(:+).to_iそのほか
PunchTime.sum_work PunchTime.sum_tardy PunchTime.sum_over_work PunchTime.sum_night_work PunchTime.tardy? PunchTime.overtime_work? PunchTime.night_overtime_work?技術的ポイント
なあなあだったタイムゾーンとか、Rationalの扱いに結構時間とられたので、これからはオープンソースは人々の協力によって支えられているのだ、ということを心に留めてGithubを利用したい
- 投稿日:2020-02-10T17:16:58+09:00
【残業計算】タイムカードの計算ができるGem「punch_time」の使い方
この記事は
タイムカード計算 punch_time を作ったメモ
やりたかったこと
Railsで深夜残業時間の計算をするのにSQLだと再利用ができなそうだったので
やり方
インストール方法
gem 'punch_time'コンフィグ
コンフィグでシフトの開始、終了時間、休憩時間、深夜時間、時差が設定できます
PunchTime.configure do |config| config.shift_in_time = Time.parse('10:00') config.shift_out_time = Time.parse('19:00') config.breaks = [ { start_time: Time.parse('12:00'), end_time: Time.parse('13:00') } ] config.night = { start_time: Time.parse('22:00'), end_time: Time.parse('05:00') } config.offset = '+0900' endタイムカードの記録
就業時間はシフトの開始時間からで計算
PunchTime.punch(DateTime.parse('20200101 10:10'), DateTime.parse('20200101 19:00'))就業時間の出力
p PunchTime.sum_work.hours例えば、business_time、holiday_jpを組み合わせれば祝日の勤務時間計算がこうなります
sum_works = [] Date.parse('20200101').upto(Date.parse('20200105')) do |x| PunchTime.punch(DateTime.parse(x.to_s + ' 10:10'), DateTime.parse(x.to_s + ' 19:00')) sum_works.append(PunchTime.sum_work.hours) unless x.workday? end p sum_works.inject(:+).to_iそのほか
PunchTime.sum_work PunchTime.sum_tardy PunchTime.sum_over_work PunchTime.sum_night_work PunchTime.tardy? PunchTime.overtime_work? PunchTime.night_overtime_work?技術的ポイント
なあなあだったタイムゾーンとか、Rationalの扱いに結構時間とられたので、これからはオープンソースは人々の協力によって支えられているのだ、ということを心に留めてGithubを利用したい
- 投稿日:2020-02-10T14:19:20+09:00
GitHub Actionsで動かすRubocopを高速化する
TL;DR
Rubocopはキャッシュファイルを生成し、2回目以降は差分スキャンをおこないます。GiuHub Actionsのキャッシュ機能で、キャッシュファイルを保持すると実行時間を大きく削減できます。
Rubocopのキャッシュ
Rubocopはスキャンを高速化するため、実行後にホームディレクトリの
.cache
以下にキャッシュファイルを生成します。ルールに変更がない場合、2回目以降のスキャンでは変更されたファイルのみ検査します。
GitHub Actionsは環境が毎回クリアされるため、GitHub Actiosのキャッシュ機能を使って、Rubocopのキャッシュファイルを流用できるようにします。設定例(抜粋)
.github/workflows/rails.yml- name: Cache rubocop uses: actions/cache@v1 with: path: ~/.cache/rubocop_cache key: ${{ runner.os }}-rubocop-${{ github.head_ref }} restore-keys: | ${{ runner.os }}-rubocop- ${{ runner.os }}-rubocop-${{ github.base_ref }} ${{ runner.os }}-rubocop-${{ hashFiles('**/.rubocop.yml') }} - name: Rubocop run: bundle exec rubocop --parallel設定内容
Rubocopの前に指定します。キャッシュのキーワードを何にするかという問題がありますが、プルリクエストをトリガーに動かしているので、"ソースブランチ名">"ターゲットブランチ名">"rubocop.ymlのハッシュ"という順番で指定しています。
なるべくキャッシュを有効に使いたいところですが、キャッシュ生成時と現在のコード差分が大きくなると、結局スキャンするファイル数が増えてしまいます。
そこで、通常はソースブランチ名をキーにキャッシュし、はじめて実行するブランチの場合は、ターゲットブランチのキャッシュを使い回します。ターゲットブランチのキャッシュもない場合は、.rubocop.yml
のハッシュをキーにしたキャッシュを使います。
これにより、ブランチ単位で違うキャッシュを使って、効率的にテストできます。--parallelオプション
本筋とは関係ありませんが、rubocopに
--parallel
オプションを付与すると並列実行されます。
GitHub Actionsが実行される環境は2vCPUなので速くなります。効果
差分がないコードで実行してみました。
設定前
設定後
- 投稿日:2020-02-10T13:53:30+09:00
RubyのFiberで銀行口座を開設してみた
私は、Ruby の
Fiber
クラスをどう使うのかと今一つピンと来てませんでした。で、全然すっきりしていないのですが、考えているうちに、ふと、 Elixir の Process と似ているかも? と気づきました。
ということで、Fiber
初心者の私は、試しに、銀行口座を開設してみることにしました。今回使っている Ruby のバージョンは、 2.7.0 です。
最初の実装
まずは、Fiber を使って以下のコードを書いてみました。
account = Fiber.new do balance = 0 loop do Fiber.yield balance end endここで、何回、
account.resume
を実行しても、Fiber.yield
の引数balance
の値が0であるため、結果は、0です。irb(main):007:0> account.resume => 0 irb(main):008:0> account.resume => 0 irb(main):009:0> account.resume => 0
Fiber.new
のブロックの中のloop
の中を少しだけ変更してみます。account = Fiber.new do balance = 0 loop do amount = Fiber.yield balance balance += amount || 0 end end
account.resume
を呼び出す時に、引数に数値を指定して呼び出したり、引数なしで呼び出したりしてみます。irb(main):009:0> account.resume # 最初の呼び出し => 0 irb(main):010:0> account.resume(1000) # 1000円の預入 => 1000 irb(main):011:0> account.resume # 残高照会 => 1000 irb(main):012:0> account.resume # 残高照会 => 1000 irb(main):013:0> account.resume(-250) # 250円引き出し => 750 irb(main):014:0> account.resume # 残高照会 => 750 irb(main):015:0> account.resume # 残高照会 => 750
resume
を呼び出す時に、残高照会なのか、引き出しなのか、預け入れなのか引数で指定して、明示的にわかるようにちょっとコードを書き換えてみます。account = Fiber.new do balance = 0 loop do request, amount = Fiber.yield balance case request when :deposit balance += amount when :draw balance -= amount when :balance # do nothing end end endresume で呼び出す時は、
:balance
(残高照会)、:deposit
(預入)、:draw
(引き出し)を指定するようにします。irb(main):032:0> account.resume(:balance) # 残高照会 => 0 irb(main):033:0> account.resume(:deposit, 1000) # 1000円預入 => 1000 irb(main):034:0> account.resume(:balance) # 残高照会 => 1000 irb(main):035:0> account.resume(:draw, 250) # 250円引き出し => 750 irb(main):036:0> account.resume(:balance) # 残高照会 => 750銀行口座を複数作りやすいようにメソッド化します。
def create_account Fiber.new do balance = 0 loop do request, amount = Fiber.yield balance case request when :deposit balance += amount when :draw balance -= amount when :balance # do nothing end end end end2つの口座を開設してみます。
irb(main):017:0> account1 = create_account irb(main):018:0> account2 = create_account irb(main):019:0> account1.resume(:balance) => 0 irb(main):020:0> account2.resume(:balance) => 0 irb(main):021:0> account1.resume(:deposit, 3000) => 3000 irb(main):022:0> account2.resume(:deposit, 1200) => 1200 irb(main):023:0> account1.resume(:balance) => 3000 irb(main):024:0> account2.resume(:balance) => 1200 irb(main):025:0> account2.resume(:draw, 500) => 700 irb(main):026:0> account1.resume(:deposit, 750) => 3750 irb(main):027:0> account1.resume(:balance) => 3750 irb(main):028:0> account2.resume(:balance) => 700口座振替(振込) ができるように、
:transfer
の処理を create_account に追加します。def create_account Fiber.new do balance = 0 loop do request, amount, other = Fiber.yield balance case request when :deposit balance += amount when :draw balance -= amount when :transfer balance -= amount other.resume(:deposit, amount) when :balance # do nothing end end end end(この辺、Ruby 2.7であれば、パターンマッチ使って、もう少しスマートなコードになりそうな気がしますが、今回は無視します。)
irb(main):048:0> account3 = create_account irb(main):049:0> account4 = create_account irb(main):050:0> account3.resume(:balance) => 0 irb(main):051:0> account4.resume(:balance) => 0 irb(main):052:0> account3.resume(:deposit, 5000) # account3 に 5000円預入 => 5000 irb(main):053:0> account4.resume(:deposit, 1000) # account4 に 1000円預入 => 1000 irb(main):054:0> account3.resume(:transfer, 1500, account4) # account3 から account 4 に1500円振り込み => 3500 irb(main):055:0> account3.resume(:balance) # account3 の残高 => 3500 irb(main):056:0> account4.resume(:balance) # account4 の残高 => 2500Ruby っぽく Account をクラスにして、ラッピングしてみます。
class Account def initialize @account = create_account end def balance @account.resume(:balance) end def deposit(amount) @account.resume(:deposit, amount) end def draw(amount) @account.resume(:draw, amount) end def transfer(amount, account) @account.resume(:transfer, amount, account.fiber) end protected def fiber @account end private def create_account Fiber.new do balance = 0 loop do request, amount, other = Fiber.yield balance case request when :deposit balance += amount when :draw balance -= amount when :transfer balance -= amount other.resume(:deposit, amount) when :balance # do nothing end end end end endAccountクラスを使って口座を開設してみましょう。
irb(main):001:0> account1 = Account.new irb(main):002:0> account2 = Account.new irb(main):003:0> account1.balance => 0 irb(main):004:0> account2.balance => 0 irb(main):005:0> account1.deposit(5000) => 5000 irb(main):006:0> account2.deposit(1000) => 1000 irb(main):007:0> account1.draw(500) => 4500 irb(main):008:0> account1.transfer(1000, account2) => 3500 irb(main):009:0> account1.balance => 3500 irb(main):009:0> account2.balance => 2000と、ここまで来て、わざわざ、
Fiber
使わんでも、 普通にインスタンス変数使って、Account
クラス作ればええやんと思ったあなたは、正しいです。
次のような実装で同じようなことが実現できます。class Account attr_reader :balance def initialize @balance = 0 end def deposit(amount) @balance += amount end def draw(amount) @balance -= amount end def transfer(amount, account) @balance -= amount account.deposit(amount) end endということで、
Fiber
使わなくても、銀行口座を開設できるということがわかったのでした。言い方を変えれば、インスタンス変数もどきとしてFiber
を利用できるということがわかったところでしょうかまた、
Fiber
に関して何か閃いたら、別の記事を書くかも知れません。追記
最初に残高照会
resume
を呼び出さないとFiber
の例はうまく動作しないことがわかりました。class Account def initialize @account = create_account end def balance @account.resume(:balance) end def deposit(amount) @account.resume(:deposit, amount) end def draw(amount) @account.resume(:draw, amount) end def transfer(amount, account) @account.resume(:transfer, amount, account.fiber) end protected def fiber @account end private def create_account Fiber.new do balance = 0 loop do request, amount, other = Fiber.yield balance case request when :deposit balance += amount when :draw balance -= amount when :transfer balance -= amount other.resume(:deposit, amount) when :balance # do nothing end end end.tap(&:resume) # 1回 resume を呼び出す end end他にも、
account.transfer(1000, account)
みたいに自分自身に振り込もうとすると、FiberError
になるとか、まあ、色々、改善の余地がありそうです参考情報
今回の記事を書くのにヒントとなった Elixir の書籍を紹介しておきます。
- Programming Elixir 1.6 (Dave Thomas)
- 投稿日:2020-02-10T12:50:35+09:00
テスト駆動開発から始めるRuby入門 ~6時間でオブジェクト指向のエッセンスを体験する~
エピソード3
初めに
この記事は テスト駆動開発から始めるRuby入門 ~2時間でTDDとリファクタリングのエッセンスを体験する~ の続編です。
前提として エピソード1を完了して、テスト駆動開発から始めるRuby入門 ~ソフトウェア開発の三種の神器を準備する~ で開発環境を構築したところから始まります。 別途、セットアップ済み環境 を用意していますのでこちらからだとすぐに始めることが出来ます。
本記事は一応オブジェクト指向プログラム入門者向けとなっていますが、入門者の方は用語についてはわからなくても結構です、コードを繰り返し写経することで感覚を掴んでもらえば自ずと書いてあることはわかるようになってきますので。あと、概要はオブジェクト指向プログラム経験者に向けて書いたのものなので読み飛ばしてもらって結構です(ネタバレ内容です)、経験者の方からのツッコミお待ちしております。
概要
本記事では、 オブジェクト指向プログラム から オブジェクト指向設計 そして モジュール分割 を テスト駆動開発 を通じて実践していきます。
オブジェクト指向プログラム
エピソード1で作成したプログラムの追加仕様を テスト駆動開発 で実装します。 次に 手続き型コード との比較から オブジェクト指向プログラム を構成する カプセル化 ポリモフィズム 継承 という概念をコードベースの リファクタリング を通じて解説します。
具体的には フィールドのカプセル から setterの削除 を適用することにより カプセル化 を実現します。続いて、 ポリモーフィズムによる条件記述の置き換え から State/Strategyによるタイプコードの置き換え を適用することにより ポリモーフィズム の効果を体験します。そして、 スーパークラスの抽出 から メソッド名の変更 メソッドの移動 の適用を通して 継承 の使い方を体験します。さらに 値オブジェクト と ファーストクラス というオブジェクト指向プログラミングに必要なツールの使い方も学習します。
オブジェクト指向設計
次に設計の観点から 単一責任の原則 に違反している
FizzBuzz
クラスを デザインパターン の1つである Commandパターン を使ったリファクタリングである メソッドオブジェクトによるメソッドの置き換え を適用してクラスの責務を分割します。オブジェクト指向設計のイデオムである デザインパターン として Commandパターン 以外に Value Objectパターン Factory Methodパターン Strategyパターン を リファクタリング を適用する過程ですでに実現していたことを説明します。そして、オープン・クローズドの原則 を満たすコードに リファクタリング されたことで既存のコードを変更することなく振る舞いを変更できるようになることを解説します。加えて、正常系の設計を改善した後 アサーションの導入 例外によるエラーコードの置き換え といった例外系の リファクタリング を適用します。最後に ポリモーフィズム の応用として 特殊ケースの導入 の適用による Null Objectパターン を使った オープン・クローズドの原則 に従った安全なコードの追加方法を解説します。
モジュールの分割
仕上げは、モノリシック なファイルから個別のクラスモジュールへの分割を ドメインオブジェクト の抽出を通して ドメインモデル へと整理することにより モジュール分割 を実現することを体験してもらいます。最後に 良いコード と 良い設計 について考えます。
Before
After
オブジェクト指向から始めるテスト駆動開発
テスト駆動開発
エピソード1ので作成したプログラムに以下の仕様を追加します。
仕様
1 から 100 までの数をプリントするプログラムを書け。 ただし 3 の倍数のときは数の代わりに「Fizz」と、5 の倍数のときは「Buzz」とプリントし、 3 と 5 両方の倍数の場合には「FizzBuzz」とプリントすること。 タイプごとに出力を切り替えることができる。 タイプ1は通常、タイプ2は数字のみ、タイプ3は FizzBuzz の場合のみをプリントする。早速開発に取り掛かりましょう。エピソード2で開発環境の自動化をしているので以下のコマンドを実行するだけで開発を始めることができます。
$ rake
guard
が起動するとコンソールが使えなくなるのでもう一つコンソールを開いておきましょう。もしくは.
を使うことでguard
内でコンソールのコマンドを呼び出すことができます。[1] guard(main)> . ls coverage Gemfile.lock lib provisioning README.md tmp Gemfile Guardfile main.rb Rakefile test Vagrantfile [2] guard(main)> . pwd /workspace/tdd_rb [3] guard(main)> . git statusTODOリスト作成
まずは追加仕様を TODOリスト に落とし込んでいきます。
TODOリスト
タイプ1の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
タイプ1の場合
テストファースト アサートファースト で最初に失敗するテストから始めます。テストを追加しましょう。
ここでは既存の
FizzBuzz.generate
メソッドにタイプを 引数 として追加することで対応できるように変更してみたいと思います。まず、fizz_buzz_test.rb
ファイルに以下のテストコードを追加します。... end describe 'タイプごとに出力を切り替えることができる' do describe 'タイプ1の場合' do def test_1を渡したら文字列1を返す assert_equal '1', FizzBuzz.generate(1, 1) end end end describe '配列や繰り返し処理を理解する' do ...... 05:32:51 - INFO - Running: all tests Coverage report generated for MiniTest to /workspace/tdd_rb/coverage. 4 / 11 LOC (36.36%) covered. Started with run options --guard --seed 37049 ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00005623e6a24260 @name="タイプごとに出力を切り替えることができる::タイプ1の場合">, 0.0019176720088580623] test_1を渡したら文字列1を返す#タイプごとに出力を切り替えることができる::タイプ1の場合 (0.00s) Minitest::UnexpectedError: ArgumentError: wrong number of arguments (given 2, expected 1) /workspace/tdd_rb/lib/fizz_buzz.rb:6:in `generate' /workspace/tdd_rb/test/fizz_buzz_test.rb:74:in `test_1を渡したら文字列1を返す' 25/25: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00796s 25 tests, 26 assertions, 0 failures, 1 errors, 0 skips ...
ArgumentError: wrong number of arguments (given 2, expected 1)
引数 が違うと指摘されていますね。FizzBuzz.generate
メソッドの引数の変更したいのですが既存のテストを壊したくないのでここは デフォルト引数 使ってみましょう。メソッドの引数にはデフォルト値を指定する定義方法があります。これは、メソッドの引数を省略した場合に割り当てられる値です。
— かんたんRuby
... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s end ...... 05:32:52 - INFO - Inspecting Ruby code style: test/fizz_buzz_test.rb Guardfile 2/2 files |====================================== 100 =======================================>| Time: 00:00:00 2 files inspected, no offenses detected 05:32:54 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |====================================== 100 =======================================>| Time: 00:00:00 0 files inspected, no offenses detected 05:37:29 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb lib/fizz_buzz.rb:6:29: W: [Corrected] Lint/UnusedMethodArgument: Unused method argument - type. If it's necessary, use _ or _type as an argument name to indicate that it won't be used. def self.generate(number, type = 1) ^^^^ 1/1 file |======================================= 100 =======================================>| Time: 00:00:00 1 file inspected, 1 offense detected, 1 offense corrected 05:37:31 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |======================================= 100 =======================================>| Time: 00:00:00 1 file inspected, no offenses detected [1] guard(main)> 05:39:37 - INFO - Run all 05:39:37 - INFO - Running: all tests Coverage report generated for MiniTest to /workspace/tdd_rb/coverage. 4 / 11 LOC (36.36%) covered. Started with run options --guard --seed 8607 25/25: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00723s 25 tests, 27 assertions, 0 failures, 0 errors, 0 skips ...ちなみにここでは 引数に
type=1
と入力したのですがコードフォーマットによって以下のように自動修正されます。... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, _type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s end ...case式 を使って 引数 を判定できるように変更しましょう。ちなみに
_type
をメソッド内で変数として使うと警告されるのでtype
に変更しています。... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) case type when 1 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s end end ...... Started with run options --seed 51330 Progress: |=============================================================| Finished in 0.00828s 25 tests, 27 assertions, 0 failures, 0 errors, 0 skips 04:27:12 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |=================== 100 ====================>| Time: 00:00:00 1 file inspected, no offenses detected 04:27:13 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |=================== 100 ===================>| Time: 00:00:00 0 files inspected, no offenses detected ...テストは無事通りました。ここでコミットしておきます。
$ git add . $ git commit -m 'test: タイプ1の場合'追加仕様の取っ掛かりができました。既存のテストを流用したいので先程作成したテストを削除して以下のように新しいグループ内に既存テストコードを移動しましょう。
... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz end describe '三の倍数の場合' do def test_3を渡したら文字列Fizzを返す assert_equal 'Fizz', @fizzbuzz.generate(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列Buzzを返す assert_equal 'Buzz', @fizzbuzz.generate(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.generate(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1) end end describe '1から100までのFizzBuzzの配列を返す' do def setup @result = FizzBuzz.generate_list end def test_配列の初めは文字列の1を返す assert_equal '1', @result.first end def test_配列の最後は文字列のBuzzを返す assert_equal 'Buzz', @result.last end def test_配列の2番目は文字列のFizzを返す assert_equal 'Fizz', @result[2] end def test_配列の4番目は文字列のBuzzを返す assert_equal 'Buzz', @result[4] end def test_配列の14番目は文字列のFizzBuzzを返す assert_equal 'FizzBuzz', @result[14] end end end end ...テストコードが壊れていないことを確認したらコミットしておきます。
$ git add . $ git commit -m 'refactor: メソッドのインライン化'TODOリスト
タイプ1の場合
数を文字列にして返す
1を渡したら文字列"1"を返す3 の倍数のときは数の代わりに「Fizz」と返す_
3を渡したら文字列"Fizz"を返す5 の倍数のときは「Buzz」と返す_
5を渡したら文字列"Buzz"を返す3 と 5 両方の倍数の場合には「FizzBuzz」と返す_
15を渡したら文字列"FizzBuzz"を返すタイプ2の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には数を文字列にして返す
- 15を渡したら文字列"15"を返す
タイプ3の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には「FizzBuzz」と返す
- 15を渡したら文字列"FizzBuzz"を返す
タイプ2の場合
TODOリスト
タイプ1の場合タイプ2の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には数を文字列にして返す
- 15を渡したら文字列"15"を返す
タイプ3の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には「FizzBuzz」と返す
- 15を渡したら文字列"FizzBuzz"を返す
続いて、タイプ2の場合に取り掛かりましょう。
... end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1, 2) end end end ...... FAIL["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00005555ec747100 @name="数を文字列にして返す::タイプ2の場合::その他の場合">, 0.002283181995153427] test_1を渡したら文字列1を返す#数を文字列にして返す::タイプ2の場合::その他の場合 (0.00s) Expected: "1" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:75:in `test_1を渡したら文字列1を返す' 24/24: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00437s 24 tests, 26 assertions, 1 failures, 0 errors, 0 skips ...まだ 引数 に2を渡した場合は何もしないので case式 に2を渡した場合の処理を追加します。
... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) case type when 1 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s end end ...... Started with run options --seed 19625 Progress: |=============================================================================| Finished in 0.00894s 24 tests, 26 assertions, 0 failures, 0 errors, 0 skips ...テストが通ったのでテストケースを追加します。ここはタイプ1の場合をコピーして編集すれば良いでしょう。
... end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3, 2) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5, 2) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.generate(15, 2) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1, 2) end end end end ...... Coverage report generated for MiniTest to /workspace/tdd_rb/coverage. 4 / 13 LOC (30.77%) covered. Started with run options --guard --seed 898 27/27: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00900s 27 tests, 29 assertions, 0 failures, 0 errors, 0 skips 06:27:40 - INFO - Inspecting Ruby code style of all files test/fizz_buzz_test.rb:11:3: C: Metrics/BlockLength: Block has too many lines. [70/62] describe '数を文字列にして返す' do ... ^^^^^^^^^^^^^^^^^^^^^^^^ 7/7 files |====================================== 100 =======================================>| Time: 00:00:00 7 files inspected, 1 offense detected ...テストは通りましたが何やら警告が表示されるようになりました。
Metrics/BlockLength:Block has too many lines. これは数を文字列にして返す
テストケースのコードブロックが長いという警告のようですがテストコードはチェックの対象から外しておきたいので.rubocop_todo.yml
に以下コードを追加してチェック対象から外しておきます。... # Offense count: 2 # Configuration parameters: CountComments, ExcludedMethods. # ExcludedMethods: refine Metrics/BlockLength: Max: 62 Exclude: - 'test/fizz_buzz_test.rb' ...ちなみに
guard(main)>
にカーソルを合わせてエンターキーを押すと自動化タスクが実行されます。[1] guard(main)> 02:03:15 - INFO - Run all /home/gitpod/.rvm/rubies/ruby-2.6.3/bin/ruby -w -I"lib" -I"/workspace/.rvm/gems/rake-13.0.1/lib" "/workspace/.rvm/gems/rake-13.0.1/lib/rake/rake_test_loader.rb" "./test/fizz_buzz_test.rb" /home/gitpod/.rvm/rubies/ruby-2.6.3/bin/ruby -w -I"lib" -I"/workspace/.rvm/gems/rake-13.0.1/lib" "/workspace/.rvm/gems/rake-13.0.1/lib/rake/rake_test_loader.rb" "./test/fizz_buzz_test.rb" Started with run options --seed 47335 Progress: |==============================================================================| Finished in 0.00781s 27 tests, 29 assertions, 0 failures, 0 errors, 0 skips Started with run options --seed 47825 Progress: |==============================================================================| Finished in 0.00761s 27 tests, 29 assertions, 0 failures, 0 errors, 0 skips 02:03:17 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 13 / 13 LOC (100.0%) covered. Started with run options --guard --seed 17744 27/27: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00789s 27 tests, 29 assertions, 0 failures, 0 errors, 0 skips 02:03:17 - INFO - Inspecting Ruby code style of all files 7/7 files |=========================== 100 ============================>| Time: 00:00:00 7 files inspected, no offenses detected 02:03:19 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/border.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/loading_background.png 0/0 files |=========================== 100 ============================>| Time: 00:00:00 0 files inspected, no offenses detected [1] guard(main)>警告は消えたのでコミットしておきます。
$ git add . $ git commit -m 'test: タイプ2の場合'TODOリスト
タイプ1の場合タイプ2の場合
数を文字列にして返す
1を渡したら文字列"1"を返す3 の倍数のときは数を文字列にして返す
3を渡したら文字列"3"を返す5 の倍数のときは数を文字列にして返す
5を渡したら文字列"5"を返す3 と 5 両方の倍数の場合には数を文字列にして返す
15を渡したら文字列"15"を返すタイプ3の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には「FizzBuzz」と返す
- 15を渡したら文字列"FizzBuzz"を返す
タイプ3の場合
TODOリスト
タイプ1の場合
タイプ2の場合タイプ3の場合
数を文字列にして返す
- 1を渡したら文字列"1"を返す
3 の倍数のときは数を文字列にして返す
- 3を渡したら文字列"3"を返す
5 の倍数のときは数を文字列にして返す
- 5を渡したら文字列"5"を返す
3 と 5 両方の倍数の場合には「FizzBuzz」と返す
- 15を渡したら文字列"FizzBuzz"を返す
続いて、タイプ3の場合ですがやることは同じなので今回は一気にテストを書いてみましょう。
... describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3, 3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5, 3) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.generate(15, 3) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1, 3) end end end end ...... FAIL["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00005642171ea5a0 @name="数を文字列にして返す::タイプ3の場合::その他の場合">, 0.003375133004738018] test_1を渡したら文字列1を返す#数を文字列にして返す::タイプ3の場合::その他の場合 (0.00s) Expected: "1" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:123:in `test_1を渡したら文字列1を返す' FAIL["test_5を渡したら文字列5を返す", #<Minitest::Reporters::Suite:0x000056421723af78 @name="数を文字列にして返す::タイプ3の場合::五の倍数の場合">, 0.003832244998193346] test_5を渡したら文字列5を返す#数を文字列にして返す::タイプ3の場合::五の倍数の場合 (0.00s) Expected: "5" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:111:in `test_5を渡したら文字列5を返す' FAIL["test_3を渡したら文字列3を返す", #<Minitest::Reporters::Suite:0x0000564217297340 @name="数を文字列にして返す::タイプ3の場合::三の倍数の場合">, 0.0043466729985084385] test_3を渡したら文字列3を返す#数を文字列にして返す::タイプ3の場合::三の倍数の場合 (0.00s) Expected: "3" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:105:in `test_3を渡したら文字列3を返す' FAIL["test_15を渡したら文字列FizzBuzzを返す", #<Minitest::Reporters::Suite:0x00005642174dec98 @name="数を文字列にして返す::タイプ3の場合::三と五の倍数の場合">, 0.006096020006225444] test_15を渡したら文字列FizzBuzzを返す#数を文字列にして返す::タイプ3の場合::三と五の倍数の場合 (0.01s) Expected: "FizzBuzz" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:117:in `test_15を渡したら文字列FizzBuzzを返す' 31/31: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00650s 31 tests, 33 assertions, 4 failures, 0 errors, 0 skips ...case式 に処理を追加します。
... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) case type when 1 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz number.to_s end end ...... Started with run options --seed 12137 Progress: |=============================================================================| Finished in 0.01662s 31 tests, 33 assertions, 0 failures, 0 errors, 0 skips 05:06:44 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png lib/fizz_buzz.rb lib/fizz_buzz.rb:6:3: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for generate is too high. [10/8] def self.generate(number, type = 1) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lib/fizz_buzz.rb:6:3: C: Metrics/PerceivedComplexity: Perceived complexity for generate is too high. [8/7] def self.generate(number, type = 1) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1/1 file |=========================== 100 ============================>| Time: 00:00:00 1 file inspected, 2 offenses detected ...テストは通りましたが新しい警告が表示されるようになりました。とりあえずコミットしておきます。
$ git add . $ git commit -m 'test: タイプ3の場合'処理の追加により一部重複が発生しました。ここは、 ステートメントのスライド を適用して重複をなくしておきましょう。
ステートメントのスライド
旧:重複した条件記述の断片の統合
— リファクタリング(第2版)
重複した条件記述の断片の統合
条件式のすべて分岐に同じコードの断片がある。
それを式の外側に移動する。
— 新装版 リファクタリング
... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) case type when 1 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz number.to_s end end ...... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s end end ...警告は消えていませんがプログラムは壊れていないことが確認できたのでコミットしておきます。
$ git add . $ git commit -m 'refactor: ステートメントのスライド'TODOリスト
タイプ1の場合
タイプ2の場合タイプ3の場合
数を文字列にして返す
1を渡したら文字列"1"を返す3 の倍数のときは数を文字列にして返す
3を渡したら文字列"3"を返す5 の倍数のときは数を文字列にして返す
5を渡したら文字列"5"を返す3 と 5 両方の倍数の場合には「FizzBuzz」と返す
15を渡したら文字列"FizzBuzz"を返すそれ以外のタイプの場合
追加仕様には対応しましたがタイプ1,2,3以外の値が 引数 として渡された場合はどうしましょうか? 現状では
nil
を返しますがこのような例外ケースも考慮する必要があります。TODOリスト
タイプ1の場合
タイプ2の場合
タイプ3の場合それ以外のタイプの場合
例外処理 を追加します。まず、例外のテストですが以下の様に書きます。
例外とは記述したプログラムが想定していない値を受け取ったり、何らかの障害が発生した場合に処理を中断して、例外オブジェクトを生成して呼び出し元のメソッドに処理を戻す機構です。
— かんたんRuby
describe 'タイプ3の場合' do ... end describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz end def test_例外を返す e = assert_raises RuntimeError do @fizzbuzz.generate(1, 4) end assert_equal '該当するタイプは存在しません', e.message end end ...... FAIL["test_例外を返す", #<Minitest::Reporters::Suite:0x0000558a26888e60 @name="数を文字列にして返す::それ以外のタイプの場合">, 0.003033002998563461] test_例外を返す#数を文字列にして返す::それ以外のタイプの場合 (0.00s) RuntimeError expected but nothing was raised. /workspace/tdd_rb/test/fizz_buzz_test.rb:134:in `test_例外を返す' 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00609s 32 tests, 34 assertions, 1 failures, 0 errors, 0 skips ...case式 に該当しないタイプが指定された場合は 例外を発生させる ようにします。
例外を明示的に発生させるには「raise」を使います。raiseには発生させたい例外クラスを指定するのですが、何も指定しない場合はRuntimeErrorオブジェクトが生成されます。
— かんたんRuby
... class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end ...... 07:04:53 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 16 / 16 LOC (100.0%) covered. Started with run options --guard --seed 32508 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00600s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...テストが通ったのでコミットしておきます。
$ git add . $ git commit -m 'test: それ以外のタイプの場合'TODOリスト
タイプ1の場合
タイプ2の場合
タイプ3の場合
それ以外のタイプの場合
TODOリスト
をすべて完了しました。追加仕様を満たすプログラムは出来ましたがまだ改善の余地がありそうですね。以降ではオブジェクト指向アプローチによるコードのリファクタリングを解説していきたいと思います。オブジェクト指向
手続き型プログラム
オブジェクト指向 の解説の前に以下のコードを御覧ください。いわゆる 手続き型 で書かれたコードですが、これも追加仕様を満たしています。
MAX_NUMBER = 100 type = 1 list = [] MAX_NUMBER.times do |i| r = '' i += 1 case type when 1 if i % 3 == 0 && i % 5 == 0 r = 'FizzBuzz' elsif i % 3 == 0 r = 'Fizz' elsif i % 5 == 0 r = 'Buzz' else r = i.to_s end when 2 r = i.to_s when 3 if i % 3 == 0 && i % 5 == 0 r = 'FizzBuzz' else r = i.to_s end else r = '該当するタイプは存在しません' end list.push(r) end puts list処理の流れをフローチャートにしたものです、実態はコードに記述されている内容を記号に置き換えて人間が読めるようにしたものです。
オブジェクト指向プログラム
続いて、これまでに作ってきたコードがこちらになります。上記の 手続き型コード との大きな違いとして
class
というキーワードでくくられている部分があります。クラスとは、大まかに説明すると何らかの値と処理(メソッド)をひとかたまりにしたものです。
— かんたんRuby
class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end def self.generate_list # 1から最大値までのFizzBuzz配列を1発で作る (1..MAX_NUMBER).map { |n| generate(n) } end endUML を使って上記のコードの構造をクラス図として表現しました。
更にシーケンス図を使って上記のコードの振る舞いを表現しました。
手続き型コード のフローチャートと比べてどう思われましたか?具体的な記述が少なくデータや処理の概要だけを表現しているけどFizzBuzzのルールを知っている人であれば何をやろうとしているかのイメージはつかみやすいのではないでしょうか?だから何?と思われるかもしれませんが現時点では オブジェクト指向 において 抽象化 がキーワードだという程度の認識で十分です。
オブジェクト指向の理解を深める取り掛かりにはこちらの記事を参照してください。
オブジェクト指向の詳細は控えるとして、ここでは カプセル化 ポリモフィズム 継承 というオブジェクト指向プログラムで原則とされる概念をリファクタリングを通して体験してもらい、オブジェクト指向プログラムの感覚を掴んでもらうことを目的に解説を進めていきたいと思います。
カプセル化
フィールドのカプセル化
まず、データとロジックを1つのクラスにまとめていくためのリファクタリングを実施していくとします。
FizzBuzz
クラスにFizzBuzz配列を保持できるようして以下のように取得できるようにしたいと思います。... fizzbuzz.generate_list @result = fizzbuzz.list ...まず、 インスタンス変数 追加します。次に
self
キーワードを外して クラスメソッド から インスタンスメソッド に変更します。クラスメソッドはいくつか定義方法がありますが、どの方法を使ってもクラスメソッドとして定義されれば「クラス名.メソッド名」という形で呼び出せます。
— かんたんRuby
インスタンスメソッドはコンストラクタと同じようにクラス内でdefキーワードを使ってメソッドを定義するだけで作成できます。
— かんたんRuby
class FizzBuzz MAX_NUMBER = 100 def self.generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end def self.generate_list # 1から最大値までのFizzBuzz配列を1発で作る (1..MAX_NUMBER).map { |n| generate(n) } end endclass FizzBuzz MAX_NUMBER = 100 def list @list end def generate(number, type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end end... ERROR["test_15を渡したら文字列FizzBuzzを返す", #<Minitest::Reporters::Suite:0x00005613555ed120 @name="数を文字列にして返す::タイプ3の場合::三と五の倍数の場合">, 0.0041351839900016785] test_15を渡したら文字列FizzBuzzを返す#数を文字列にして返す::タイプ3の場合::三と五の倍数の場合 (0.00s) Minitest::UnexpectedError: NoMethodError: undefined method `generate' for FizzBuzz:Class /workspace/tdd_rb/test/fizz_buzz_test.rb:117:in `test_15を渡したら文字列FizzBuzzを返す' ...FizzBuzz配列を インスタンス変数
@list
に 代入 して インスタンス変数経由で取得できるように変更しました。変更にあたり クラスメソッドFizzBuzz.generate
とFizzBuzz.generate_list
を インスタンスメソッド に変更しています。それに伴ってテストが失敗してNoMethodError: undefined method `generate'
と表示されるようになってしまいました。インスタンスメソッド が使えるようにするためnew
メソッドを使ってFizzBuzzクラスの インスタンス を作りFizzBuzz配列を インスタンス変数 経由で取得するようにテストコードを変更します。クラスとして定義された情報を元に具体的な値を伴ったオブジェクトを作成することをインスタンス化と呼び、生成されたオブジェクトのことをインスタンスと呼びます。
— かんたんRuby
... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new end ... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new fizzbuzz.generate_list @result = fizzbuzz.list end ... end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new end ... end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new end ... end describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz.new end ... end end ...... 07:17:36 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 5 / 17 LOC (29.41%) covered. Started with run options --guard --seed 7701 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00616s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...テストが直りました。クラスメソッド インスタンスメソッド インスタンス変数 インスタンス などいろんな単語が出てきて戸惑ってしまったかもしれませんが、ピンとこないうちは クラス に値や状態を保持させるためには インスタンス化 する必要があってそのためには
new
メソッドを使わないといけないのね程度の理解で十分です。大概のことは手を動かしているうちにピンと来るようになります。インスタンス変数 に直接アクセスしているのでここは アクセッサメソッド を使って フィールドのカプセル化 を適用しておきます。
オブジェクト指向ではクラス内の値をカプセル化することが重要ですが、時には内部で保持しているインスタンス変数を参照や更新できる方が良い場合もあります。複雑な処理ではなく、単にインスタンス変数にアクセスするためのメソッドのことを、アクセッサメソッドと呼びます。
— かんたんRuby
フィールドのカプセル化
公開フィールドがある。
それを非公開にして、そのアクセサを用意する。
— 新装版 リファクタリング
自動実行の結果、以下のように書き換えられている部分を変更します。
class FizzBuz、 MAX_NUMBER = 100 attr_reader :list ...class FizzBuzz MAX_NUMBER = 100 attr_accessor :list ...テストが動作して既存のコードが壊れていないことが確認できたのでここでコミットします。
$ git add . $ git commit -m 'refactor: フィールドのカプセル化'引き続き、FizzBuzz配列は保持できるようになりましたがタイプごとに出力される配列のパターンは違います。FizzBuzzクラスにタイプを持たる必要があります。ここでは コンストラクタ を使って インスタンス化 する際に インスタンス変数 に 代入 するようにします。Rubyでは initialize というメソッドを使って初期化処理を実行します。
クラスをインスタンス化した時に初期化処理を行うシチュエーションはよくあります。このような初期化処理を行うメソッドをコンストラクタと呼び、Rubyではinitializeという特別なメソッドを用意することで実現できます。
— かんたんRuby
class FizzBuzz MAX_NUMBER = 100 attr_accessor :list def initialize(type) @type = type end ...... ERROR["test_3を渡したら文字列3を返す", #<Minitest::Reporters::Suite:0x00005564e21e85b0 @name="数を文字列にして返す::タイプ3の場合::三の倍数の場合">, 0.004276092993677594] test_3を渡したら文字列3を返す#数を文字列にして返す::タイプ3の場合::三の倍数の場合 (0.00s) Minitest::UnexpectedError: ArgumentError: wrong number of arguments (given 0, expected 1) /workspace/tdd_rb/lib/fizz_buzz.rb:7:in `initialize' /workspace/tdd_rb/test/fizz_buzz_test.rb:101:in `new' /workspace/tdd_rb/test/fizz_buzz_test.rb:101:in `setup' ...テストが失敗して引数が違うというエラーが表示される用になりました。
new
メソッドの 引数 にタイプを渡すようにテストを変更します。... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new(1) end ... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list @result = fizzbuzz.list end ... end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(2) end ... end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(3) end ... end describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz.new(4) end ... end end ...... 07:28:38 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 6 / 19 LOC (31.58%) covered. Started with run options --guard --seed 46661 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00793s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...テストは直りましたがまだ インスタンス変数 のタイプが使われていないので使うようにプロダクトコードを変更します。
class FizzBuzz MAX_NUMBER = 100 attr_accessor :list def initialize(type) @type = type end def generate(number, _type = 1) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case @type ...
FizzBuzz.gnerate
メソッドの 引数 からtype
を削除します。class FizzBuzz MAX_NUMBER = 100 attr_accessor :list def initialize(type) @type = type end def generate(number) ...... ERROR["test_15を渡したら文字列FizzBuzzを返す", #<Minitest::Reporters::Suite:0x0000564e16c14200 @name="数を文字列にして返す::タイプ3の場合::三と五の倍数の場合">, 0.01706391001062002] test_15を渡したら文字列FizzBuzzを返す#数を文字列にして返す::タイプ3の場合::三と五の倍数の場合 (0.02s) Minitest::UnexpectedError: ArgumentError: wrong number of arguments (given 2, expected 1) /workspace/tdd_rb/lib/fizz_buzz.rb:11:in `generate' /workspace/tdd_rb/test/fizz_buzz_test.rb:118:in `test_15を渡したら文字列FizzBuzzを返す' ...続いて、
FizzBuzz#generate
メソッドから不要になった 引数type
を削除したところテストが壊れたのでテストコードを修正します。... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do ... describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(2) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.generate(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1) end end end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(3) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.generate(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1) end end end describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz.new(4) end def test_例外を返す e = assert_raises RuntimeError do @fizzbuzz.generate(1) end assert_equal '該当するタイプは存在しません', e.message end end end ...... 07:34:57 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 15 / 19 LOC (78.95%) covered. Started with run options --guard --seed 59116 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00700s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...インスタンス変数 の
@type
も アクセッサメソッド を使って フィールドのカプセル化 を適用しておきます。class FizzBuzz MAX_NUMBER = 100 attr_accessor :list def initialize(type) @type = type end ...class FizzBuzz MAX_NUMBER = 100 attr_accessor :list attr_accessor :type def initialize(type) @type = type end ...... Started with run options --guard --seed 56315 32/32: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01069s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...コミットしておきます。
$ git add . $ git commit -m 'refactor: フィールドのカプセル化'setterの削除
FizzBuzz配列を取得する アクセッサメソッド は現在このように定義されています。
class FizzBuzz MAX_NUMBER = 100 attr_accessor :list attr_accessor :type ...以下のようにテストコードを変更したらどうなるでしょうか?
... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list @result = fizzbuzz.list end ...... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list fizzbuzz.list = [] @result = fizzbuzz.list end ...... FAIL["test_配列の2番目は文字列のFizzを返す", #<Minitest::Reporters::Suite:0x0000563c29a8a8c0 @name="数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.005137628992088139] test_配列の2番目は文字列のFizzを返す#数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) Expected: "Fizz" Actual: nil /workspace/tdd_rb/test/fizz_buzz_test.rb:58:in `test_配列の2番目は文字列のFizzを返す' ...FizzBuzz配列が初期化されてしまいました。アクセッサメソッド に参照のための getter と 更新するための setter が許可されているため カプセル化 が破られてしまいました。ここは setterの削除 を適用して外部からの更新を出来ないようにしておきましょう。
getterを定義するには、「attr_reader」を使います。このメソッドにインスタンス変数の「@」を除いた名称をシンボル表現にしたものを列挙します。複数ある場合はカンマで区切って複数の値を指定することができます。
— かんたんRuby
setterを定義するには、「attr_writer」を使います。このメソッドもattr_readerと同じくインスタンス変数名の「@」を除いた名称をシンボル表現にしたものを列挙します。複数ある場合はカンマで区切って複数の値を指定することができます。
— かんたんRuby
getter/setterの両方を定義する場合、そのインスタンスは属しているクラス外から自由に参照や更新ができてしまいます。これはカプセル化の観点には反した挙動なので、できる限りattr_readerだけで済ませられないか検討しましょう。
— かんたんRuby
setterの削除
setterが用意されているということは、フィールドが変更される可能性があることを意味します。オブジェクトを生成した後でフィールドを変更したくないなら、setterは用意しません(加えて、フィールドを変更不可にします)。そうすることで、フィールドはコンストラクタでのみで設定され、変更させないという意図が明確になって、フィールドが変更される可能性を、たいていは排除できます。
— リファクタリング(第2版)
Rubyでは以下のようにして インスタンス変数 を読み取り専用にします。
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_accessor :type ...ERROR["test_配列の2番目は文字列のFizzを返す", #<Minitest::Reporters::Suite:0x000055b32efd75f0 @name="数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.008614362974185497] test_配列の2番目は文字列のFizzを返す#数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) Minitest::UnexpectedError: NoMethodError: undefined method `list=' for #<FizzBuzz:0x000055b32ee8c678> Did you mean? list /workspace/tdd_rb/test/fizz_buzz_test.rb:45:in `setup'更新メソッドは存在しませんというエラーに変わったことが確認できたのでテストを元にもどします。
同様に インスタンス変数 の
@type
も読み取り専用にします。class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type ...... 04:32:06 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 22 / 22 LOC (100.0%) covered. Started with run options --guard --seed 20902 32/32: [===========================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00920s ...テストが壊れていないことを確認したらコミットします。
$ git add . $ git commit -m 'refactor: setterの削除'ポリモーフィズム
ポリモーフィズムによる条件記述の置き換え 1
リファクタリングによりデータとロジックを1つのクラスにまとめて カプセル化 を進めることが出来ました。しかし、以下の警告メッセージが表示されたままです。ポリモーフィズム を使ったロジックのリファクタリングを実施していきましょう。
... 07:53:29 - INFO - Inspecting Ruby code style: test/fizz_buzz_test.rb lib/fizz_buzz.rb lib/fizz_buzz.rb:11:3: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for generate is too high. [10/8] def generate(number) ... ^^^^^^^^^^^^^^^^^^^^ lib/fizz_buzz.rb:11:3: C: Metrics/PerceivedComplexity: Perceived complexity for generate is too high. [8/7] def generate(number) ... ^^^^^^^^^^^^^^^^^^^^ 2/2 files |====================================== 100 =======================================>| Time: 00:00:00 2 files inspected, 2 offenses detected ...循環的複雑度 が高く可読性が低く複雑なコードと警告されているようです。対象となっている
FizzBuzz#generate
を確認してみましょう。... def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case @type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end ...コードの不吉な臭いである スイッチ文 に該当するコードのようなのでここはリファクタリングカタログに従って ポリモーフィズムによる条件記述の置き換え を適用していきましょう。比較的大きなリファクタリングなのでいくつかのステップに分けて進めていきます。
スイッチ文
オブジェクト指向プログラミングのメリットして、スイッチ文が従来にくらべて少なくなるということがあります。スイッチ文は重複したコードを生み出す問題児です。コードのあちらこちらに同じようなスイッチ文が見られることがあります。これでは新たな分岐を追加したときに、すべてのスイッチ文を探して似たような変更をしていかなければなりません。オブジェクト指向ではポリモーフィズムを使い、この問題をエレガントに解決できます。
— 新装版 リファクタリング
重複したスイッチ文
最近はポリモーフィズムも一般的となり、15年前に比べるとswitch文が単純に赤信号というわけでもなくなりました。また、多くのプログラミング言語が、基本データ型以外をサポートする、より洗練されたswitch文を提供してきています。そこで、今後問題とするのは、重複したswitch文のみとします。switch/case文や、ネストしたif/else文の形で、コードのさまざまな箇所に同じ条件分岐ロジックが書かれていれば、それは「不吉な臭い」です。重複した条件分岐が問題なのは、新たな分岐を追加したら、すべての重複した条件分岐を探して更新指定かなけれけならないからです。ポリモーフィズムは、そうした単調な繰り返しに誘うダークフォースに対抗するための、洗練された武器です。コードベースをよりモダンにしていきましょう。
— リファクタリング(第2版)
ポリモーフィズムによる条件記述の置き換え
オブジェクトのタイプによって異なる振る舞いを選択する条件記述がある。
条件記述の各アクション部をサブクラスでオーバーライドするメソッドに移動する。元のメソッドはabstractにする。
— 新装版 リファクタリング
class FizzBuzz ... end class FizzBuzzType01; end class FizzBuzzType02; end class FizzBuzzType03; endまず、タイプごとのクラスを定義します。
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type end def self.create(type) case type when 1 FizzBuzzType01.new when 2 FizzBuzzType02.new when 3 FizzBuzzType03.new else raise '該当するタイプは存在しません' end ...次に、タイプごとのクラスを インスタンス化 する ファクトリメソッド をFizzBuzzクラスに追加します。この時点では新しいクラスとメソッドの追加だけなのでテストは壊れていないはずです(警告は出ていますが・・・)。ここでコミットしておきますがリファクタリング作業としては 仕掛 なのでWIP(Work In Progress)をメッセージに追加してコミットします。
$ git add . $ git commit -m 'refactor(WIP): ポリモーフィズムによる条件記述の置き換え'ポリモーフィズムによる条件記述の置き換え 2
続いて、各タイプクラスに インスタンスメソッド を実装します。ここでは case式 の各処理をコピー&ペーストしています。カット&ペーストするとプロダクトコードが壊れたままリファクタリングを進めることになるのでここは慎重に進めていきます。
class FizzBuzz ... end class FizzBuzzType01; end class FizzBuzzType02; end class FizzBuzzType03; end... class FizzBuzzType01 def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s end end ...... class FizzBuzzType02 def generate(number) number.to_s end end ...... class FizzBuzzType03 def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz number.to_s end end警告は出ますがテストは壊れていないのでコミットします。
$ git add . $ git commit -m 'refactor(WIP): ポリモーフィズムによる条件記述の置き換え'ポリモーフィズムによる条件記述の置き換え 3
これで準備は整いましたのでテストコードの
setup
メソッドを ファクトリメソッド の呼び出しに変更します。以下の部分は変更してはいけません。理由はわかりますか?... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list @result = fizzbuzz.list end ...... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.create(1) end ... describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.create(2) end ... describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.create(3) end ... describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz.create(4) end def test_例外を返す e = assert_raises RuntimeError do @fizzbuzz.generate(1) end assert_equal '該当するタイプは存在しません', e.message end end end... 08:14:14 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 26 / 42 LOC (61.9%) covered. Started with run options --guard --seed 37585 ERROR["test_例外を返す", #<Minitest::Reporters::Suite:0x000056317940fa28 @name="数を文字列にして返す::それ以外のタイプの場合">, 0.0037079370085848495] test_例外を返す#数を文字列にして返す::それ以外のタイプの場合 (0.00s) Minitest::UnexpectedError: RuntimeError: 該当するタイプは存在しません /workspace/tdd_rb/lib/fizz_buzz.rb:20:in `create' /workspace/tdd_rb/test/fizz_buzz_test.rb:132:in `setup' 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00685s 32 tests, 33 assertions, 0 failures, 1 errors, 0 skips ...失敗するテストがありますね、該当するコードを確認したところ例外が発生するタイミングが変わってしまったので以下のように変更します。
... describe 'それ以外のタイプの場合' do def setup @fizzbuzz = FizzBuzz.create(4) end def test_例外を返す e = assert_raises RuntimeError do @fizzbuzz.generate(1) end assert_equal '該当するタイプは存在しません', e.message end end ...... describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzz.create(4) end assert_equal '該当するタイプは存在しません', e.message end end ...... 08:18:08 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 37 / 42 LOC (88.1%) covered. Started with run options --guard --seed 40171 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00559s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips ...コミットしておきましょう。
$ git add . $ git commit -m 'refactor(WIP): ポリモーフィズムによる条件記述の置き換え'ポリモーフィズムによる条件記述の置き換え 4
タイプごとにFizzBuzzを生成するクラスを用意したのでFizzBuzzクラスから呼び出せるようにしましょう。
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type end ... def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end endまず、コンストラクタ から クラスメソッド の ファクトリメソッド を呼び出して インスタンス変数 の
type
にタイプクラスの 参照 を 代入 します。class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = FizzBuzz.create(type) end ... def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end endERROR["test_配列の14番目は文字列のFizzBuzzを返す", #<Minitest::Reporters::Suite:0x000055670a343110 @name="数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.006740843993611634] test_配列の14番目は文字列のFizzBuzzを返す#数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) Minitest::UnexpectedError: RuntimeError: 該当するタイプは存在しません /workspace/tdd_rb/lib/fizz_buzz.rb:42:in `generate' /workspace/tdd_rb/lib/fizz_buzz.rb:48:in `block in generate_list' /workspace/tdd_rb/lib/fizz_buzz.rb:48:in `each' /workspace/tdd_rb/lib/fizz_buzz.rb:48:in `map' /workspace/tdd_rb/lib/fizz_buzz.rb:48:in `generate_list' /workspace/tdd_rb/test/fizz_buzz_test.rb:44:in `setup'テストが失敗して沢山エラーが表示するようになりましたが落ち着いてください。次に インスタンスメソッド
FizzBuzz#generate_list
内のFizzBuzz#generate
メソッド呼び出しを インスタンス変数type
が参照するタイプクラスのメソッドFizzBuzzTypeXX#generate
を呼び出すように変更します。class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = FizzBuzz.create(type) end ... def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| @type.generate(n) } end endStarted with run options --seed 13878 Progress: |=====================================================================================================| Finished in 0.00960s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 05:54:49 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb lib/fizz_buzz.rb:24:3: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for generate is too high. [10/8] def generate(number) ... ^^^^^^^^^^^^^^^^^^^^ lib/fizz_buzz.rb:24:3: C: Metrics/PerceivedComplexity: Perceived complexity for generate is too high. [8/7] def generate(number) ... ^^^^^^^^^^^^^^^^^^^^ 1/1 file |======================================= 100 ========================================>| Time: 00:00:00 1 file inspected, 2 offenses detected再びテストが通るようになりました。始めのうちはコードを少し変更しただけでなんで動くようになったの?と思うかもしれませんがこれが ポリモーフィズム の威力です。この概念を感覚としてつかんで使いこなせるようになることがオブジェクト指向プログラミングの第一歩です。感覚は意識して手を動かしていればそのうちつかめます(多分)。
ポリモーフィズムによる条件記述の置き換え が完了したのでWIPを外してコミットします。
$ git add . $ git commit -m 'refactor ポリモーフィズムによる条件記述の置き換え'State/Strategyによるタイプコードの置き換え
仕上げは State/Strategyによるタイプコードの置き換え を適用して、警告メッセージを消すとしましょう。
State/Strategyによるタイプコードの置き換え
クラスの振る舞いに影響するタイプコードがあるが、サブクラス化はできない。
状態オブジェクトでタイプコードを置き換える
— 新装版 リファクタリング
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = FizzBuzz.create(type) end def self.create(type) case type when 1 FizzBuzzType01.new when 2 FizzBuzzType02.new when 3 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? case @type when 1 return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s when 2 number.to_s when 3 return 'FizzBuzz' if is_fizz && is_buzz number.to_s else raise '該当するタイプは存在しません' end end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| @type.generate(n) } end end ...まず、
FizzBuzz#generate
のメソッド呼び出しを インスタンス変数type
が参照するタイプクラスのメソッドFizzBuzzTypeXX#generate
に 委譲 するように変更します。... def generate(number) @type.generate(number) end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| @type.generate(n) } end end ...... Started with run options --seed 49543 Progress: |=====================================================================================================| Finished in 0.00925s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 06:34:27 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |======================================= 100 ========================================>| Time: 00:00:00 1 file inspected, no offenses detected 06:34:29 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |======================================= 100 =======================================>| Time: 00:00:00 0 files inspected, no offenses detecte ...警告が消えました。しかもテストは壊れていないようです。実は
FizzBuzz#generate
メソッドはどこからも使われていないためテストも壊れることが無いのですがこれでは不要なメソッドになってしまうので 移譲の隠蔽 を実施して、ロジックを カプセル化 します。委譲の隠蔽
オブジェクト指向について最初に教わる時、カプセル化とはフィールドを隠すことだと習うでしょう。しかし経験を積むにつれて、他にもカプセル化できるものがあることに気づきます。
— リファクタリング(第2版)
... def generate(number) @type.generate(number) end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end end ...テストもFizzBuzzインスタンス経由で実行するように修正しておきます。これですべての呼び出しが
new
メソッド経由となりテストコードに一貫性を取り戻すことが出来ました。... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new(1) end ... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list @result = fizzbuzz.list end ... describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(2) end ... describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(3) end ... describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzz.new(4) end assert_equal '該当するタイプは存在しません', e.message end end end ...... 08:32:17 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 32 / 32 LOC (100.0%) covered. Started with run options --guard --seed 63863 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00564s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:32:18 - INFO - Inspecting Ruby code style of all files 7/7 files |====================================== 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detected ...ポリモーフィズム の感覚がつかめないうちは
FizzBuzz#generate
のコードが一行になったのに既存のテストも壊れず動いていることが不思議に思うかもしれません。しかしコードとしてはFizzBuzzクラスのgenerate
メソッドは任意のタイプクラスのgenerate
メソッドを呼び出しているだけで処理の詳細は理解しなくても振る舞いを理解できる 抽象化 された読みやすいコードになりました。静的コード解析も可読性が高くシンプルなコードとみなしてくれているようです。さて、警告メッセージもなくなり、テストも壊れていないのでコミットしておきましょう。$ git add . $ git commit -m 'refactor: State/Strategyによるタイプコードの置き換え'継承
分割したタイプクラスのメソッドに重複する処理があるので 継承 を使ってリファクタリングしましょう。ここでは スーパークラスの抽出を適用します。
スーパークラスの抽出
似通った特性を持つ2つのクラスがある。
スーパークラスを作成して、共通の特性を移動する。
— 新装版 リファクタリング
スーパークラスの抽出
まずは、タイプクラスのスーパークラスとなる
FizzBuzzType
クラスを作成して各タイプクラスに継承させます。クラスベースのオブジェクト指向言語の多くはクラスの継承機能を有しています。クラスの継承とはあるクラスを元として、新しいクラスを定義することです。この時、継承元となるクラスを親クラスやスーパークラスと呼び、継承したクラスのことを子クラスやサブクラスと呼びます。
— かんたんRuby
Rubyの クラスの継承 は以下のように書きます。
class FizzBuzz ... end class FizzBuzzType; end class FizzBuzzType01 ...... class FizzBuzzType; end class FizzBuzzType01 < FizzBuzzType ... end class FizzBuzzType02 < FizzBuzzType ... end class FizzBuzzType03 < FizzBuzzType ... endスーパークラス
FizzBuzzType
を定義して各サブクラスに継承させます。08:42:24 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 33 / 33 LOC (100.0%) covered. Started with run options --guard --seed 43548 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00860s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:42:25 - INFO - Inspecting Ruby code style of all files 7/7 files |====================================== 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detected次に
is_fizz
is_buzz
部分を共通メソッドとしてスーパークラスに定義して各タイプクラスで呼び出すように変更します。... class FizzBuzzType; end class FizzBuzzType01 < FizzBuzzType def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz return 'Fizz' if is_fizz return 'Buzz' if is_buzz number.to_s end end class FizzBuzzType02 < FizzBuzzType def generate(number) number.to_s end end class FizzBuzzType03 < FizzBuzzType def generate(number) is_fizz = number.modulo(3).zero? is_buzz = number.modulo(5).zero? return 'FizzBuzz' if is_fizz && is_buzz number.to_s end end... class FizzBuzzType def is_fizz(number) number.modulo(3).zero? end def is_buzz(number) number.modulo(5).zero? end end class FizzBuzzType01 < FizzBuzzType def generate(number) return 'FizzBuzz' if is_fizz(number) && is_buzz(number) return 'Fizz' if is_fizz(number) return 'Buzz' if is_buzz(number) number.to_s end end class FizzBuzzType02 < FizzBuzzType def generate(number) number.to_s end end class FizzBuzzType03 < FizzBuzzType def generate(number) return 'FizzBuzz' if is_fizz(number) && is_buzz(number) number.to_s end end08:50:16 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 33 / 33 LOC (100.0%) covered. Started with run options --guard --seed 45685 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01073s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:50:17 - INFO - Inspecting Ruby code style of all files lib/fizz_buzz.rb:35:7: C: Naming/PredicateName: Rename is_fizz to fizz?. def is_fizz(number) ^^^^^^^ lib/fizz_buzz.rb:39:7: C: Naming/PredicateName: Rename is_buzz to buzz?. def is_buzz(number) ^^^^^^^ 7/7 files |====================================== 100 =======================================>| Time: 00:00:00 7 files inspected, 2 offenses detectedテストが壊れていないことが確認できたのでコミットしておきます。
$ git add . $ git commit -m 'refactor: スーパークラスの抽出'メソッド名の変更
スーパークラスの抽出 を実施したところまた警告メッセージが表示されるようになりました。
08:50:19 - INFO - Inspecting Ruby code styl e: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png lib/fizz_buzz.rb lib/fizz_buzz.rb:35:7: C: Naming/PredicateName: Rename is_fizz to fizz?. def is_fizz(number) ^^^^^^^ lib/fizz_buzz.rb:39:7: C: Naming/PredicateName: Rename is_buzz to buzz?. def is_buzz(number) ^^^^^^^ 1/1 file |======================================= 100 =======================================>| Time: 00:00:00 1 file inspected, 2 offenses detectedNaming/PredicateName Rubyのネーミングとしてはよろしくないようなので指示に従って メソッド名の変更 を実施しましょう。
... class FizzBuzzType def is_fizz(number) number.modulo(3).zero? end def is_buzz(number) number.modulo(5).zero? end end class FizzBuzzType01 < FizzBuzzType def generate(number) return 'FizzBuzz' if is_fizz(number) && is_buzz(number) return 'Fizz' if is_fizz(number) return 'Buzz' if is_buzz(number) number.to_s end end class FizzBuzzType02 < FizzBuzzType def generate(number) number.to_s end end class FizzBuzzType03 < FizzBuzzType def generate(number) return 'FizzBuzz' if is_fizz(number) && is_buzz(number) number.to_s end end... class FizzBuzzType def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end class FizzBuzzType01 < FizzBuzzType def generate(number) return 'FizzBuzz' if fizz?(number) && buzz?(number) return 'Fizz' if fizz?(number) return 'Buzz' if buzz?(number) number.to_s end end class FizzBuzzType02 < FizzBuzzType def generate(number) number.to_s end end class FizzBuzzType03 < FizzBuzzType def generate(number) return 'FizzBuzz' if fizz?(number) && buzz?(number) number.to_s end endProgress: |====================================================================================================| Finished in 0.01144s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:53:35 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |======================================= 100 =======================================>| Time: 00:00:00 1 file inspected, no offenses detected作業としては難しくないのでミスタイプしないように(まあ、ミスタイプしてもテストが教えてくれますが・・・)変更してコミットしましょう。
$ git add . $ git commit -m 'refactor: メソッド名の変更'メソッドの移動
FizzBuzz
クラスの ファクトリメソッド ですが 特性の横恋慕 の臭いがするので メソッドの移動 を実施します。特性の横恋慕
オブジェクト指向には、処理および処理に必要なデータを1つにまとめてしまうという重要な考え方があります。あるメソッドが、自分のクラスより他のクラスに興味を持つような場合には、古典的な誤りを犯しています。
— 新装版 リファクタリング
メソッドの移動
あるクラスでメソッドが定義されているが、現在または将来において、そのクラスの特性よりも他のクラスの特性の方が、そのメソッドを使ったり、そのメソッドから使われたりすることが多い。
同様の本体を持つ新たなメソッドを、それを最も多用するクラスに作成する。元のメソッドは、単純な委譲とするか、またはまるごと取り除く。
— 新装版 リファクタリング
class FizzBuzz MAX_NUMBER = 100 attr_reader :list def initialize(type) @type = FizzBuzz.create(type) end def self.create(type) case type when 1 FizzBuzzType01.new when 2 FizzBuzzType02.new when 3 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end def generate(number) @type.generate(number) end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end end class FizzBuzzType def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end ...クラスメソッド
FizzBuzz.create
をカット&ペーストしてFizzBuzzType.create
に移動します。FizzBuzz
の コンストラクタ で呼び出している クラスメソッド をFizzBuzzType.create
に変更します。class FizzBuzz MAX_NUMBER = 100 attr_reader :list def initialize(type) @type = FizzBuzzType.create(type) end def generate(number) @type.generate(number) end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end end class FizzBuzzType def self.create(type) case type when 1 FizzBuzzType01.new when 2 FizzBuzzType02.new when 3 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end ...08:59:27 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 33 / 33 LOC (100.0%) covered. Started with run options --guard --seed 19583 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00688s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:59:28 - INFO - Inspecting Ruby code style of all files 7/7 files |====================================== 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detectedテストが壊れていないことを確認したらコミットします。
$ git add . $ git commit -m 'refactor: メソッドの移動'値オブジェクト
オブジェクトによるプリミティブの置き換え
FizzBuzz
クラスを インスタンス化 するには以下のように書きます。fizz_buzz = FizzBuzz.new(1)クラスとして定義された情報を元に具体的な値を伴ったオブジェクトを作成することをインスタンス化と呼び、生成されたオブジェクトのことをインスタンスと呼びます。
— かんたんRuby
コンストラクタ の 引数 に渡される
1
は何を表しているのでしょうか?もちろんタイプですが初めてこのコードを見る人にはわからないでしょう。このような整数、浮動小数点、文字列などの基本データ(プリミティブ)型の使い方からは 基本データ型への執着の臭いがします。 オブジェクトによるプリミティブの置き換え を実施してコードの意図を明確にしましょう。基本データ型への執着
オブジェクト指向のメリットとして、基本データ型とそれより大きなクラスとの境界を取り除くということがあります。プログラミング言語の組み込み(built-in)型と区別できないような小さなクラスを自分で定義することが容易です。
— 新装版 リファクタリング
基本データ型への執着
興味深いことに、多くのプログラマは、対象としているドメインに役立つ、貨幣、座標、範囲などの基本的な型を導入するのを嫌がる傾向があります。
— リファクタリング(第2版)
オブジェクトによるデータ値の置き換え
追加のデータや振る舞いが必要なデータ項目がある。
そのデータ項目をオブジェクトに変える。
— 新装版 リファクタリング
オブジェクトによるプリミティブの置き換え
旧:オブジェクトによるデータ値の置き換え
旧:クラスによるタイプコードの置き換え
— リファクタリング(第2版)
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = FizzBuzzType.create(type) end ...class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type end ...コンストラクタ で引き渡されるタイプは整数ではなくタイプクラスの インスタンス に変更します。
... ERROR["test_1を渡したら文字列1を返す", #<Minitest::Reporters::Suite:0x00005654f32602c0 @name="数を文字列にして返す::タイプ3の場合::その他の場合">, 0.00241121300496161] test_1を渡したら文字列1を返す#数を文字列にして返す::タイプ3の場合::その他の場合 (0.00s) Minitest::UnexpectedError: NoMethodError: undefined method `generate' for 3:Integer /workspace/tdd_rb/lib/fizz_buzz.rb:12:in `generate' /workspace/tdd_rb/test/fizz_buzz_test.rb:125:in `test_1を渡したら文字列1を返す' ...テストが失敗しました。 コンストラクタ の引数を整数からタイプクラスの インスタンス に変更します。
... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new(1) end ... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(1) fizzbuzz.generate_list @result = fizzbuzz.list end ... describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(2) end ... describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(3) end ... describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzz.new(4) end assert_equal '該当するタイプは存在しません', e.message end end endここで注意するのは
それ以外のタイプの場合
ですが例外を投げなくなります。静的に型付けされた言語なら型チェックエラーになるのですがRubyは動的に型付けされる言語のためFizzBuzz#generate
メソッド実行までエラーになりません。そこで例外を投げるFizzBuzzType#create
メソッドに変更しておきます。class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) end ... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) fizzbuzz.generate_list @result = fizzbuzz.list end ... describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType02.new) end ... describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType03.new) end ... describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzzType.create(4) end assert_equal '該当するタイプは存在しません', e.message end end endそれ以外のタイプの場合は ファクトリメソッド 経由でないと 例外 を出さなくなるので注意してください。
09:09:40 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 30 / 33 LOC (90.91%) covered. Started with run options --guard --seed 17452 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00687s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips初めてコードを見る人でもテストコードを見ればコードの意図が読み取れるようになりましたのでコミットします。
$ git add . $ git commit -m 'refactor: オブジェクトによるプリミティブの置き換え'マジックナンバーの置き換え
まだプリミティグ型を使っている部分があります。ここは マジックナンバーの置き換え を実施して可読性を上げておきましょう。
... class FizzBuzzType def self.create(type) case type when 1 FizzBuzzType01.new when 2 FizzBuzzType02.new when 3 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end ...... class FizzBuzzType TYPE_01 = 1 TYPE_02 = 2 TYPE_03 = 3 def self.create(type) case type when FizzBuzzType::TYPE_01 FizzBuzzType01.new when FizzBuzzType::TYPE_02 FizzBuzzType02.new when FizzBuzzType::TYPE_03 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end ...09:18:51 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 33 / 36 LOC (91.67%) covered. Started with run options --guard --seed 41124 32/32: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00909s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skipsテストは壊れていないのでコミットします。
$ git add . $ git commit -m 'refactor: マジックナンバーの置き換え'オブジェクトによるプリミティブの置き換え
次に 基本データ型への執着 の臭いがする箇所として
FizzBuzz#generate
メソッドが返すFizzBuzzの値が文字型である点です。文字列の代わりに 値オブジェクトFizzBuzzValue
クラスを定義します。値の種類ごとに専用の型を用意するとコードが安定し、コードの意図が明確になります。このように、値を扱うための専用クラスを作るやり方を値オブジェクト(ValueObject)と呼びます。
— 現場で役立つシステム設計の原則
... class FizzBuzzValue attr_reader :number, :value def initialize(number, value) @number = number @value = value end def to_s "#{@number}:#{@value}" end def ==(other) @number == other.number && @value == other.value end alias eql? == end各タイプクラスの
generate
メソッドが文字列のプリミティブ型を返しているので 値オブジェクトFizzBuzzValue
を返すように変更します。... class FizzBuzzType01 < FizzBuzzType def generate(number) return 'FizzBuzz' if fizz?(number) && buzz?(number) return 'Fizz' if fizz?(number) return 'Buzz' if buzz?(number) number.to_s end end class FizzBuzzType02 < FizzBuzzType def generate(number) number.to_s end end class FizzBuzzType03 < FizzBuzzType def generate(number) return 'FizzBuzz' if fizz?(number) && buzz?(number) number.to_s end end ...... class FizzBuzzType01 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) return FizzBuzzValue.new(number, 'Fizz') if fizz?(number) return FizzBuzzValue.new(number, 'Buzz') if buzz?(number) FizzBuzzValue.new(number, number.to_s) end end class FizzBuzzType02 < FizzBuzzType def generate(number) FizzBuzzValue.new(number, number.to_s) end end class FizzBuzzType03 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) FizzBuzzValue.new(number, number.to_s) end end ...... FAIL["test_配列の2番目は文字列のFizzを返す", #<Minitest::Reporters::Suite:0x000055feccc65ab8 @name="数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.012104410998290405] test_配列の2番目は文字列のFizzを返す#数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) --- expected +++ actual @@ -1 +1 @@ -"Fizz" +#<FizzBuzzValue:0xXXXXXX @number=3, @value="Fizz"> /workspace/tdd_rb/test/fizz_buzz_test.rb:57:in `test_配列の2番目は文字列のFizzを返す' ...変更によりテストが失敗しました。エラー内容を見てみると文字列からオブジェクトを返しているためアサーションが失敗しているようです。ここは、値オブジェクト の アクセッサメソッド を経由して取得した値をアサーション対象に変更しましょう。
... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) end describe '三の倍数の場合' do def test_3を渡したら文字列Fizzを返す assert_equal 'Fizz', @fizzbuzz.generate(3).value end end describe '五の倍数の場合' do def test_5を渡したら文字列Buzzを返す assert_equal 'Buzz', @fizzbuzz.generate(5).value end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.generate(15).value end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1).value end end describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) fizzbuzz.generate_list @result = fizzbuzz.list end def test_配列の初めは文字列の1を返す assert_equal '1', @result.first.value end def test_配列の最後は文字列のBuzzを返す assert_equal 'Buzz', @result.last.value end def test_配列の2番目は文字列のFizzを返す assert_equal 'Fizz', @result[2].value end def test_配列の4番目は文字列のBuzzを返す assert_equal 'Buzz', @result[4].value end def test_配列の14番目は文字列のFizzBuzzを返す assert_equal 'FizzBuzz', @result[14].value end end end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType02.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3).value end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5).value end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.generate(15).value end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1).value end end end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType03.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.generate(3).value end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.generate(5).value end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.generate(15).value end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.generate(1).value end end end describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzzType.create(4) end assert_equal '該当するタイプは存在しません', e.message end end end ...08:49:28 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 41 / 46 LOC (89.13%) covered. Started with run options --guard --seed 25972 32/32: [==================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00619s 32 tests, 35 assertions, 0 failures, 0 errors, 0 skips 08:49:29 - INFO - Inspecting Ruby code style of all files 7/7 files |======================================= 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detected 08:49:30 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |======================================= 100 =======================================>| Time: 00:00:00 0 files inspected, no offenses detectedテストコードをそれほど変更することなく 値オブジェクト を返すリファクタリングが出来ました。コミットしておきましょう。
$ git add . $ git commit -m 'refactor: オブジェクトによるプリミティブの置き換え'学習用テスト
値オブジェクト の理解を深めるために 学習用テスト を追加します。
... describe 'FizzBuzzValue' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) end def test_同じで値である value1 = @fizzbuzz.generate(1) value2 = @fizzbuzz.generate(1) assert value1.eql?(value2) end def test_to_stringメソッド value = @fizzbuzz.generate(3) assert_equal '3:Fizz', value.to_s end end end$ git add . $ git commit -m 'test: 学習用テスト'ファーストクラスコレクション
コレクションのカプセル化
値オブジェクト を扱うFizzBuzzリストですが コレクションのカプセル化 を適用して ファーストクラスコレクション オブジェクトを追加しましょう。
コレクションのカプセル化
メソッドがコレクションを返している。
読み取り専用のビューを返して、追加と削除のメソッドを提供する。
— 新装版 リファクタリング
このように、コレクション型のデータとロジックを特別扱いにして、コレクションを1つだけ持つ専用クラスを作るやり方をコレクションオブジェクトあるいはファーストクラスコレクションと呼びます。
— 現場で役立つシステム設計の原則
まず、 ファーストクラスコレクション クラスを追加します。
... class FizzBuzzList attr_reader :value def initialize(list) @value = list end def to_s @value.to_s end def add(value) FizzBuzzList.new(@value + value) end endFizzBuzz配列を ファーストクラスコレクション から取得するように変更します。
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type end ... def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = (1..MAX_NUMBER).map { |n| generate(n) } end endclass FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type @list = FizzBuzzList.new([]) end ... def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = @list.add((1..MAX_NUMBER).map { |n| @type.generate(n) }) end endなんだか紛らわしい書き方になってしましました。配列を作るのに以前の配列を元に新しい配列を作るとか回りくどいことをしないで既存の配列を使い回せばいいじゃんと思うかもしれませんが 変更可能なデータ はバグの原因となる傾向があります。変更可能な ミュータブル な変数ではなく 永続的に変更されない イミュータブル な変数を使うように心がけましょう。
変更可能なデータ
データの変更はしばし予期せぬ結果結果や、厄介なバグを引き起こします。他で違う値を期待していることに気づかないままに、ソフトウェアのある箇所で値を変更してしまえば、それだけで動かなくなってしまいます。これは値が変わる条件がまれにしかない場合、特に見つけにくいバグとなります。そのため、ソフトウェア開発の一つの潮流である関数型プログラミングは、データは不変であるべきで、更新時は常に元にデータ構造のコピーを返すようにし、元データには手を触れないという思想に基づいています。
— リファクタリング(第2版)
値オブジェクトと同じようにコレクションオブジェクトも、できるだけ「不変」スタイルで設計します。そのほうがプログラムが安定します。
— 現場で役立つシステム設計の原則
... ERROR["test_配列の14番目は文字列のFizzBuzzを返す", #<Minitest::Reporters::Suite:0x00005561331b7940 @name="FizzBuzz::数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.011710233025951311] test_配列の14番目は文字列のFizzBuzzを返す#FizzBuzz::数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) Minitest::UnexpectedError: NoMethodError: undefined method `[]' for #<FizzBuzzList:0x0000556133198ba8 @value=[]> /workspace/tdd_rb/test/fizz_buzz_test.rb:66:in `test_配列の14番目は文字列のFizzBuzzを返す' ...ファーストクラスコレクション 経由で取得するようになったので アクセッサメソッド を変更する必要があります。
class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type end ...class FizzBuzz MAX_NUMBER = 100 attr_reader :list attr_reader :type def initialize(type) @type = type @list = FizzBuzzList.new([]) end ...class FizzBuzz MAX_NUMBER = 100 attr_reader :type def list @list.value end def initialize(type) @type = type @list = FizzBuzzList.new([]) end ....09:12:46 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 53 / 56 LOC (94.64%) covered. Started with run options --guard --seed 61051 34/34: [==================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01285s 34 tests, 37 assertions, 0 failures, 0 errors, 0 skips 09:12:47 - INFO - Inspecting Ruby code style of all files 7/7 files |======================================= 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detected 09:12:48 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |======================================= 100 =======================================>| Time: 00:00:00 0 files inspected, no offenses detectedテストが直ったのでコミットしておきます。
$ git add . $ git commit -m 'refactor: コレクションのカプセル化'学習用テスト
ファーストクラスコレクション を理解するため 学習用テスト を追加しておきましょう。
... describe 'FizzBuzzValueList' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) end def test_新しいインスタンスが作られる list1 = @fizzbuzz.generate_list list2 = list1.add(list1.value) assert_equal 100, list1.value.count assert_equal 200, list2.value.count end end end$ git add . $ git commit -m 'refactor: 学習用テスト'オブジェクト指向設計
値オブジェクト 及び ファーストクラスコレクション の適用で 基本データ型への執着 の臭いはなくなりました。今度は設計の観点から全体を眺めてみましょう。ここで気になるのが
FizzBuzz
クラスです。このクラスは他のクラスと比べてやることが多いようです。このようなクラスは 単一責任の原則 に違反している可能性があります。そこで デザインパターン の1つである Commandパターン を使ったリファクタリングである メソッドオブジェクトによるメソッドの置き換え 適用してみようと思います。SRP:
単一責任の原則かつて単一責任の原則(SRP)は、以下のように語られてきた。
モジュールを変更する理由はたったひとつだけであるべきであるソフトウェアシステムに手を加えるのは、ユーザーやステークホルダーを満足させるためだ。この「ユーザーやステークホルダー」こそが、単一責任の原則(SRP)を指す「変更する理由」である。つまり、この原則は以下のように言い換えられる。
モジュールはたったひとりのユーザーやステークホルダーに対して責任を負うべきである。残念ながら「たったひとりのユーザーやステークホルダー」という表現は適切ではない。複数のユーザーやステークホルダーがシステムを同じように変更したいと考えることもある。ここでは、変更を望む人たちをひとまとめにしたグループとして扱いたい。このグループのことをアクターと呼ぶことにしよう。
これを踏まえると、最終的な単一責任の原則(SRP)は以下のようになる。モジュールはたったひとつのアクターに対して責任を負うべきである。さて、ここでいう「モジュール」とは何のことだろう?端的に言えば、モジュールとはソースファイルのことである。たいていの場合は、この定義で問題ないだろう。だが、ソースファイル以外のところにコードを格納する言語や開発環境も存在する。そのような場合の「モジュール」は、いくつかの関数やデータをまとめた凝集性のあるものだと考えよう。
「凝集性のある」という言葉が単一責任の原則(SRP)を匂わせる。凝集性が、ひとつのアクターに対する責務を負うコードをまとめるフォースとなる。
— Clean Architecture 達人に学ぶソフトウェアの構造と設計
Commandパターン
処理の呼び出しが、シンプルなメソッド呼び出しよりも複雑になってきたときはどうすればよいだろうか---処理のためのオブジェクトを作成し、それを起動するようにしよう。
— テスト駆動開発
メソッドオブジェクトによるメソッドの置き換え
長いメソッドで、「メソッドの抽出」を適用できないようなローカル変数の使い方をしている。
メソッド自身をオブジェクトとし、すべてのローカル変数をそのオブジェクトのフィールドとする。そうすれば、そのメソッドを同じオブジェクト中のメソッド群に分解できる。
— 新装版 リファクタリング
メソッドオブジェクトによるメソッドの置き換え
まず、値オブジェクト の
FizzBuzzValue
を返す責務だけを持った メソッドオブジェクト を抽出します。Rubyのような動的言語では必要が無いのですが Commandパターン の説明のため インターフェイス にあたるスーパークラスを継承した メソッドオブジェクト を定義します。... class FizzBuzzCommand def execute; end end class FizzBuzzValueCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) @type.generate(number).value end endテストコードを
FizzBuzzValueCommand
を呼び出すように変更します。... class FizzBuzzTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType01.new) end describe '三の倍数の場合' do def test_3を渡したら文字列Fizzを返す assert_equal 'Fizz', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列Buzzを返す assert_equal 'Buzz', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) fizzbuzz.generate_list @result = fizzbuzz.list end def test_配列の初めは文字列の1を返す assert_equal '1', @result.first.value end def test_配列の最後は文字列のBuzzを返す assert_equal 'Buzz', @result.last.value end def test_配列の2番目は文字列のFizzを返す assert_equal 'Fizz', @result[2].value end def test_配列の4番目は文字列のBuzzを返す assert_equal 'Buzz', @result[4].value end def test_配列の14番目は文字列のFizzBuzzを返す assert_equal 'FizzBuzz', @result[14].value end end end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType02.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType03.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzzType.create(4) end assert_equal '該当するタイプは存在しません', e.message end end end ...... 09:56:19 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 60 / 63 LOC (95.24%) covered. Started with run options --guard --seed 27353 35/35: [==================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00692s 35 tests, 39 assertions, 0 failures, 0 errors, 0 skips 09:56:20 - INFO - Inspecting Ruby code style of all files 7/7 files |======================================= 100 =======================================>| Time: 00:00:00 7 files inspected, no offenses detected 09:56:21 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/loading_background.png coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/border.png 0/0 files |======================================= 100 =======================================>| Time: 00:00:00 ...
FizzBuzzValueCommand
の抽出ができたのでコミットしておきます。$ git add . $ git commit -m 'refactor: メソッドオブジェクトによるメソッドの置き換え'メソッドオブジェクトによるメソッドの置き換え
続いて、ファーストクラスコレクション を扱う
FizzBuzzList
を返す責務だけを持った メソッドオブジェクト を抽出します。... class FizzBuzzListCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) FizzBuzzList.new((1..number).map { |i| @type.generate(i) }).value end endテストコードを FizzBuzzListCommand 経由から実行するように変更します
... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzz.new(FizzBuzzType01.new) fizzbuzz.generate_list @result = fizzbuzz.list end ...... describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzzListCommand.new(FizzBuzzType01.new) @result = fizzbuzz.execute(100) end ...01:27:54 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 61 / 66 LOC (92.42%) covered. Started with run options --guard --seed 62253 35/35: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00652s 35 tests, 39 assertions, 0 failures, 0 errors, 0 skipsテストが通ったのでコミットします。
$ git add . $ git commit -m 'refactor: メソッドオブジェクトによるメソッドの置き換え'デッドコードの削除
FizzBuzz
クラスの責務は各 メソッドオブジェクト が実行するようになったので削除しましょう。class FizzBuzz MAX_NUMBER = 100 def initialize(type) @type = type @list = FizzBuzzList.new([]) end def list @list.value end def generate(number) @type.generate(number) end def generate_list # 1から最大値までのFizzBuzz配列を1発で作る @list = @list.add((1..MAX_NUMBER).map { |n| @type.generate(n) }) end end class FizzBuzzType ...class FizzBuzzType ...... ERROR["test_同じで値である", #<Minitest::Reporters::Suite:0x0000562fd34f7848 @name="FizzBuzzValue">, 0.008059715997660533] test_同じで値である#FizzBuzzValue (0.01s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz /workspace/tdd_rb/test/fizz_buzz_test.rb:225:in `setup' ERROR["test_to_stringメソッド", #<Minitest::Reporters::Suite:0x0000562fd37694a0 @name="FizzBuzzValue">, 0.01728590900893323] test_to_stringメソッド#FizzBuzzValue (0.02s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz /workspace/tdd_rb/test/fizz_buzz_test.rb:225:in `setup' ERROR["test_新しいインスタンスが作られる", #<Minitest::Reporters::Suite:0x0000562fd39be070 @name="FizzBuzzValueList">, 0.028008958004647866] test_新しいインスタンスが作られる#FizzBuzzValueList (0.03s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::FizzBuzz /workspace/tdd_rb/test/fizz_buzz_test.rb:244:in `setup' ========================================| Finished in 0.03539s 35 tests, 35 assertions, 0 failures, 3 errors, 0 skips ...テストが失敗しました。これは 学習用テスト で
FizzBuzz
クラスを使っている箇所があるからですね。 メソッドオブジェクト 呼び出しに変更しておきましょう。describe 'FizzBuzzValue' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) end def test_同じで値である value1 = @fizzbuzz.generate(1) value2 = @fizzbuzz.generate(1) assert value1.eql?(value2) end def test_to_stringメソッド value = @fizzbuzz.generate(3) assert_equal '3:Fizz', value.to_s end end describe 'FizzBuzzValueList' do def setup @fizzbuzz = FizzBuzz.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) end def test_新しいインスタンスが作られる list1 = @fizzbuzz.generate_list list2 = list1.add(list1.value) assert_equal 100, list1.value.count assert_equal 200, list2.value.count end end end... describe 'FizzBuzzValue' do def test_同じで値である value1 = FizzBuzzValue.new(1, '1') value2 = FizzBuzzValue.new(1, '1') assert value1.eql?(value2) end def test_to_stringメソッド value = FizzBuzzValue.new(3, 'Fizz') assert_equal '3:Fizz', value.to_s end end describe 'FizzBuzzValueList' do def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(100) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 100, list1.value.count assert_equal 200, list2.value.count end end end... 01:35:22 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 50 / 56 LOC (89.29%) covered. Started with run options --guard --seed 10411 35/35: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00704s 35 tests, 39 assertions, 0 failures, 0 errors, 0 skips ...不要なコードを残しておくとメンテナンスの時に削除していいのかわからなくなり可読性を落とし原因となります。削除できる時に削除しておきましょう。後で必要になったとしてもバージョン管理システムを使えば問題ありません。ということでコミットします。
デッドコードの削除
コードが使用されなくなったら削除すべきです。そのコードが将来必要になるかもしれないなどという心配はしません。必要になったらいつでも、バージョン管理システムから再び掘り起こせるからです。
(中略)
デッドコードのコメントアウトは、かつては一般的な習慣でした。それは、バージョン管理システムが広く使用される以前の時代や、使いづらかった時代には有用でした。現在では、とても小さなコードベースでもバージョン管理システムに置けるため、もはや必要のない習慣です。
— リファクタリング(第2版)
$ git add . $ git commit -m 'refactor: デッドコードの削除'デザインパターン
メソッドオブジェクトによるメソッドの置き換え リファクタリングの結果として Commandパターン という デザインパターン を適用しました。実はこれまでにも オブジェクトによるプリミティブの置き換え では Value Objectパターン を ポリモーフィズムによる条件記述の置き換え では Factory Methodパターン をそして、 委譲の隠蔽 の実施による State/Strategyによるタイプコードの置き換え では Strategyパターン を適用しています。
Value Objectパターン
広く共有されるものの、同一インスタンスであることはさほど重要でないオブジェクトを設計するにはどうしたらよいだろうか----オブジェクト作成時に状態を設定したら、その後決して変えないようにする。オブジェクトへの操作は必ず新しいオブジェクトを返すようにしよう。
— テスト駆動開発
Factory Methodパターン
オブジェクト作成に柔軟性をもたせたいときは、どうすればよいだろうか---単にコンストラクタで作るのではなく、メソッドを使ってオブジェクトを作成しよう。
— テスト駆動開発
作成したコードはパターンと完全に一致しているわけではありませんし、Rubyのような動的言語ではもっと簡単な実現方法もありますがここでは先人の考えた設計パターンというものがありオブジェクト指向設計の イデオム として使えること。そしてテスト駆動開発では一般的な設計アプローチとは異なる形で導かれているということくらいを頭に残しておけば結構です。どのパターンをいつ適用するかはリファクタリングを繰り返しているうちに思いつくようになってきます(多分)。
ただ、書籍『デザインパターン』(通称Gof本)の大ヒットは、その反面、それらパターンを表現する方法の多様性を奪ってしまった。Gof本には、設計をフェーズとして扱うという暗黙の前提があるように見受けられる。つまり、リファクタリングを設計行為として捉えていない。TDDにおける設計は、デザインパターンを少しだけ違う側面から捉えなければならない。
— テスト駆動開発
あと、設計の観点から今回 単一責任の原則 に従って
FizzBuzz
クラスを メソッドオブジェクト に分割して削除しました。もし、新しい処理を追加する必要が発生した場合はどうしましょうか?
FizzBuzzCommand
インターフェイスを実装した メソッドオブジェクト を追加しましょう。もし、新しいタイプが必要になったらどうしましょうか?
FizzBuzzType
クラスを継承した新しいタイプクラスを追加しましょう。このように既存のコードを変更することなく振る舞いを変更できるので オープン・クローズドの原則 を満たした設計といえます。
OCP:オープン・クローズドの原則
「オープン・クローズドの原則(OCP)」は、1988年にBertrand Maeerが提唱した以下のような原則だ。
ソフトウェアの構成要素は拡張に対しては開いていて、修正に対しては閉じていなければならない。 『アジャイルソフトウェア開発の奥義 第2版』(SBクリエイティブ)より引用言い換えれば、ソフトウェアの振る舞いは、既存の成果物を変更せず拡張できるようにすべきである、ということだ。
— Clean Architecture 達人に学ぶソフトウェアの構造と設計
例外
ここまでは、正常系をリファクタリングして設計を改善してきました。しかし、アプリケーションは例外系も考慮する必要があります。続いて、アサーションの導入 を適用した例外系のリファクタリングに取り組むとしましょう。
アサーションの導入
前提を明示するためのすぐれたテクニックとして、アサーションを記述する方法があります。
— リファクタリング(第2版)
アサーションの導入
まず、 メソッドオブジェクト の
FizzBuzzValueCommand
にマイナスの値が渡された場合の振る舞いをどうするか考えます。ここでは正の値のみ許可する振る舞いにしたいので以下のテストコードを追加します。class FizzBuzzTest < Minitest::Test ... describe '例外ケース' do def test_値は正の値のみ許可する assert_raises Assertions::AssertionFailedError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end end end end... ERROR["test_値は正の値のみ許可する", #<Minitest::Reporters::Suite:0x00007fadf30c45d8 @name="例外ケース">, 0.006546000000525964] test_値は正の値のみ許可する#例外ケース (0.01s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::Assertions /Users/k2works/Projects/sandbox/tdd_rb/test/fizz_buzz_test.rb:249:in `test_値は正の値のみ許可する' 36/36: [=========================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.03159s 36 tests, 39 assertions, 0 failures, 1 errors, 0 skips ...テストを通すためアサーションモジュールを追加します。Rubyでは モジュール を使います。
モジュールはクラスと非常によく似ていますが、以下の二点が異なります。
モジュールはインスタンス化できない
本章後半可能なのは include や extend が可能なのはモジュールだけ
それ以外のクラスメソッドや定数の定義などはクラスと同じように定義することができます。
— かんたんRuby
... module Assertions class AssertionFailedError < StandardError; end def assert(&condition) raise AssertionFailedError, 'Assertion Failed' unless condition.call end end class FizzBuzzValue ...アサーションモジュールを追加してエラーはなくなりましたがテストは失敗したままです。
... FAIL["test_値は正の値のみ許可する", #<Minitest::Reporters::Suite:0x00007fdcfc0c2548 @name="例外ケース">, 0.005800000000817818] test_値は正の値のみ許可する#例外ケース (0.01s) Assertions::AssertionFailedError expected but nothing was raised. /Users/k2works/Projects/sandbox/tdd_rb/test/fizz_buzz_test.rb:249:in `test_値は正の値のみ許可する' ============================================================================================================| Finished in 0.00621s 36 tests, 40 assertions, 1 failures, 0 errors, 0 skips ...追加したモジュールを
FizzBuzzValue
クラスをに Mix-in します。そして、コンストラクタ 実行時に数値は0以上であるアサーションを追加します。Rubyでの継承は一種類、単一継承しか実行できませんが、複数のクラスを継承する多重継承の代わりにMix-inというメソッドの共有方法を提供します。
— かんたんRuby
class FizzBuzzValue attr_reader :number, :value def initialize(number, value) @number = number @value = value end ... endclass FizzBuzzValue include Assertions attr_reader :number, :value def initialize(number, value) assert { number >= 0 } @number = number @value = value end ... end... Started with run options --seed 37354 Progress: |====================================================================================================| Finished in 0.01433s 36 tests, 40 assertions, 0 failures, 0 errors, 0 skips ...アサーションが機能するようになりました、コミットしておきます。
$ git add . $ git commit -m 'refactor: アサーションの導入'次は、メソッドオブジェクト の
FizzBuzzListCommand
の実行時に100件以上指定された場合の振る舞いをどうするか考えます。ここでは100までを許可する振る舞いにします。... describe '例外ケース' do def test_値は正の値のみ許可する assert_raises Assertions::AssertionFailedError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end end def test_100より多い数を許可しない assert_raises Assertions::AssertionFailedError do FizzBuzzListCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(101) end end end end
FizzBuzzList
にアサーションモジュールを Mix-in します。コンストラクタ 実行時に配列のサイズは100までというアサーションを追加します。... class FizzBuzzList include Assertions attr_reader :value def initialize(list) assert { list.count <= 100 } @value = list end ...... ERROR["test_新しいインスタンスが作られる", #<Minitest::Reporters::Suite:0x00005558ca6e8e80 @name="FizzBuzzValueList">, 0.010412617004476488] test_新しいインスタンスが作られる#FizzBuzzValueList (0.01s) Minitest::UnexpectedError: Assertions::AssertionFailedError: Assertion Failed /workspace/tdd_rb/lib/fizz_buzz.rb:58:in `assert' /workspace/tdd_rb/lib/fizz_buzz.rb:88:in `initialize' /workspace/tdd_rb/lib/fizz_buzz.rb:97:in `new' /workspace/tdd_rb/lib/fizz_buzz.rb:97:in `add' /workspace/tdd_rb/test/fizz_buzz_test.rb:259:in `test_新しいインスタンスが作られる' ====================================================================================================| Finished in 0.01238s 36 tests, 38 assertions, 0 failures, 1 errors, 0 skips ...追加したテストはパスするようになりましたが既存のテストコードでエラーが出るようになりました。該当するテストコードを見たところ100件より多い 学習用テスト で ファーストクラスコレクション を作ろうとしたため
AssertionFailedError
を発生させたようです。テストコードを修正しておきましょう。... describe 'FizzBuzzValueList' do def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(100) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 100, list1.value.count assert_equal 200, list2.value.count end end ...最初は50件作るように変更します。
... describe 'FizzBuzzValueList' do def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(50) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 100, list1.value.count assert_equal 200, list2.value.count end end ...アサーションエラーはなくなりましたが期待した値と違うと指摘されています。テストコードのアサーションを修正します。
FAIL["test_新しいインスタンスが作られる", #<Minitest::Reporters::Suite:0x0000556b5137c780 @name="FizzBuzzValueList">, 0.003735148988198489] test_新しいインスタンスが作られる#FizzBuzzValueList (0.00s) Expected: 100 Actual: 50 /workspace/tdd_rb/test/fizz_buzz_test.rb:261:in `test_新しいインスタンスが作られる' 36/36: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00837s 36 tests, 39 assertions, 1 failures, 0 errors, 0 skips... describe 'FizzBuzzValueList' do def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(50) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 50, list1.value.count assert_equal 200, list2.value.count end end ...2つ目のアサーションに引っかかってしまいました。こちらも修正します。
FAIL["test_新しいインスタンスが作られる", #<Minitest::Reporters::Suite:0x0000563a0c4fc2b0 @name="FizzBuzzValueList">, 0.005684088013367727] test_新しいインスタンスが作られる#FizzBuzzValueList (0.01s) Expected: 200 Actual: 100 /workspace/tdd_rb/test/fizz_buzz_test.rb:262:in `test_新しいインスタンスが作られる' 36/36: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00809s 36 tests, 40 assertions, 1 failures, 0 errors, 0 skips... describe 'FizzBuzzValueList' do def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(50) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 50, list1.value.count assert_equal 100, list2.value.count end end ...... 01:58:57 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 61 / 64 LOC (95.31%) covered. Started with run options --guard --seed 44956 36/36: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00717s 36 tests, 40 assertions, 0 failures, 0 errors, 0 skips ...仕様変更による反映が出来たのでコミットしましょう。
$ git add . $ git commit -m 'refactor: アサーションの導入'アサーションの導入 とは別のアプローチとして 例外 を返す方法もあります。 例外によるエラーコードの置き換え を適用してアサーションモジュールを削除しましょう。
例外によるエラーコードの置き換え
エラーを示す特別なコードをメソッドがリターンしている。
代わりに例外を発生させる。
— 新装版 リファクタリング
例外によるエラーコードの置き換え
アサーションモジュールを削除してアサーション部分を 例外 に変更します。
... module Assertions class AssertionFailedError < StandardError; end def assert(&condition) raise AssertionFailedError, 'Assertion Failed' unless condition.call end end class FizzBuzzValue include Assertions attr_reader :number, :value def initialize(number, value) assert { number >= 0 } @number = number @value = value end ... end class FizzBuzzList include Assertions attr_reader :value def initialize(list) assert { list.count <= 100 } @value = list end ... end ...... class FizzBuzzValue attr_reader :number, :value def initialize(number, value) raise '正の値のみ有効です' if number < 0 @number = number @value = value end ... end class FizzBuzzList attr_reader :value def initialize(list) raise '上限は100件までです' if list.count > 100 @value = list end ... end... ERROR["test_値は正の値のみ許可する", #<Minitest::Reporters::Suite:0x000055d30f0b8a50 @name="FizzBuzz::数を文字列にして返す::例外ケース">, 0.004186890990240499] test_値は正の値のみ許可する#FizzBuzz::数を文字列にして返す::例外ケース (0.00s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::Assertions /workspace/tdd_rb/test/fizz_buzz_test.rb:143:in `test_値は正の値のみ許可する' ERROR["test_100より多い数を許可しない", #<Minitest::Reporters::Suite:0x000055d30f114210 @name="FizzBuzz::数を文字列にして返す::例外ケース">, 0.008254560001660138] test_100より多い数を許可しない#FizzBuzz::数を文字列にして返す::例外ケース (0.01s) Minitest::UnexpectedError: NameError: uninitialized constant FizzBuzzTest::Assertions /workspace/tdd_rb/test/fizz_buzz_test.rb:151:in `test_100より多い数を許可しない' 37/37: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01731s 37 tests, 39 assertions, 0 failures, 2 errors, 0 skips ...アサーションモジュールを削除したのでエラーが発生しています。テストコードを修正しましょう。
... describe '例外ケース' do def test_値は正の値のみ許可する assert_raises Assertions::AssertionFailedError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end end def test_100より多い数を許可しない assert_raises Assertions::AssertionFailedError do FizzBuzzListCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(101) end end end end... describe '例外ケース' do def test_値は正の値のみ許可する e = assert_raises RuntimeError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end assert_equal '正の値のみ有効です', e.message end def test_100より多い数を許可しない e = assert_raises RuntimeError do FizzBuzzListCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(101) end assert_equal '上限は100件までです', e.message end end end... 02:13:46 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 55 / 58 LOC (94.83%) covered. Started with run options --guard --seed 55179 37/37: [=================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00738s 37 tests, 43 assertions, 0 failures, 0 errors, 0 skips ...再びテストが通るようになったのでコミットしておきます。
$ git add . $ git commit -m 'refactor: 例外によるエラーコードの置き換え'アルゴリズムの置き換え
02:13:46 - INFO - Inspecting Ruby code style: test/fizz_buzz_test.rb lib/fizz_buzz.rb lib/fizz_buzz.rb:58:26: C: Style/NumericPredicate: Use number.negative? instead of number < 0. raise '正の値のみ有効です' if number < 0 ^^^^^^^^^^ 2/2 files |====================================== 100 =======================================>| Time: 00:00:00 2 files inspected, 1 offense detectedテストは通りますが警告が表示されるようになりました。
Style/NumericPredicate: Use number.negative? instead of number < 0.
とのことなので アルゴリズムの置き換え を適用しておきましょう。アルゴリズムの取り替え
アルゴリズムをよりわかりやすいものに置き換えたい
メソッドの本体を新たなアルゴリズムで置き換える。
— 新装版 リファクタリング
... class FizzBuzzValue attr_reader :number, :value def initialize(number, value) raise '正の値のみ有効です' if number < 0 ...... class FizzBuzzValue attr_reader :number, :value def initialize(number, value) raise '正の値のみ有効です' if number.negative? ...02:18:31 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |======================================= 100 =======================================>| Time: 00:00:00 1 file inspected, no offenses detected警告が消えたのでコミットします。
$ git add . $ git commit -m 'refactor: アルゴリズムの置き換え'マジックナンバーの置き換え
件数に リテラル を使っています。ここは マジックナンバーの置き換え を適用するべきですね。
シンボリック定数によるマジックナンバーの置き換え
特別な意味を持った数字のリテラルがある。
定数を作り、それにふさわしい名前をつけて、そのリテラルを置き換える。
— 新装版 リファクタリング
... class FizzBuzzList attr_reader :value def initialize(list) raise '上限は100件までです' if list.count > 100 @value = list end ...式展開 を使ってメッセージ内容も定数から参照するようにしましょう。
式展開
式展開とは、「#{}」の書式で文字列中に何らかの変数や式を埋め込むことが可能な機能です。これは、ダブルクオートを使用した場合のみの機能です。
— かんたんRuby
class FizzBuzzList MAX_COUNT = 100 attr_reader :value def initialize(list) raise "上限は#{MAX_COUNT}件までです" if list.count > MAX_COUNT @value = list end ...テストは壊れていないようですが
MAX_COUNT
を変更したらテストが失敗するか確認しておきましょう。class FizzBuzzList MAX_COUNT = 10 ...... ERROR["test_配列の14番目は文字列のFizzBuzzを返す", #<Minitest::Reporters::Suite:0x000055942ab5e230 @name="FizzBuzz::数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す">, 0.008073228993453085] test_配列の14番目は文字列のFizzBuzzを返す#FizzBuzz::数を文字列にして返す::タイプ1の場合::1から100までのFizzBuzzの配列を返す (0.01s) Minitest::UnexpectedError: RuntimeError: 上限は10件までです /workspace/tdd_rb/lib/fizz_buzz.rb:80:in `initialize' /workspace/tdd_rb/lib/fizz_buzz.rb:112:in `new' /workspace/tdd_rb/lib/fizz_buzz.rb:112:in `execute' /workspace/tdd_rb/test/fizz_buzz_test.rb:45:in `setup' ...想定通りのエラーが発生したのでコードを元に戻してコミットしましょう。
class FizzBuzzList MAX_COUNT = 100 ...... Started with run options --seed 5525 Progress: |====================================================================================================| Finished in 0.01262s 37 tests, 43 assertions, 0 failures, 0 errors, 0 skips ...$ git add . $ git commit -m 'refactor: マジックナンバーの置き換え'特殊ケースの導入
最後に ポリモーフィズム の応用としてタイプクラスが未定義の場合に 例外 ではなく未定義のタイプクラスを返す 特殊ケースの導入 を適用してみましょう。
ヌルオブジェクトの導入
null値のチェックが繰り返し現れる。
そのnull値をヌルオブジェクトで置き換える。
— 新装版 リファクタリング
特殊ケースの導入
旧:ヌルオブジェクトの導入
特殊ケースの処理を要する典型的な値がnullなので、このパターンをヌルオブジェクトパターンと呼ぶことがあります、しかし、通常の特殊ケースとアプローチは同じです。いわばヌルオブジェクトは「特殊ケース」の特殊ケースです。
— リファクタリング(第2版)
まず、それ以外のタイプの場合の振る舞いを変更します。
... describe 'それ以外のタイプの場合' do def test_例外を返す e = assert_raises RuntimeError do FizzBuzzType.create(4) end assert_equal '該当するタイプは存在しません', e.message end end end ...... describe 'それ以外のタイプの場合' do def test_未定義のタイプを返す fizzbuzz = FizzBuzzType.create(4) assert_equal '未定義', fizzbuzz.to_s end end end ...... ERROR["test_未定義のタイプを返す", #<Minitest::Reporters::Suite:0x00005593e21297d0 @name="数を文字列にして返す::それ以外のタイプの場合">, 0.0065623498521745205] test_未定義のタイプを返す#数を文字列にして返す::それ以外のタイプの場合 (0.01s) Minitest::UnexpectedError: RuntimeError: 該当するタイプは存在しません /workspace/tdd_rb/lib/fizz_buzz.rb:17:in `create' /workspace/tdd_rb/test/fizz_buzz_test.rb:131:in `test_未定義のタイプを返す' 37/37: [==================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00780s 37 tests, 41 assertions, 0 failures, 1 errors, 0 skips ...現時点では 例外 を投げるので未定義タイプ
FizzBuzzTypeNotDefined
を作成して ファクトリメソッド を変更します。class FizzBuzzType TYPE_01 = 1 TYPE_02 = 2 TYPE_03 = 3 def self.create(type) case type when FizzBuzzType::TYPE_01 FizzBuzzType01.new when FizzBuzzType::TYPE_02 FizzBuzzType02.new when FizzBuzzType::TYPE_03 FizzBuzzType03.new else raise '該当するタイプは存在しません' end end def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end class FizzBuzzType01 < FizzBuzzType ...class FizzBuzzType TYPE_01 = 1 TYPE_02 = 2 TYPE_03 = 3 def self.create(type) case type when FizzBuzzType::TYPE_01 FizzBuzzType01.new when FizzBuzzType::TYPE_02 FizzBuzzType02.new when FizzBuzzType::TYPE_03 FizzBuzzType03.new else FizzBuzzTypeNotDefined.new end end ... class FizzBuzzTypeNotDefined < FizzBuzzType def generate(number) FizzBuzzValue.new(number, '') end def to_s '未定義' end end class FizzBuzzValue ...... Started with run options --seed 33939 Progress: |=====================================================================================================| Finished in 0.01193s 37 tests, 42 assertions, 0 failures, 0 errors, 0 skips 06:46:48 - INFO - Inspecting Ruby code style: lib/fizz_buzz.rb 1/1 file |======================================= 100 ========================================>| Time: 00:00:00 1 file inspected, no offenses detected 06:46:49 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/border.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/loading_background.png 0/0 files |======================================= 100 =======================================>| Time: 00:00:00 0 files inspected, no offenses detected ...テストが通るようになりました。 メソッドオブジェクト から実行された場合の振る舞いも明記しておきましょう。
... describe 'それ以外のタイプの場合' do def test_未定義のタイプを返す fizzbuzz = FizzBuzzType.create(4) assert_equal '未定義', fizzbuzz.to_s end def test_空の文字列を返す type = FizzBuzzType.create(4) command = FizzBuzzValueCommand.new(type) assert_equal '', command.execute(3) end end end ...... 06:48:54 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 62 / 65 LOC (95.38%) covered. Started with run options --guard --seed 18202 38/38: [==================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00747s 38 tests, 43 assertions, 0 failures, 0 errors, 0 skips ...
FizzBuzzTypeNotDefined
オブジェクトは Null Objectパターン を適用したものです。Null Objectパターン
特殊な状況をオブジェクトで表現するにはどうすればよいだろうか---その特殊な状況を表現するオブジェクトを作り、通常のオブジェクトと同じプロトコル(メソッド群)を実装しよう。
— テスト駆動開発
オープン・クローズドの原則 に従って未定義のタイプである Null Object を安全に追加することができたのでコミットしておきます。
$ git add . $ git commit -m 'refactor: 特殊ケースの導入'モジュール分割
クラスモジュールの抽出によってアプリケーションの構造が 抽象化 された結果、視覚的に把握できるようになりました。ここでアプリケーションを実行してみましょう。
$ ruby main.rb Traceback (most recent call last): main.rb:5:in `<main>': uninitialized constant FizzBuzz (NameError) Did you mean? FizzBuzzTypeエラーが出ています、これはアプリケーションの構成が変わったためです。クライアントプログラムをアプリケーションの変更に合わせて修正します。
# frozen_string_literal: true require './lib/fizz_buzz.rb' puts FizzBuzz.generate_list# frozen_string_literal: true require './lib/fizz_buzz.rb' command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) command.execute(100).each { |i| puts i.value }$ ruby main.rb 1 2 Fizz 4 Buzz ... Fizz
クライアントプログラムが直ったのでコミットしておきます。
$ git add . $ git commit -m 'fix: プリントする'ドメインモデル
fizz_buzz.rb
ファイル内のクラスモジュールをファイルとして分割していきます。まずは ドメインオブジェクト を抽出して ドメインモデル として整理しましょう。既存のテストを壊さないように1つづつコピー&ペーストしていきます。関連する業務データと業務ロジックを1つにまとめたこのようなオブジェクトをドメインオブジェクトと呼びます。
「ドメイン」とは、対象領域とか問題領域という意味です。業務アプリケーションの場合、そのアプリケーションが対象となる業務活動全体がドメインです。業務活動という問題領域(ドメイン)で扱うデータと業務ロジックを、オブジェクトとして表現したものドメインオブジェクトです。ドメインオブジェクトは、業務データと業務ロジックを密接に関係づけます。
— 現場で役立つシステム設計の原則
このように業務アプリケーションの対象領域(ドメイン)をオブジェクトのモデルとして整理したものをドメインモデルと呼びます。
— 現場で役立つシステム設計の原則
/main.rb |--lib/ | -- fizz_buzz.rb |--test/ | -- fizz_buzz_test.rb /main.rb |--lib/ | domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz_type_not_defined.rb -- fizz_buzz.rb |--test/ | -- fizz_buzz_test.rb値オブジェクトクラス と タイプクラス を
domain
フォルダ以下に配置します。# frozen_string_literal: true class FizzBuzzValue attr_reader :number, :value def initialize(number, value) raise '正の値のみ有効です' if number.negative? @number = number @value = value end def to_s "#{@number}:#{@value}" end def ==(other) @number == other.number && @value == other.value end alias eql? == end# frozen_string_literal: true class FizzBuzzList MAX_COUNT = 100 attr_reader :value def initialize(list) raise "上限は#{MAX_COUNT}件までです" if list.count > MAX_COUNT @value = list end def to_s @value.to_s end def add(value) FizzBuzzList.new(@value + value) end end# frozen_string_literal: true class FizzBuzzType TYPE_01 = 1 TYPE_02 = 2 TYPE_03 = 3 def self.create(type) case type when FizzBuzzType::TYPE_01 FizzBuzzType01.new when FizzBuzzType::TYPE_02 FizzBuzzType02.new when FizzBuzzType::TYPE_03 FizzBuzzType03.new else FizzBuzzTypeNotDefined.new end end def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end# frozen_string_literal: true class FizzBuzzType01 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) return FizzBuzzValue.new(number, 'Fizz') if fizz?(number) return FizzBuzzValue.new(number, 'Buzz') if buzz?(number) FizzBuzzValue.new(number, number.to_s) end end# frozen_string_literal: true class FizzBuzzType02 < FizzBuzzType def generate(number) FizzBuzzValue.new(number, number.to_s) end end# frozen_string_literal: true class FizzBuzzType03 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) FizzBuzzValue.new(number, number.to_s) end end# frozen_string_literal: true class FizzBuzzTypeNotDefined < FizzBuzzType def generate(number) FizzBuzzValue.new(number, '') end def to_s '未定義' end end... 07:29:03 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/border.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/loading_background.png lib/domain/type/fizz_buzz_type_not_defined.rb lib/domain/type/fizz_buzz_type_03.rb lib/domain/type/fizz_buzz_type_02.rb lib/domain/type/fizz_buzz_type_01.rb lib/domain/type/fizz_buzz_type.rb lib/domain/model/fizz_buzz_list.rb lib/domain/model/fizz_buzz_value.rb lib/domain/type/fizz_buzz_type_not_defined.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzTypeNotDefined < FizzBuzzType ^^^^^ lib/domain/type/fizz_buzz_type_03.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzType03 < FizzBuzzType ^^^^^ lib/domain/type/fizz_buzz_type_02.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzType02 < FizzBuzzType ^^^^^ lib/domain/type/fizz_buzz_type_01.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzType01 < FizzBuzzType ^^^^^ lib/domain/type/fizz_buzz_type.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzType ^^^^^ lib/domain/model/fizz_buzz_list.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzList ^^^^^ lib/domain/model/fizz_buzz_value.rb:3:1: C: Style/Documentation: Missing top-level class documentation comment. class FizzBuzzValue ^^^^^ 7/7 files |======================== 100 =========================>| Time: 00:00:00 7 files inspected, 7 offenses detected ...テストは壊れていないようですが警告が出るようになりました。まだ仕掛ですが一旦コミットしておきます。
$ git add . $ git commit -m 'refactor(WIP): モジュール分割'アプリケーション
続いて アプリケーション層 の分割を行います。
データクラスと機能クラスを分ける手続き型の設計では、アプリケーション層のクラスに業務ロジックの詳細を記述します。
— 現場で役立つシステム設計の原則
/main.rb |--lib/ | domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | -- fizz_buzz_test.rb /main.rb |--lib/ | application/ | -- fizz_buzz_command.rb -- fizz_buzz_value_command.rb -- fizz_buzz_list_command.rb domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | -- fizz_buzz_test.rbここでは ドメインオブジェクト を操作する メソッドオブジェクト を
application
フォルダ以下に配置します。# frozen_string_literal: true class FizzBuzzCommand def execute; end end# frozen_string_literal: true class FizzBuzzValueCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) @type.generate(number).value end end# frozen_string_literal: true class FizzBuzzListCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) FizzBuzzList.new((1..number).map { |i| @type.generate(i) }).value end endテストは壊れていないのでコミットしておきます。
$ git add . $ git commit -m 'refactor(WIP): モジュール分割'テスト
アプリケーションのメイン部分は分割できました。続いてテストも分割しましょう。
/main.rb |--lib/ | application/ | -- fizz_buzz_command.rb -- fizz_buzz_value_command.rb -- fizz_buzz_list_command.rb domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | -- fizz_buzz_test.rb /main.rb |--lib/ | application/ | -- fizz_buzz_command.rb -- fizz_buzz_value_command.rb -- fizz_buzz_list_command.rb domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | application/ | -- fizz_buzz_value_command_test.rb -- fizz_buzz_list_command_test.rb domain/ | model/ | -- fizz_buzz_value_test.rb -- fizz_buzz_list_test.rb | -- learning_test.rb# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzValueCommandTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType01.new) end describe '三の倍数の場合' do def test_3を渡したら文字列Fizzを返す assert_equal 'Fizz', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列Buzzを返す assert_equal 'Buzz', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType02.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType03.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'それ以外のタイプの場合' do def test_未定義のタイプを返す fizzbuzz = FizzBuzzType.create(4) assert_equal '未定義', fizzbuzz.to_s end def test_空の文字列を返す type = FizzBuzzType.create(4) command = FizzBuzzValueCommand.new(type) assert_equal '', command.execute(3) end end end describe '例外ケース' do def test_値は正の値のみ許可する e = assert_raises RuntimeError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end assert_equal '正の値のみ有効です', e.message end end end# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzListCommandTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzzListCommand.new(FizzBuzzType01.new) @result = fizzbuzz.execute(100) end def test_配列の初めは文字列の1を返す assert_equal '1', @result.first.value end def test_配列の最後は文字列のBuzzを返す assert_equal 'Buzz', @result.last.value end def test_配列の2番目は文字列のFizzを返す assert_equal 'Fizz', @result[2].value end def test_配列の4番目は文字列のBuzzを返す assert_equal 'Buzz', @result[4].value end def test_配列の14番目は文字列のFizzBuzzを返す assert_equal 'FizzBuzz', @result[14].value end end end end describe '例外ケース' do def test_100より多い数を許可しない e = assert_raises RuntimeError do FizzBuzzListCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(101) end assert_equal '上限は100件までです', e.message end end end# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzValueTest < Minitest::Test def test_同じで値である value1 = FizzBuzzValue.new(1, '1') value2 = FizzBuzzValue.new(1, '1') assert value1.eql?(value2) end def test_to_stringメソッド value = FizzBuzzValue.new(3, 'Fizz') assert_equal '3:Fizz', value.to_s end end# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzListTest < Minitest::Test def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(50) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 50, list1.value.count assert_equal 100, list2.value.count end end# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' class LearningTest < Minitest::Test describe '配列や繰り返し処理を理解する' do def test_繰り返し処理 $stdout = StringIO.new [1, 2, 3].each { |i| p i * i } output = $stdout.string assert_equal "1\n" + "4\n" + "9\n", output end def test_selectメソッドで特定の条件を満たす要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].select(&:integer?) assert_equal [2, 4], result end def test_find_allメソッドで特定の条件を満たす要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].find_all(&:integer?) assert_equal [2, 4], result end def test_特定の条件を満たさない要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].reject(&:integer?) assert_equal [1.1, 3.3], result end def test_mapメソッドで新しい要素の配列を返す result = %w[apple orange pineapple strawberry].map(&:size) assert_equal [5, 6, 9, 10], result end def test_collectメソッドで新しい要素の配列を返す result = %w[apple orange pineapple strawberry].collect(&:size) assert_equal [5, 6, 9, 10], result end def test_findメソッドで配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry].find(&:size) assert_equal 'apple', result end def test_detectメソッドで配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry].detect(&:size) assert_equal 'apple', result end def test_指定した評価式で並び変えた配列を返す result1 = %w[2 4 13 3 1 10].sort result2 = %w[2 4 13 3 1 10].sort { |a, b| a.to_i <=> b.to_i } result3 = %w[2 4 13 3 1 10].sort { |b, a| a.to_i <=> b.to_i } assert_equal %w[1 10 13 2 3 4], result1 assert_equal %w[1 2 3 4 10 13], result2 assert_equal %w[13 10 4 3 2 1], result3 end def test_配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry apricot].grep(/^a/) assert_equal %w[apple apricot], result end def test_ブロック内の条件式が真である間までの要素を返す result = [1, 2, 3, 4, 5, 6, 7, 8, 9].take_while { |item| item < 6 } assert_equal [1, 2, 3, 4, 5], result end def test_ブロック内の条件式が真である以降の要素を返す result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].drop_while { |item| item < 6 } assert_equal [6, 7, 8, 9, 10], result end def test_injectメソッドで畳み込み演算を行う result = [1, 2, 3, 4, 5].inject(0) { |total, n| total + n } assert_equal 15, result end def test_reduceメソッドで畳み込み演算を行う result = [1, 2, 3, 4, 5].reduce { |total, n| total + n } assert_equal 15, result end end endファイル分割でテストは壊れていないようですが警告がたくさん出てきました。
... test/learning_test.rb:70:14: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers. def test_ブロック内の条件式が真である間までの要素を返す ^^^^^^^^^^^^^^^^^^^^^^^ test/learning_test.rb:75:9: C: Naming/MethodName: Use snake_case for method names. def test_ブロック内の条件式が真である以降の要素を返す ^^^^^^^^^^^^^^^^^^^^^^^^^^^ test/learning_test.rb:75:14: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers. def test_ブロック内の条件式が真である以降の要素を返す ^^^^^^^^^^^^^^^^^^^^^^ test/learning_test.rb:80:9: C: Naming/MethodName: Use snake_case for method names. def test_injectメソッドで畳み込み演算を行う ^^^^^^^^^^^^^^^^^^^^^^^^^ test/learning_test.rb:80:20: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers. def test_injectメソッドで畳み込み演算を行う ^^^^^^^^^^^^^^ test/learning_test.rb:85:9: C: Naming/MethodName: Use snake_case for method names. def test_reduceメソッドで畳み込み演算を行う ^^^^^^^^^^^^^^^^^^^^^^^^^ test/learning_test.rb:85:20: C: Naming/AsciiIdentifiers: Use only ascii symbols in identifiers. def test_reduceメソッドで畳み込み演算を行う ^^^^^^^^^^^^^^ 15/15 files |======================= 100 ========================>| Time: 00:00:00 15 files inspected, 87 offenses detected ...これらはテストコードに関する警告がほとんどなので
.rubocop.yml
を編集してチェック対象から外しておきましょう。inherit_from: .rubocop_todo.yml Naming/AsciiIdentifiers: Exclude: - 'test/**/*' Naming/MethodName: EnforcedStyle: snake_case Exclude: - 'test/**/*' Metrics/BlockLength: Max: 62 Exclude: - 'test/**/*' Documentation: Enabled: false... 08:21:55 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 144 / 215 LOC (66.98%) covered. Started with run options --guard --seed 55977 70/70: [=====================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.01518s 70 tests, 79 assertions, 0 failures, 0 errors, 0 skips 08:21:56 - INFO - Inspecting Ruby code style of all files /workspace/tdd_rb/.rubocop.yml: Warning: no department given for Documentation. 22/22 files |======================= 100 ========================>| Time: 00:00:00 22 files inspected, no offenses detected 08:21:58 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/border.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/loading_background.png /workspace/tdd_rb/.rubocop.yml: Warning: no department given for Documentation. 0/0 files |======================== 100 =========================>| Time: 00:00:00 0 files inspected, no offenses detected ...警告は消えました、仕上げに
fizz_buzz_test.rb
ファイルを削除します。... 08:24:12 - INFO - Running: all tests Coverage report generated for MiniTest, Unit Tests to /workspace/tdd_rb/coverage. 135 / 201 LOC (67.16%) covered. Started with run options --guard --seed 40104 32/32: [=====================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00601s 32 tests, 36 assertions, 0 failures, 0 errors, 0 skips 08:24:13 - INFO - Inspecting Ruby code style of all files /workspace/tdd_rb/.rubocop.yml: Warning: no department given for Documentation. 21/21 files |======================= 100 ========================>| Time: 00:00:00 21 files inspected, no offenses detected 08:24:14 - INFO - Inspecting Ruby code style: coverage/assets/0.10.2/colorbox/controls.png coverage/assets/0.10.2/colorbox/border.png coverage/assets/0.10.2/colorbox/loading.gif coverage/assets/0.10.2/colorbox/loading_background.png /workspace/tdd_rb/.rubocop.yml: Warning: no department given for Documentation. 0/0 files |======================== 100 =========================>| Time: 00:00:00 0 files inspected, no offenses detected ...テストの分割も完了したのでコミットしておきます。
$ git add . $ git commit -m 'refactor(WIP): モジュール分割'エントリーポイント
仕上げはクラスモジュールのエントリーポイント作成とテストヘルパーの追加です。
/main.rb |--lib/ | application/ | -- fizz_buzz_command.rb -- fizz_buzz_value_command.rb -- fizz_buzz_list_command.rb domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | application/ | -- fizz_buzz_value_command_test.rb -- fizz_buzz_list_command._test.rb domain/ | model/ | -- fizz_buzz_value_test.rb -- fizz_buzz_list_test.rb | -- learning_test.rb /main.rb |--lib/ | application/ | -- fizz_buzz_command.rb -- fizz_buzz_value_command.rb -- fizz_buzz_list_command.rb domain/ | model/ | -- fizz_buzz_value.rb -- fizz_buzz_list.rb type/ | -- fizz_buzz_type.rb -- fizz_buzz_type_01.rb -- fizz_buzz_type_02.rb -- fizz_buzz_type_03.rb -- fizz_buzz.rb |--test/ | application/ | -- fizz_buzz_value_command_test.rb -- fizz_buzz_list_command._test.rb domain/ | model/ | -- fizz_buzz_value_test.rb -- fizz_buzz_list_test.rb | -- learning_test.rb -- test_helper.rb
fizz_buzz.rb
ファイルの内容をクラスモジュール読み込みに変更します。require './lib/application/fizz_buzz_command.rb' require './lib/application/fizz_buzz_value_command.rb' require './lib/application/fizz_buzz_list_command.rb' require './lib/domain/model/fizz_buzz_value.rb' require './lib/domain/model/fizz_buzz_list.rb' require './lib/domain/type/fizz_buzz_type.rb' require './lib/domain/type/fizz_buzz_type_01.rb' require './lib/domain/type/fizz_buzz_type_02.rb' require './lib/domain/type/fizz_buzz_type_03.rb' require './lib/domain/type/fizz_buzz_type_not_defined.rb'... 08:34:32 - INFO - Running: all tests Coverage report generated for MiniTest to /workspace/tdd_rb/coverage. 119 / 211 LOC (56.4%) covered. Started with run options --guard --seed 18696 32/32: [=====================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00561s 32 tests, 36 assertions, 0 failures, 0 errors, 0 skips ....コードカバレッジがうまく機能していないようなので、
test_helper.rb
を追加して共通部分を各テストファイルから読み込むように変更します。# frozen_string_literal: true require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use!require 'simplecov' SimpleCov.start require 'minitest/reporters' Minitest::Reporters.use! require 'minitest/autorun' require './lib/fizz_buzz' ...require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' ...テストタスクを実行したところ動作しなくなりました。
$ rake testテスト対象をテストディレクトリ内のすべてのテストコードに変更します。
... Rake::TestTask.new do |test| test.test_files = Dir['./test/fizz_buzz_test.rb'] test.verbose = true end ...... Rake::TestTask.new do |test| test.test_files = Dir['./test/**/*_test.rb'] test.verbose = true end ...$ rake test Started with run options --seed 46929 32/32: [=====================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.00800s 32 tests, 36 assertions, 0 failures, 0 errors, 0 skipsテストも壊れていないし警告も出ていません。モジュール分割完了です。
$ git add . $ git commit -m 'refactor: モジュール分割'ふりかえり
今回、 オブジェクト指向プログラム から オブジェクト指向設計 そして モジュール分割 を テスト駆動開発を通じて実践しました。各トピックを振り返ってみましょう。
オブジェクト指向プログラム
エピソード1で作成したプログラムの追加仕様を テスト駆動開発 で実装しました。 次に 手続き型コード との比較から オブジェクト指向プログラム を構成する カプセル化 ポリモフィズム 継承 という概念をコードベースの リファクタリング を通じて解説しました。
具体的には フィールドのカプセル から setterの削除 を適用することにより カプセル化 を実現しました。続いて、 ポリモーフィズムによる条件記述の置き換え から State/Strategyによるタイプコードの置き換え を適用することにより ポリモーフィズム の効果を体験しました。そして、 スーパークラスの抽出 から メソッド名の変更 メソッドの移動 の適用を通して 継承 の使い方を体験しました。さらに 値オブジェクト と ファーストクラス というオブジェクト指向プログラミングに必要なツールの使い方も学習しました。
オブジェクト指向設計
次に設計の観点から 単一責任の原則 に違反している
FizzBuzz
クラスを デザインパターン の1つである Commandパターン を使ったリファクタリングである メソッドオブジェクトによるメソッドの置き換え を適用してクラスの責務を分割しました。オブジェクト指向設計のイデオムである デザインパターン として Commandパターン 以外に Value Objectパターン Factory Methodパターン Strategyパターン を リファクタリング を適用する過程ですでに実現していたことを説明しました。そして、オープン・クローズドの原則 を満たすコードに リファクタリング されたことで既存のコードを変更することなく振る舞いを変更できるようになりました。加えて、正常系の設計を改善した後 アサーションの導入 例外によるエラーコードの置き換え といった例外系の リファクタリング を適用しました。最後に ポリモーフィズム の応用として 特殊ケースの導入 の適用による Null Objectパターン を使った オープン・クローズドの原則 に従った安全なコードの追加方法を解説しました。
モジュールの分割
仕上げに、モノリシック なファイルから個別のクラスモジュールへの分割を ドメインオブジェクト の抽出を通して ドメインモデル へと整理することにより モジュール分割 を実現しました。最終的にプログラムからアプリケーションへと体裁を整えることが出来ました。以下が最終的なモジュール構造とコードです。
- Application
/main.rb.
# frozen_string_literal: true require './lib/fizz_buzz.rb' command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) command.execute(100).each { |i| puts i.value }/lib/application/fizz_buzz_command.rb.
# frozen_string_literal: true class FizzBuzzCommand def execute; end end/lib/application/fizz_buzz_value_command.rb.
# frozen_string_literal: true class FizzBuzzValueCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) @type.generate(number).value end end/lib/application/fizz_buzz_list_command.rb.
# frozen_string_literal: true class FizzBuzzListCommand < FizzBuzzCommand def initialize(type) @type = type end def execute(number) FizzBuzzList.new((1..number).map { |i| @type.generate(i) }).value end end
- Domain
/lib/domain/model/fizz_buzz_value.rb.
# frozen_string_literal: true class FizzBuzzValue attr_reader :number, :value def initialize(number, value) raise '正の値のみ有効です' if number.negative? @number = number @value = value end def to_s "#{@number}:#{@value}" end def ==(other) @number == other.number && @value == other.value end alias eql? == end/lib/domain/model/fizz_buzz_list.rb.
# frozen_string_literal: true class FizzBuzzList MAX_COUNT = 100 attr_reader :value def initialize(list) raise "上限は#{MAX_COUNT}件までです" if list.count > MAX_COUNT @value = list end def to_s @value.to_s end def add(value) FizzBuzzList.new(@value + value) end end/lib/domain/type/fizz_buzz_type.rb.
# frozen_string_literal: true class FizzBuzzType TYPE_01 = 1 TYPE_02 = 2 TYPE_03 = 3 def self.create(type) case type when FizzBuzzType::TYPE_01 FizzBuzzType01.new when FizzBuzzType::TYPE_02 FizzBuzzType02.new when FizzBuzzType::TYPE_03 FizzBuzzType03.new else FizzBuzzTypeNotDefined.new end end def fizz?(number) number.modulo(3).zero? end def buzz?(number) number.modulo(5).zero? end end/lib/domain/type/fizz_buzz_type_01.rb.
# frozen_string_literal: true class FizzBuzzType01 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) return FizzBuzzValue.new(number, 'Fizz') if fizz?(number) return FizzBuzzValue.new(number, 'Buzz') if buzz?(number) FizzBuzzValue.new(number, number.to_s) end end/lib/domain/type/fizz_buzz_type_02.rb.
# frozen_string_literal: true class FizzBuzzType02 < FizzBuzzType def generate(number) FizzBuzzValue.new(number, number.to_s) end end/lib/domain/type/fizz_buzz_type_03.rb.
# frozen_string_literal: true class FizzBuzzType03 < FizzBuzzType def generate(number) return FizzBuzzValue.new(number, 'FizzBuzz') if fizz?(number) && buzz?(number) FizzBuzzValue.new(number, number.to_s) end end/lib/domain/type/fizz_buzz_type_not_defined.b.
# frozen_string_literal: true class FizzBuzzTypeNotDefined < FizzBuzzType def generate(number) FizzBuzzValue.new(number, '') end def to_s '未定義' end end
- Test
/test/application/fizz_buzz_value_command_test.rb.
# frozen_string_literal: true require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzValueCommandTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType01.new) end describe '三の倍数の場合' do def test_3を渡したら文字列Fizzを返す assert_equal 'Fizz', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列Buzzを返す assert_equal 'Buzz', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'タイプ2の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType02.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列15を返す assert_equal '15', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'タイプ3の場合' do def setup @fizzbuzz = FizzBuzzValueCommand.new(FizzBuzzType03.new) end describe '三の倍数の場合' do def test_3を渡したら文字列3を返す assert_equal '3', @fizzbuzz.execute(3) end end describe '五の倍数の場合' do def test_5を渡したら文字列5を返す assert_equal '5', @fizzbuzz.execute(5) end end describe '三と五の倍数の場合' do def test_15を渡したら文字列FizzBuzzを返す assert_equal 'FizzBuzz', @fizzbuzz.execute(15) end end describe 'その他の場合' do def test_1を渡したら文字列1を返す assert_equal '1', @fizzbuzz.execute(1) end end end describe 'それ以外のタイプの場合' do def test_未定義のタイプを返す fizzbuzz = FizzBuzzType.create(4) assert_equal '未定義', fizzbuzz.to_s end def test_空の文字列を返す type = FizzBuzzType.create(4) command = FizzBuzzValueCommand.new(type) assert_equal '', command.execute(3) end end end describe '例外ケース' do def test_値は正の値のみ許可する e = assert_raises RuntimeError do FizzBuzzValueCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(-1) end assert_equal '正の値のみ有効です', e.message end end end/test/application/fizz_buzz_list_command_test.rb.
# frozen_string_literal: true require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzListCommandTest < Minitest::Test describe '数を文字列にして返す' do describe 'タイプ1の場合' do describe '1から100までのFizzBuzzの配列を返す' do def setup fizzbuzz = FizzBuzzListCommand.new(FizzBuzzType01.new) @result = fizzbuzz.execute(100) end def test_配列の初めは文字列の1を返す assert_equal '1', @result.first.value end def test_配列の最後は文字列のBuzzを返す assert_equal 'Buzz', @result.last.value end def test_配列の2番目は文字列のFizzを返す assert_equal 'Fizz', @result[2].value end def test_配列の4番目は文字列のBuzzを返す assert_equal 'Buzz', @result[4].value end def test_配列の14番目は文字列のFizzBuzzを返す assert_equal 'FizzBuzz', @result[14].value end end end end describe '例外ケース' do def test_100より多い数を許可しない e = assert_raises RuntimeError do FizzBuzzListCommand.new( FizzBuzzType.create(FizzBuzzType::TYPE_01) ).execute(101) end assert_equal '上限は100件までです', e.message end end end/test/domain/model/fizz_buzz_value_test.rb.
# frozen_string_literal: true require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzValueTest < Minitest::Test def test_同じで値である value1 = FizzBuzzValue.new(1, '1') value2 = FizzBuzzValue.new(1, '1') assert value1.eql?(value2) end def test_to_stringメソッド value = FizzBuzzValue.new(3, 'Fizz') assert_equal '3:Fizz', value.to_s end end/test/domain/model/fizz_buzz_list_test.rb.
# frozen_string_literal: true require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' class FizzBuzzListTest < Minitest::Test def test_新しいインスタンスが作られる command = FizzBuzzListCommand.new(FizzBuzzType.create(FizzBuzzType::TYPE_01)) array = command.execute(50) list1 = FizzBuzzList.new(array) list2 = list1.add(array) assert_equal 50, list1.value.count assert_equal 100, list2.value.count end end/test/learning_test.rb.
# frozen_string_literal: true require './test/test_helper' require 'minitest/autorun' require './lib/fizz_buzz' class LearningTest < Minitest::Test describe '配列や繰り返し処理を理解する' do def test_繰り返し処理 $stdout = StringIO.new [1, 2, 3].each { |i| p i * i } output = $stdout.string assert_equal "1\n" + "4\n" + "9\n", output end def test_selectメソッドで特定の条件を満たす要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].select(&:integer?) assert_equal [2, 4], result end def test_find_allメソッドで特定の条件を満たす要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].find_all(&:integer?) assert_equal [2, 4], result end def test_特定の条件を満たさない要素だけを配列に入れて返す result = [1.1, 2, 3.3, 4].reject(&:integer?) assert_equal [1.1, 3.3], result end def test_mapメソッドで新しい要素の配列を返す result = %w[apple orange pineapple strawberry].map(&:size) assert_equal [5, 6, 9, 10], result end def test_collectメソッドで新しい要素の配列を返す result = %w[apple orange pineapple strawberry].collect(&:size) assert_equal [5, 6, 9, 10], result end def test_findメソッドで配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry].find(&:size) assert_equal 'apple', result end def test_detectメソッドで配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry].detect(&:size) assert_equal 'apple', result end def test_指定した評価式で並び変えた配列を返す result1 = %w[2 4 13 3 1 10].sort result2 = %w[2 4 13 3 1 10].sort { |a, b| a.to_i <=> b.to_i } result3 = %w[2 4 13 3 1 10].sort { |b, a| a.to_i <=> b.to_i } assert_equal %w[1 10 13 2 3 4], result1 assert_equal %w[1 2 3 4 10 13], result2 assert_equal %w[13 10 4 3 2 1], result3 end def test_配列の中から条件に一致する要素を取得する result = %w[apple orange pineapple strawberry apricot].grep(/^a/) assert_equal %w[apple apricot], result end def test_ブロック内の条件式が真である間までの要素を返す result = [1, 2, 3, 4, 5, 6, 7, 8, 9].take_while { |item| item < 6 } assert_equal [1, 2, 3, 4, 5], result end def test_ブロック内の条件式が真である以降の要素を返す result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].drop_while { |item| item < 6 } assert_equal [6, 7, 8, 9, 10], result end def test_injectメソッドで畳み込み演算を行う result = [1, 2, 3, 4, 5].inject(0) { |total, n| total + n } assert_equal 15, result end def test_reduceメソッドで畳み込み演算を行う result = [1, 2, 3, 4, 5].reduce { |total, n| total + n } assert_equal 15, result end end end良い設計
エピソード1では 良いコード について考えました。
TDDは「より良いコードを書けば、よりうまくいく」という素朴で奇妙な仮設によって成り立っている
— テスト駆動開発
「動作するきれいなコード」。RonJeffriesのこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。動作するきれいなコードはあらゆる意味で価値がある。
— テスト駆動開発
良いコードかどうかは、変更がどれだけ容易なのかで決まる。
— リファクタリング(第2版)
コードは理解しやすくなければいけない。
— リーダブルコード
本エピソードでは テスト駆動開発 による オブジェクト指向プログラミング の リファクタリング を経てコードベースを改善してきました。そして オブジェクト指向設計 により 良いコード のプログラムを 良い設計 のアプリケーションへと進化させることができました。
どこに何が書いてあるかをわかりやすくし、変更の影響を狭い範囲に閉じ込め、安定して動作する部品を柔軟に組み合わせながらソフトウェアを構築する技法がオブジェクト指向設計です。
— 現場で役立つシステム設計の原則
設計の良し悪しは、ソフトウェアを変更するときにはっきりします。
構造が入り組んだわかりづらいプログラムは内容の理解に時間がかかります。重複したコードをあちこちで修正する作業が増え、変更の副作用に悩まされます。
一方、うまく設計されたプログラムは変更が楽で安全です。変更すべき箇所がかんたんにわかり、変更するコード量が少なく、変更の影響を狭い範囲に限定できます。
プログラムの修正に3日かかるか、それとも半日で済むか。その違いを生むのが「設計」なのです。
— 現場で役立つシステム設計の原則
では、いつ設計をしていたのでしょうか? わかりますよね、このエピソードの始まりから終わりまで常に設計をしていたのです。
TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。
— テスト駆動開発
参考サイト
参考図書
テスト駆動開発 Kent Beck (著), 和田 卓人 (翻訳): オーム社; 新訳版 (2017/10/14)
新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES) Martin
Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 新装版
(2014/7/26)リファクタリング(第2版): 既存のコードを安全に改善する (OBJECT TECHNOLOGY SERIES) Martin
Fowler (著), 児玉 公信 (翻訳), 友野 晶夫 (翻訳), 平澤 章 (翻訳), その他: オーム社; 第2版
(2019/12/1)リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
Dustin Boswell (著), Trevor Foucher (著), 須藤 功平 (解説), 角 征典 (翻訳):
オライリージャパン; 初版八刷版 (2012/6/23)Clean Code アジャイルソフトウェア達人の技 (アスキードワンゴ) Robert C.Martin (著), 花井
志生 (著) ドワンゴ (2017/12/28)現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 増田 亨 (著) 技術評論社
(2017/7/5)かんたん Ruby (プログラミングの教科書) すがわらまさのり (著) 技術評論社 (2018/6/21)
- 投稿日:2020-02-10T10:43:00+09:00
Ruby コーティング規約について 命名規則編 1
はじめに
レイアウト編 3 はこちらをクリック願います。過去記事リンクあり。
構文編 2 はこちらをクリック願います。過去記事リンクあり。
Rubyの基礎を学習中の方に向けて記載致します。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。命名規則について
① シンボル、メソッド、変数にはsnake_caseを使う。
qiita.rb# 悪い例 :'some symbol' :SomeSymbol :someSymbol someVar = 3 def someMethod # 処理 end def SomeMethod # 処理 end # 良い例 :some_symbol some_var = 3 def some_method # 処理 end② クラスやモジュールにはCamelCaseを使う。
qiita.rb# 悪い例 class Someclass # 処理 end class Some_Class # 処理 end class SomeXml # 処理 end class XmlSomething # 処理 end # 良い例 class SomeClass # 処理 end class SomeXML # 処理 end class XMLSomething # 処理 end③ ファイル名にはsnake_caseを使う。例)hello_qiita.rb
④ ディレクトリ名にはsnake_caseを使う。例)Add/hello_qiita.rb
→ 早速、GitHubで使ってみます。さいごに
毎日更新します。
皆様の復習等にご活用頂けますと幸いです。
- 投稿日:2020-02-10T09:32:02+09:00
【初心者】AWS Amazon Linux2でFluentdのfluent-plugin-cloudwatch-logsのインストールに詰まった話
概要
ログを取ろうと、Amazon Linux2でFluentdは入ったものの、fluent-plugin-cloudwatch-logsでインストールに詰まった。原因としてはrubyの開発環境がなかっただけだった話。
症状
こんなエラーが生じてうまく入らない。
$ sudo /opt/td-agent/embedded/bin/gem install fluent-plugin-cloudwatch-logs --no-ri --no-rdoc Building native extensions. This could take a while... ERROR: Error installing fluent-plugin-cloudwatch-logs: ERROR: Failed to build gem native extension. current directory: /opt/td-agent/embedded/lib/ruby/gems/2.4.0/gems/msgpack-1.3.3/ext/msgpack /opt/td-agent/embedded/bin/ruby -r ./siteconf20200210-967-1s5gonu.rb extconf.rb checking for ruby/st.h... *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --with-opt-include --without-opt-include=${opt-dir}/include --with-opt-lib --without-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/opt/td-agent/embedded/bin/$(RUBY_BASE_NAME) /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:468:in `try_do': The compiler failed to generate an executable file. (RuntimeError) You have to install development tools first. from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:599:in `try_cpp' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:1107:in `block in have_header' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:957:in `block in checking_for' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:351:in `block (2 levels) in postpone' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:321:in `open' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:351:in `block in postpone' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:321:in `open' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:347:in `postpone' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:956:in `checking_for' from /opt/td-agent/embedded/lib/ruby/2.4.0/mkmf.rb:1106:in `have_header' from extconf.rb:3:in `<main>' To see why this extension failed to compile, please check the mkmf.log which can be found here: /opt/td-agent/embedded/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0/msgpack-1.3.3/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /opt/td-agent/embedded/lib/ruby/gems/2.4.0/gems/msgpack-1.3.3 for inspection. Results logged to /opt/td-agent/embedded/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0/msgpack-1.3.3/gem_make.out原因
Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers.
や、You have to install development tools first.
というメッセージがあることから、rubyの開発環境が足りていないことがわかる。対応策
Rubyの開発環境を一式放り込んだ。
sudo yum -y install gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel libffi-devel libxml2 libxslt libxml2-devel libxslt-devel sqlite-devel結果
無事インストールできた。
$ sudo /opt/td-agent/embedded/bin/gem install fluent-plugin-cloudwatch-logs --no-ri --no-rdoc Building native extensions. This could take a while... Successfully installed msgpack-1.3.3 Fetching: fluentd-1.9.1.gem (100%) Successfully installed fluentd-1.9.1 Fetching: aws-partitions-1.271.0.gem (100%) Successfully installed aws-partitions-1.271.0 Fetching: aws-sdk-core-3.89.1.gem (100%) Successfully installed aws-sdk-core-3.89.1 Fetching: aws-sdk-cloudwatchlogs-1.28.0.gem (100%) Successfully installed aws-sdk-cloudwatchlogs-1.28.0 Fetching: fluent-plugin-cloudwatch-logs-0.8.0.gem (100%) Successfully installed fluent-plugin-cloudwatch-logs-0.8.0 6 gems installed雑感
AWSとか普通の環境を触り始めて1か月くらいたってようやくこの手のエラー対応に慣れてきたなぁ、と思いました(遅い)
- 投稿日:2020-02-10T09:10:48+09:00
Docker+RubyonRails でよく使うコマンドメモ
アプリを作成するときはDockerを利用するのですが、Railsコマンド打つとき最初よく分からなくて躓いてたんで自分用メモとして記します。
gemのインストール
docker-compose build --no-cacheDocker-compose downしてから行ってください。
dockerでrailsコマンドを打つ
docker-compose run --rm web railsDockerfileやdocker-compose.ymlの変更を反映、railsサーバーを再起動
docker-compose up --buildMySQL
docker-compose exec db mysql -u root -pMysqlは「database.yml」で指定したパスワードで中身を見ることができます。
まとめ
よくこの辺を使うので参考になればと思います。
- 投稿日:2020-02-10T07:50:28+09:00
rails 星評価の実装
rails 星評価を実装する手順
*players controllers で作業を行なっています。
まずやること
3つの星の画像を保存する
https://github.com/wbotelhos/raty/tree/master/lib/images
上記のファイルの中の「satar」から始まる3つの画像をapp/assets/imagesにダウンロードするjquaryファイルを保存する
https://github.com/wbotelhos/raty/blob/master/lib/jquery.raty.js
上記のファイルをapp/assets/javascript/jquery.raty.jsファイルを作成し、コピーするapp/assets/javascript/application.jsに
「//=require jquary」と記入 *require_treeよりも必ず前に書くGem.fileに
「gem 'jquary-rails'」と記入後bundle installrate(float型)のカラムを作成 (rails g migration AddRateToテーブル名 rate:float)
手順1 routes.rbにsearchアクションを追加する
routes.rbcollection do get 'search' endcollection do => resourcesに含まれないアクションを追加するときに使用する
手順2 controllerでsearchアクションの定義づけをする
players.controllerdef search @players = Player.search(params[:search]) end手順3 rateカラムを保存するフォームを作成する
new.html<input class="number" max="5.0" min="0" name="rate" step="0.5" type="number" placeholder="強さ(5段階評価)"/>type="number" => タグ内で使用すると数値の入力欄が作成される
手順3 rateの数字に応じて星評価を表示する
show.html<div id = "star-rate-<%= @player.id %>"></div> <script> $('#star-rate-<%= @player.id %>').raty({ size: 36, starOff: '<%= asset_path('star-off.png') %>', starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', half: true, readOnly: true, score: <%= @player.rate %>, }) </script>
- id => classと同じ役割。ただ、WEB1ページに1回しか使用できない。idクラスを呼ぶときは「#」を先頭につける
- $ => jQuaryを呼び出す記号
- ('#star-rate-<%= @player.id %>') => body要素内のstar-rate-<%= @player.id %>にアクセスする
- .raty => ratyのプロバティを使用することができるようになる
- asset_path => asset/imageの画像を表示する
- readOnly: true, => 画面表示のみで変更できないようにする
以上の操作で私は星評価を作成することができました。
- 投稿日:2020-02-10T07:45:50+09:00
Ruby + Mustache で Hello World
概要
- Ruby の mustache パッケージを使ってテンプレートを処理する
- 今回の環境: macOS Catalina + Ruby 2.7.0 + mustache 1.1.1
mustache パッケージのインストール
$ gem install mustacheHello World
シンプルなサンプルコードを示す。
require 'mustache' # 第一引数にテンプレート文字列 # 第二引数にHashオブジェクト hello = Mustache.new.render('Hello, {{planet}}.', {planet: 'world'}) puts hello実行結果。
Hello, world.HTML テンプレートファイルを読み込んで値を埋め込む
HTML を記述したテンプレートファイルを用意。
今回は my-template-file.mustache というファイル名で保存する。<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>{{title}}</title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p>{{mydata.message}}</p> <p> <!-- ループ --> {{#mydata}} {{#list}} {{.}}<br> {{/list}} {{/mydata}} </p> <p> <!-- hoge が存在する場合に出力--> {{#mydata.hoge}} Hoge exists. {{/mydata.hoge}} </p> <p> <!-- fuga が存在しない場合に出力--> {{^mydata.fuga}} Fuga does not exists. {{/mydata.fuga}} </p> </body> </html>ソースコード。
require 'mustache' # 第一引数にテンプレートファイル名 # 第二引数にHashオブジェクト output = Mustache.render_file( 'my-template-file', { :title => 'タイトル', :mydata => { :message => 'メッセージ', 'list' => ['foo', 'bar', 'baz'], 'hoge' => 'ほげ' } }) puts output実行結果。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- title を出力--> <title>タイトル</title> </head> <body> <!-- mydata オブジェクトの message を出力--> <p>メッセージ</p> <p> <!-- ループ --> foo<br> bar<br> baz<br> </p> <p> <!-- hoge が存在する場合に出力--> Hoge exists. </p> <p> <!-- fuga が存在しない場合に出力--> Fuga does not exists. </p> </body> </html>参考資料
- 投稿日:2020-02-10T06:25:16+09:00
【Rails】Railsに保存した画像ファイルをVue.js側で表示するサンプルコード(Base64、Active Storage使用)
はじめに
Rails APIモード→Vue.jsでの画像データのやりとりをする方法を残します。(Base64、Active Storage使用)
今回の対象
Vue.js→Rails(こちらの記事をご参照下さい。引用失礼します!)
Rails→Vue.js ←ココ環境
OS: macOS Catalina 10.15.3 Ruby: 2.6.5 Rails: 6.0.2.1 Vue: 2.6.10 axios: 0.19.0前提:実施済とみなすこと
※引用記事の例を使用します。
rails new
- Active Storageのインストール
Post
モデルの作成- Vue.jsのインストールと利用するための準備
eyecatch
として画像ファイルがPost
モデルのインスタンスに添付されている- (今ココ)
1.【Rails】画像ファイルをBase64形式でエンコードするメソッドを定義する
base64_module.rb# 各モデルのレコードに添付された画像ファイルをBase64でエンコードする def encode_base64(image_file) image = Base64.encode64(image_file.download) # 画像ファイルをActive Storageでダウンロードし、エンコードする blob = ActiveStorage::Blob.find(image_file[:id]) # Blobを作成 "data:#{blob[:content_type]};base64,#{image}" # Vue側でそのまま画像として読み込み出来るBase64文字列にして返す end2.【Rails】Active Storageでアタッチした画像ファイルを読み込み
posts#show
で投稿データを返すとします。posts_controller.rbdef show post = Post.find(params[:id]).as_json #JSON形式にしておく eyecatch = post.eyecatch #eyecatchは添付した画像ファイル if eyecatch.present? post['image'] = encode_base64(eyecatch) # 画像ファイルを1.で定義したメソッドでBase64エンコードし、renderするデータに追加する end render json: post end3.【Rails】ルーティングを設定
Rails.application.routes.draw do # 略 get 'posts', to: 'posts#show' # 略 end4.【Vue.js】画像を取得し、表示するコンポーネントを作成
show.vue<template> <div> <p>投稿表示フォーム</p> <!-- preventでsetPost()メソッドがページ遷移なく発火する --> <form v-on:submit.prevent="setPost()"> <p> <label>Title</label> <input name="post.title" type="text" v-model="post.title"><br /> </p> <p> <label>Body</label> <input name="post.body" type="text" v-model="post.body"><br /> </p> <!-- post.idを指定して... --> <p> <label>IDを指定</label> <input name="post.id" type="text" v-model="post.id"> </p> <!-- ここを押してデータ取得 --> <input type="submit" value="ここを押して投稿データ取得" > <!-- Base64形式であればimgタグでそのまま読み込みが可能 --> <img :src="post.image" alt="post.image"> </form> </div> </template> <script> import axios from 'axios' export default { name: 'sample', data() { return { post: {}, } }, methods: { setPost() { axios.get('/posts', {params: {id: this.post.id}}) //入力したidに応じてpostが返ってくる .then(response => { this.post = response.data }) .catch( error => { console.error(error) }) } } } </script>※実際は自分で
id
を指定することはないと思いますので、状況に応じて変更して頂ければと思います。以上です!
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2020-02-10T05:06:11+09:00
GF(2)上の多項式を掛け算するプログラム
def seki(a , b)
c=0;
while(a!=0)
if ((a & 1)==1)
c^=b;
end
b<<=1; a>>=1;
endreturn c;
end
- 投稿日:2020-02-10T02:32:27+09:00
RedisベースのGo Background Jobライブラリー
Asynq: RedisベースのGo Background Jobライブラリー
Ruby,Railsのサークルの中ではResqueやSidekiqがBackground-jobのライブラリーで人気ですが、Goのコミュニティーの中でこれといったライブラリーがあまり見つからなかったので自分でSidekiqのデザインを基にしてBackground Jobライブラリーを書いてみました(github.com/hibiken/asynq)。
GoやRedisに興味があってGithubでコントリビュートするプロジェクトを探していたら、是非!
- 投稿日:2020-02-10T00:33:07+09:00
deviseのログイン機能でユーザー名にバリデーションをかける。
- 投稿日:2020-02-10T00:12:09+09:00
【残業計算】タイムカード計算ができる Gem「punch-time」の使い方
この記事は
タイムカード計算 punch-time を作ったメモ
やりたかったこと
Railsで深夜残業時間の計算をするのにSQLだと再利用ができなそうだったので
やり方
インストール方法
gem 'punch-time'コンフィグ
コンフィグでシフトの開始、終了時間、休憩時間、深夜時間、時差が設定できます
PunchTime.configure do |config| config.shift_in_time = Time.parse('10:00') config.shift_out_time = Time.parse('19:00') config.breaks = [ { start_time: Time.parse('12:00'), end_time: Time.parse('13:00') } ] config.night = { start_time: Time.parse('22:00'), end_time: Time.parse('05:00') } config.offset = '+0900' endタイムカードの記録
就業時間はシフトの開始時間からで計算
PunchTime.punch(DateTime.parse('20200101 10:10'), DateTime.parse('20200101 19:00'))就業時間の出力
p PunchTime.sum_work.hours例えば、business_time、holiday_jpを組み合わせれば祝日の勤務時間計算がこうなります
sum_works = [] Date.parse('20200101').upto(Date.parse('20200105')) do |x| PunchTime.punch(DateTime.parse(x.to_s + ' 10:10'), DateTime.parse(x.to_s + ' 19:00')) sum_works.append(PunchTime.sum_work.hours) unless x.workday? end p sum_works.inject(:+)そのほか
PunchTime.sum_work PunchTime.sum_tardy PunchTime.sum_over_work PunchTime.sum_night_work PunchTime.tardy? PunchTime.overtime_work? PunchTime.night_overtime_work?技術的ポイント
なあなあだったタイムゾーンとか、Rationalの扱いに結構時間とられたので、これからはオープンソースは人々の協力によって支えられているのだ、ということを心に留めてGithubを利用したい