20200210のRubyに関する記事は24件です。

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', MustacheTemplate

Hello 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.&lt;&amp;&gt;</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.mustache

app.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
end

views/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 stop

curl でアクセス

必要な値が 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>こんにちは&amp;世界</p>
<p>ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]</p>
</body>
</html>

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スクリプト言語の比較しながらGoのお勉強 〜 標準入力編

前回からのおさらい

前回、Hello World編にて、標準出力に文字列を表示するという処理を確認しました。
今回はその対になります標準入力にてキーボードにて入力する文字列を受け取る処理を比較してみたいと思います。

標準入力

簡単にキーボードから1行入力された文字列を表示するプログラムを見ていくことにしましょう。

Goの標準入力

Goでの標準入力のプログラムです。

input.go
package 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パッケージはシステムのローレベルな処理から汎用的にレベルを上げたパッケージということですね。

bufioパッケージは、

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.py
text = input()
print(text)

実行結果

$ python input.py
Hello, Python
Hello, Python

Ruby

Rubyでは組み込み関数のgetsで実装可能です。
module Kernel

input.rb
text = gets
print text

実行結果

$ ruby input.rb
Hello, Ruby
Hello, Ruby

Perl

PerlではI/O演算子の<>で標準入力のファイルハンドルSTDINを指定して入力処理を実装します。
I/O 演算子

input.pl
$text = <STDIN>;
print $text;

実行結果

$ perl input.pl
Hello, Perl
Hello, Perl

Bash

Bashではreadコマンドで入力処理を実装します。
man read

input.sh
read text
echo $text

実行結果

$ bash input.sh
Hello, Bash
Hello, Bash

入力処理を比較してみて

入力処理を比較してみて感じたことはHello World編と同様で、本体は簡素に作られているというところです。
今回は多少パッケージのソースにも目を通してみたところ、低レベルの処理を駆使して実装はできるのでしょうが、
車輪の再発明は無駄で何も旨味がないためパッケージの使用を学ぶべきと感じました。
(但し、実装方法を確認することは多くの知見を得られますので、できる限りソースには目を通すべきだと思います)
では、次回以降は変数に関しての演算子や文字列処理について勉強したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

if文 配列の中身があるかを判定する書き方

配列の中身が空か判定しtrue/falseを返すメソッドとして
if 変数 present?
が使える
presentは配列の中身があればtrueを返し
presentは配列の中身が無ければfalseを返します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独学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, Github

1月 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系自社開発企業へ転職するまでのロードマップ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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系は入れるのが面倒だったので未確認です)
入出力例のスクレイピングは全コンテスト試したわけではないので、未対応のケースがあるかもしれません。もし見つけたら教えてくれると嬉しいです。
提出まわりはつくっていないのですが気が向いたら作ります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_forでのデータ取得

form_forでのデータ取得方法
params.require(:モデル名).permit(:カラム名)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 tilt

Hello 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>

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails フォームで入力した値を取得する方法

フォームで入力した値を取得する方法params[:keyword]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【残業時間】タイムカード の計算ができる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を利用したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【残業計算】タイムカード の計算ができる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を利用したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【残業計算】タイムカードの計算ができる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を利用したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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なので速くなります。

効果

差分がないコードで実行してみました。

設定前

1分27秒かかっています
87秒.png

設定後

3秒で終わってます
3秒.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
end

resume で呼び出す時は、 :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
end

2つの口座を開設してみます。

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 の残高
=> 2500

Ruby っぽく 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
end

Accountクラスを使って口座を開設してみましょう。

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 を利用できるということがわかったところでしょうか :sweat_smile:

また、 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 になるとか、まあ、色々、改善の余地がありそうです :sweat_drops:

参考情報

今回の記事を書くのにヒントとなった Elixir の書籍を紹介しておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト駆動開発から始めるRuby入門 ~6時間でオブジェクト指向のエッセンスを体験する~

エピソード3

初めに

この記事は テスト駆動開発から始めるRuby入門 ~2時間でTDDとリファクタリングのエッセンスを体験する~ の続編です。

前提として エピソード1を完了して、テスト駆動開発から始めるRuby入門 ~ソフトウェア開発の三種の神器を準備する~ で開発環境を構築したところから始まります。 別途、セットアップ済み環境 を用意していますのでこちらからだとすぐに始めることが出来ます。

本記事は一応オブジェクト指向プログラム入門者向けとなっていますが、入門者の方は用語についてはわからなくても結構です、コードを繰り返し写経することで感覚を掴んでもらえば自ずと書いてあることはわかるようになってきますので。あと、概要はオブジェクト指向プログラム経験者に向けて書いたのものなので読み飛ばしてもらって結構です(ネタバレ内容です)、経験者の方からのツッコミお待ちしております。

概要

本記事では、 オブジェクト指向プログラム から オブジェクト指向設計 そして モジュール分割テスト駆動開発 を通じて実践していきます。

オブジェクト指向プログラム

エピソード1で作成したプログラムの追加仕様を テスト駆動開発 で実装します。 次に 手続き型コード との比較から オブジェクト指向プログラム を構成する カプセル化 ポリモフィズム 継承 という概念をコードベースの リファクタリング を通じて解説します。

具体的には フィールドのカプセル から setterの削除 を適用することにより カプセル化 を実現します。続いて、 ポリモーフィズムによる条件記述の置き換え から State/Strategyによるタイプコードの置き換え を適用することにより ポリモーフィズム の効果を体験します。そして、 スーパークラスの抽出 から メソッド名の変更 メソッドの移動 の適用を通して 継承 の使い方を体験します。さらに 値オブジェクトファーストクラス というオブジェクト指向プログラミングに必要なツールの使い方も学習します。

オブジェクト指向設計

次に設計の観点から 単一責任の原則 に違反している FizzBuzz クラスを デザインパターン の1つである Commandパターン を使ったリファクタリングである メソッドオブジェクトによるメソッドの置き換え を適用してクラスの責務を分割します。オブジェクト指向設計のイデオムである デザインパターン として Commandパターン 以外に Value Objectパターン Factory Methodパターン Strategyパターンリファクタリング を適用する過程ですでに実現していたことを説明します。そして、オープン・クローズドの原則 を満たすコードに リファクタリング されたことで既存のコードを変更することなく振る舞いを変更できるようになることを解説します。

加えて、正常系の設計を改善した後 アサーションの導入 例外によるエラーコードの置き換え といった例外系の リファクタリング を適用します。最後に ポリモーフィズム の応用として 特殊ケースの導入 の適用による Null Objectパターン を使った オープン・クローズドの原則 に従った安全なコードの追加方法を解説します。

モジュールの分割

仕上げは、モノリシック なファイルから個別のクラスモジュールへの分割を ドメインオブジェクト の抽出を通して ドメインモデル へと整理することにより モジュール分割 を実現することを体験してもらいます。最後に 良いコード良い設計 について考えます。

Before

diag-c63943e73aed75ba31adf85779eaf481.png

After

diag-84a49e2f281dfc169055d0bfc4b4aeb6.png

オブジェクト指向から始めるテスト駆動開発

テスト駆動開発

エピソード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 status

TODOリスト作成

まずは追加仕様を 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

処理の流れをフローチャートにしたものです、実態はコードに記述されている内容を記号に置き換えて人間が読めるようにしたものです。

diag-c465147b957dba9fdfaa1a1196d29378.png

オブジェクト指向プログラム

続いて、これまでに作ってきたコードがこちらになります。上記の 手続き型コード との大きな違いとして 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
end

UML を使って上記のコードの構造をクラス図として表現しました。

diag-c63943e73aed75ba31adf85779eaf481.png

更にシーケンス図を使って上記のコードの振る舞いを表現しました。

diag-4cda3860e38bd75023756d182d6db0b7.png

手続き型コード のフローチャートと比べてどう思われましたか?具体的な記述が少なくデータや処理の概要だけを表現しているけどFizzBuzzのルールを知っている人であれば何をやろうとしているかのイメージはつかみやすいのではないでしょうか?だから何?と思われるかもしれませんが現時点では オブジェクト指向 において 抽象化 がキーワードだという程度の認識で十分です。

オブジェクト指向の理解を深める取り掛かりにはこちらの記事を参照してください。

オブジェクト指向の詳細は控えるとして、ここでは カプセル化 ポリモフィズム 継承 というオブジェクト指向プログラムで原則とされる概念をリファクタリングを通して体験してもらい、オブジェクト指向プログラムの感覚を掴んでもらうことを目的に解説を進めていきたいと思います。

カプセル化

フィールドのカプセル化

diag-c63943e73aed75ba31adf85779eaf481.png

まず、データとロジックを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
end
class 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.generateFizzBuzz.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: フィールドのカプセル化'

diag-102c44abf73ca5bda6fd8b87cc722ac9.png

引き続き、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の削除'

diag-a8b45c800de2a31873f9eba1feac2184.png

ポリモーフィズム

ポリモーフィズムによる条件記述の置き換え 1

diag-a8b45c800de2a31873f9eba1feac2184.png

リファクタリングによりデータとロジックを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): ポリモーフィズムによる条件記述の置き換え'

diag-c83e15398192d4cb68c948dfda55870b.png

ポリモーフィズムによる条件記述の置き換え 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): ポリモーフィズムによる条件記述の置き換え'

diag-c9ffad9b803420dabdb72a8eaf15cb72.png

ポリモーフィズムによる条件記述の置き換え 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): ポリモーフィズムによる条件記述の置き換え'

diag-852794d6dd3e17ad001905a500b520e3.png

ポリモーフィズムによる条件記述の置き換え 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
end
ERROR["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
end
Started 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によるタイプコードの置き換え

クラスの振る舞いに影響するタイプコードがあるが、サブクラス化はできない。

状態オブジェクトでタイプコードを置き換える

— 新装版 リファクタリング

diag-852794d6dd3e17ad001905a500b520e3.png

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によるタイプコードの置き換え'

diag-852794d6dd3e17ad001905a500b520e3.png

継承

分割したタイプクラスのメソッドに重複する処理があるので 継承 を使ってリファクタリングしましょう。ここでは スーパークラスの抽出を適用します。

スーパークラスの抽出

似通った特性を持つ2つのクラスがある。

スーパークラスを作成して、共通の特性を移動する。

— 新装版 リファクタリング

スーパークラスの抽出

diag-852794d6dd3e17ad001905a500b520e3.png

まずは、タイプクラスのスーパークラスとなる 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

diag-df749de89e01204bc0eab92419515a4c.png

次に 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
end
08: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: スーパークラスの抽出'

diag-119864cf3ef287a3cb00d3a5ae7f7768.png

メソッド名の変更

スーパークラスの抽出 を実施したところまた警告メッセージが表示されるようになりました。

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 detected

Naming/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
end
Progress: |====================================================================================================|

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: メソッド名の変更'

diag-f06a12d01484b03fa6f2f85b062a3cf0.png

メソッドの移動

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: メソッドの移動'

diag-c24dbdafdc4766204e9b3fba9938dbba.png

値オブジェクト

diag-c24dbdafdc4766204e9b3fba9938dbba.png

オブジェクトによるプリミティブの置き換え

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: オブジェクトによるプリミティブの置き換え'

diag-c24dbdafdc4766204e9b3fba9938dbba.png

マジックナンバーの置き換え

まだプリミティグ型を使っている部分があります。ここは マジックナンバーの置き換え を実施して可読性を上げておきましょう。

...
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: マジックナンバーの置き換え'

diag-be7c345eb3b4de2cc3b4530da0d96f5d.png

オブジェクトによるプリミティブの置き換え

次に 基本データ型への執着 の臭いがする箇所として 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: オブジェクトによるプリミティブの置き換え'

diag-719d1727313ce1fe35a0a3eaeca9a624.png

学習用テスト

値オブジェクト の理解を深めるために 学習用テスト を追加します。

...
  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: 学習用テスト'

ファーストクラスコレクション

diag-719d1727313ce1fe35a0a3eaeca9a624.png

コレクションのカプセル化

値オブジェクト を扱う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
end

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
class 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: コレクションのカプセル化'

diag-f753577a92fab80607a2e63cf53e8389.png

学習用テスト

ファーストクラスコレクション を理解するため 学習用テスト を追加しておきましょう。

...
  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: 学習用テスト'

オブジェクト指向設計

diag-f753577a92fab80607a2e63cf53e8389.png

値オブジェクト 及び ファーストクラスコレクション の適用で 基本データ型への執着 の臭いはなくなりました。今度は設計の観点から全体を眺めてみましょう。ここで気になるのが 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: メソッドオブジェクトによるメソッドの置き換え'

diag-ee5628564b01a6264bda537c84b0458b.png

メソッドオブジェクトによるメソッドの置き換え

続いて、ファーストクラスコレクション を扱う 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: メソッドオブジェクトによるメソッドの置き換え'

diag-4a094b5dcd7f922c01a8ad5b39f6839d.png

デッドコードの削除

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: デッドコードの削除'

diag-55cb49d0a54190528d027b3768e4aa29.png

デザインパターン

メソッドオブジェクトによるメソッドの置き換え リファクタリングの結果として Commandパターン という デザインパターン を適用しました。実はこれまでにも オブジェクトによるプリミティブの置き換え では Value Objectパターンポリモーフィズムによる条件記述の置き換え では Factory Methodパターン をそして、 委譲の隠蔽 の実施による State/Strategyによるタイプコードの置き換え では Strategyパターン を適用しています。

Command パターン

diag-3f9e53f62bd2f1b3bfe0f476521170ca.png

Value Objectパターン

広く共有されるものの、同一インスタンスであることはさほど重要でないオブジェクトを設計するにはどうしたらよいだろうか----オブジェクト作成時に状態を設定したら、その後決して変えないようにする。オブジェクトへの操作は必ず新しいオブジェクトを返すようにしよう。

— テスト駆動開発

Factory Methodパターン

オブジェクト作成に柔軟性をもたせたいときは、どうすればよいだろうか---単にコンストラクタで作るのではなく、メソッドを使ってオブジェクトを作成しよう。

— テスト駆動開発

Strategy パターン

diag-4969f773bcc5408d3afec24f14c006d3.png

作成したコードはパターンと完全に一致しているわけではありませんし、Rubyのような動的言語ではもっと簡単な実現方法もありますがここでは先人の考えた設計パターンというものがありオブジェクト指向設計の イデオム として使えること。そしてテスト駆動開発では一般的な設計アプローチとは異なる形で導かれているということくらいを頭に残しておけば結構です。どのパターンをいつ適用するかはリファクタリングを繰り返しているうちに思いつくようになってきます(多分)。

ただ、書籍『デザインパターン』(通称Gof本)の大ヒットは、その反面、それらパターンを表現する方法の多様性を奪ってしまった。Gof本には、設計をフェーズとして扱うという暗黙の前提があるように見受けられる。つまり、リファクタリングを設計行為として捉えていない。TDDにおける設計は、デザインパターンを少しだけ違う側面から捉えなければならない。

— テスト駆動開発

あと、設計の観点から今回 単一責任の原則 に従って FizzBuzz クラスを メソッドオブジェクト に分割して削除しました。

diag-51044325ad9691ebfe6e60879f6c95e7.png

もし、新しい処理を追加する必要が発生した場合はどうしましょうか? FizzBuzzCommand インターフェイスを実装した メソッドオブジェクト を追加しましょう。

diag-ea3196c5b1015bd1c10121bc92095fcb.png

もし、新しいタイプが必要になったらどうしましょうか? FizzBuzzType クラスを継承した新しいタイプクラスを追加しましょう。

diag-479f7874fa6e2b242ebd5a2f54730e37.png

このように既存のコードを変更することなく振る舞いを変更できるので オープン・クローズドの原則 を満たした設計といえます。

OCP:オープン・クローズドの原則

「オープン・クローズドの原則(OCP)」は、1988年にBertrand Maeerが提唱した以下のような原則だ。

ソフトウェアの構成要素は拡張に対しては開いていて、修正に対しては閉じていなければならない。
            『アジャイルソフトウェア開発の奥義 第2版』(SBクリエイティブ)より引用

言い換えれば、ソフトウェアの振る舞いは、既存の成果物を変更せず拡張できるようにすべきである、ということだ。

— Clean Architecture 達人に学ぶソフトウェアの構造と設計

例外

diag-55cb49d0a54190528d027b3768e4aa29.png

ここまでは、正常系をリファクタリングして設計を改善してきました。しかし、アプリケーションは例外系も考慮する必要があります。続いて、アサーションの導入 を適用した例外系のリファクタリングに取り組むとしましょう。

アサーションの導入

前提を明示するためのすぐれたテクニックとして、アサーションを記述する方法があります。

— リファクタリング(第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
...
end
class 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: アサーションの導入'

diag-31809a5e0bf909bd8ffd6bf80e82857a.png

次は、メソッドオブジェクト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: アサーションの導入'

diag-6390574a6b0b9b04721636e71b66aea3.png

アサーションの導入 とは別のアプローチとして 例外 を返す方法もあります。 例外によるエラーコードの置き換え を適用してアサーションモジュールを削除しましょう。

例外によるエラーコードの置き換え

エラーを示す特別なコードをメソッドがリターンしている。

代わりに例外を発生させる。

— 新装版 リファクタリング

例外によるエラーコードの置き換え

アサーションモジュールを削除してアサーション部分を 例外 に変更します。

...
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:  例外によるエラーコードの置き換え'

diag-cd1dbf997043a35c9bb55b407c0a2af9.png

アルゴリズムの置き換え

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: マジックナンバーの置き換え'

diag-cd1dbf997043a35c9bb55b407c0a2af9.png

特殊ケースの導入

最後に ポリモーフィズム の応用としてタイプクラスが未定義の場合に 例外 ではなく未定義のタイプクラスを返す 特殊ケースの導入 を適用してみましょう。

ヌルオブジェクトの導入

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: 特殊ケースの導入'

diag-88e1d9090efb8e346a8986204d1decac.png

モジュール分割

diag-88e1d9090efb8e346a8986204d1decac.png

クラスモジュールの抽出によってアプリケーションの構造が 抽象化 された結果、視覚的に把握できるようになりました。ここでアプリケーションを実行してみましょう。

$ 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): モジュール分割'

diag-5f47ca3306a2dc4364311b77c4b5cea3.png

アプリケーション

続いて アプリケーション層 の分割を行います。

データクラスと機能クラスを分ける手続き型の設計では、アプリケーション層のクラスに業務ロジックの詳細を記述します。

— 現場で役立つシステム設計の原則

/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): モジュール分割'

diag-84a49e2f281dfc169055d0bfc4b4aeb6.png

テスト

アプリケーションのメイン部分は分割できました。続いてテストも分割しましょう。

/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: モジュール分割'

diag-c335939a7cce862d0b767629390a1852.png

ふりかえり

今回、 オブジェクト指向プログラム から オブジェクト指向設計 そして モジュール分割テスト駆動開発を通じて実践しました。各トピックを振り返ってみましょう。

オブジェクト指向プログラム

エピソード1で作成したプログラムの追加仕様を テスト駆動開発 で実装しました。 次に 手続き型コード との比較から オブジェクト指向プログラム を構成する カプセル化 ポリモフィズム 継承 という概念をコードベースの リファクタリング を通じて解説しました。

具体的には フィールドのカプセル から setterの削除 を適用することにより カプセル化 を実現しました。続いて、 ポリモーフィズムによる条件記述の置き換え から State/Strategyによるタイプコードの置き換え を適用することにより ポリモーフィズム の効果を体験しました。そして、 スーパークラスの抽出 から メソッド名の変更 メソッドの移動 の適用を通して 継承 の使い方を体験しました。さらに 値オブジェクトファーストクラス というオブジェクト指向プログラミングに必要なツールの使い方も学習しました。

オブジェクト指向設計

次に設計の観点から 単一責任の原則 に違反している FizzBuzz クラスを デザインパターン の1つである Commandパターン を使ったリファクタリングである メソッドオブジェクトによるメソッドの置き換え を適用してクラスの責務を分割しました。オブジェクト指向設計のイデオムである デザインパターン として Commandパターン 以外に Value Objectパターン Factory Methodパターン Strategyパターンリファクタリング を適用する過程ですでに実現していたことを説明しました。そして、オープン・クローズドの原則 を満たすコードに リファクタリング されたことで既存のコードを変更することなく振る舞いを変更できるようになりました。

加えて、正常系の設計を改善した後 アサーションの導入 例外によるエラーコードの置き換え といった例外系の リファクタリング を適用しました。最後に ポリモーフィズム の応用として 特殊ケースの導入 の適用による Null Objectパターン を使った オープン・クローズドの原則 に従った安全なコードの追加方法を解説しました。

モジュールの分割

仕上げに、モノリシック なファイルから個別のクラスモジュールへの分割を ドメインオブジェクト の抽出を通して ドメインモデル へと整理することにより モジュール分割 を実現しました。最終的にプログラムからアプリケーションへと体裁を整えることが出来ました。以下が最終的なモジュール構造とコードです。

diag-c335939a7cce862d0b767629390a1852.png

  • 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)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で使ってみます。

さいごに

毎日更新します。
皆様の復習等にご活用頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】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か月くらいたってようやくこの手のエラー対応に慣れてきたなぁ、と思いました(遅い)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker+RubyonRails でよく使うコマンドメモ

アプリを作成するときはDockerを利用するのですが、Railsコマンド打つとき最初よく分からなくて躓いてたんで自分用メモとして記します。

gemのインストール

docker-compose build --no-cache

Docker-compose downしてから行ってください。

dockerでrailsコマンドを打つ

docker-compose run --rm web rails

Dockerfileやdocker-compose.ymlの変更を反映、railsサーバーを再起動

docker-compose up --build

MySQL

docker-compose exec db mysql -u root -p

Mysqlは「database.yml」で指定したパスワードで中身を見ることができます。

まとめ

よくこの辺を使うので参考になればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 install

  • rate(float型)のカラムを作成 (rails g migration AddRateToテーブル名 rate:float)

手順1 routes.rbにsearchアクションを追加する

routes.rb
collection do
  get 'search'  
end

collection do => resourcesに含まれないアクションを追加するときに使用する

手順2 controllerでsearchアクションの定義づけをする

players.controller
def 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, => 画面表示のみで変更できないようにする

以上の操作で私は星評価を作成することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby + Mustache で Hello World

概要

  • Ruby の mustache パッケージを使ってテンプレートを処理する
  • 今回の環境: macOS Catalina + Ruby 2.7.0 + mustache 1.1.1

mustache パッケージのインストール

$ gem install mustache

Hello 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>

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Railsに保存した画像ファイルをVue.js側で表示するサンプルコード(Base64、Active Storage使用)

はじめに

Rails APIモード→Vue.jsでの画像データのやりとりをする方法を残します。(Base64、Active Storage使用)

今回の対象

:x: Vue.js→Rails(こちらの記事をご参照下さい。引用失礼します!)
:o: 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文字列にして返す
  end

2.【Rails】Active Storageでアタッチした画像ファイルを読み込み

posts#showで投稿データを返すとします。

posts_controller.rb
  def 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
  end

3.【Rails】ルーティングを設定

Rails.application.routes.draw do
# 略
  get 'posts', to: 'posts#show'
# 略
end

4.【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を指定することはないと思いますので、状況に応じて変更して頂ければと思います。

以上です!

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GF(2)上の多項式を掛け算するプログラム

def seki(a , b)

c=0;
while(a!=0)
if ((a & 1)==1)
c^=b;
end
b<<=1; a>>=1;
end

return c;
end

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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でコントリビュートするプロジェクトを探していたら、是非!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseのログイン機能でユーザー名にバリデーションをかける。

・deviseを使用したログイン機能でのデフォルトバリデーションemail,passwordのみのため

~/models/user.rbにアクセス。

filename.rb
validates :nickname, presence: true, length: { maximum: 6 }

1.presenceは、指定された属性が空でないことを確認します。

2.length属性は値の長さを検証。多くのオプションがあり、長さ制限をさまざまな方法で指定可能。
:minimum - 属性はこの値より小さな値を取れない。
:maximum - 属性はこの値より大きな値を取れない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【残業計算】タイムカード計算ができる 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を利用したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む