はじめに
今回、学習のためにブラックジャックゲームを作成したのですが、その際にコードが長く複雑になり過ぎてしまったためRuboCopを使用して修正を図っていきました。
その際の操作を備忘録として残したいと思い、本記事に書き起こしていこうと思います。
RuboCopとは?
RuboCopは、Rubyプログラムの静的コード解析ツールのことで、コードの品質をチェックし、一貫性のあるコーディングスタイルを維持することを目的としています。
詳しくはこちらをご参照ください。
基本的な使い方
それではRuboCopをインストールしていきたいと思います。
今回はRubyアプリに組み込むことを想定して、Gemfileに記述する方法をご紹介します。
最初にGemfileがなかったので雛形を作成するためにbundle init
を実行します。
% bundle init
Writing new Gemfile to /Users/[UserName]/Desktop/milliontech-sample-pj/Gemfile
ちなみにgemfileの中身は以下のようになっています。
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
# gem "rails"
ここにgem 'rubocop', require:false
を追加します。
require:falseは、Rubocopがインストールされた後に自動的にロードされないようにするためのオプションです。このオプションを使用することで、プロジェクトの起動時にRubocopが実行されるのを防止し、必要なときに手動でRubocopを実行することができます。
追加したらbundle install
を実行しましょう。
そして、下記がRuboCopの基本コマンドです。
% rubocop
# RuboCopによるチェックがスタートし、結果が表示される。
% rubocop -a
# RuboCopによる自動修正が行われる。
% rubocop --help
# その他オプションの確認
それでは上記に則って実行してみます。
% rubocop
zsh: command not found: rubocop
先ほどの手順でRubyのコード解析ツール「Rubocop」の利用を試みましたが、エラーが出てしまい実行できませんでした・・・
こちらの記事によるとrbenvを利用している場合、bundle install
を実行しただけではshimディレクトリが更新されないため、実行できないみたいです。
そのため、rbenv rehash
を実行することで、どのディレクトリにいてもrubocopコマンドが使えるようになるみたいです。
% rbenv rehash
% rubocop
Inspecting 44 files ・・・
無事に導入が完了しました。
それでは本題であるRuboCopによるプログラムの修正を図っていきます。
プログラムの修正
今回は下記のプログラムの修正を図っていきます。
class Card
attr_reader :suit,:value
def initialize(suit, value)
@suit = suit
@value = value
end
def to_s
"#{suit}の#{value}"
end
def point
case value
when "A"
11
when "J", "Q", "K"
10
else
value.to_i
end
end
end
class Deck
SUITS = ["ハート", "ダイヤ", "クラブ", "スペード"]
VALUES = (2..10).to_a.map { |num| num.to_s }+ %w[J Q K A]
def initialize
@cards = SUITS.product(VALUES).map { |suit, value| Card.new(suit, value) }
@cards.shuffle!
end
def draw
@cards.pop
end
end
class Player
attr_reader :hand, :score
def initialize
@hand = []
@score = 0
end
def hit(card)
@hand << card
update_score(card)
end
private
def update_score(card)
@score += card.point
adjust_for_ace if @score > 21 && aces_in_hand?
end
def aces_in_hand?
@hand.any? { |card| card.value == "A" }
end
def adjust_for_ace
@hand.each do |card|
if card.value == "A" && @score > 21
@score -= 10
end
end
end
end
def play_blackjack
puts "ブラックジャックを開始します。"
deck = Deck.new
player = Player.new
dealer = Player.new
2.times do
player.hit(deck.draw)
dealer.hit(deck.draw)
end
puts "あなたの引いたカードは#{player.hand[0]}です。"
puts "あなたの引いたカードは#{player.hand[1]}です。"
puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
puts "ディーラーの引いた2枚目のカードはわかりません。"
loop do
puts "あなたの現在の得点は#{player.score}です。カードを引きますか?(Y/N)"
response = gets.chomp.upcase
if response == "Y"
new_card = deck.draw
player.hit(new_card)
puts "あなたの引いたカードは#{new_card}です。"
else
break
end
break if player.score > 21
end
puts "あなたの現在の得点は#{player.score}です。"
if player.score > 21
puts "あなたの負けです!"
else
puts "ディーラーの引いた2枚目のカードは#{dealer.hand[1]}でした。"
while dealer.score < 17
new_card = deck.draw
dealer.hit(new_card)
puts "ディーラーの引いたカードは#{new_card}です。"
end
puts "ディーラーの現在の得点は#{dealer.score}です。"
if dealer.score > 21 || player.score > dealer.score
puts "あなたの勝ちです!"
elsif dealer.score == player.score
puts "引き分けです。"
else
puts "あなたの負けです!"
end
end
puts "ブラックジャックを終了します。"
end
play_blackjack
これだけだとわかりにくいのでざっくりと説明します。
[プログラムの説明]
このプログラムは、ブラックジャックと呼ばれるトランプゲームをRubyで実装しています。
このプログラムは、3つのクラス(Card(カード)、Deck(デッキ)、Player(プレイヤー))を定義しています。以下は各クラスの機能の説明です。
・ Cardクラス:トランプのカードを表すクラス。カードのスート(クラブ、ハート、ダイヤ、スペードのマークを指す言葉)と値をインスタンス変数として持ちます。また、カードの得点を計算するメソッドも定義しています。
・ Deckクラス:トランプのデッキを表すクラス。全ての52枚のカードを持ち、カードをシャッフルし、カードを1枚ずつ引くことができます。
・ Playerクラス:プレイヤーを表すクラス。プレイヤーはカードを持ち、現在の得点を計算することができます。また、カードを引くこともできます。
最後に、play_blackjackメソッドは、実際にブラックジャックのゲームを実行します。
デッキと2人のプレイヤー(プレイヤーとディーラー)を作成し、カードを配り、プレイヤーにカードを引かせ、ディーラーがカードを引くループを実行し、勝者を決定します。
勝者は、最高得点が21を超えないようにカードを選択したプレイヤーです。
ではこの状態でrubocop -a
を実行します。
% rubocop -a
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.
Please also note that you can opt-in to new cops by default by adding this to your config:
AllCops:
NewCops: enable
・
・
・
2 files inspected, 66 offenses detected, 59 offenses corrected, 3 more offenses can be corrected with `rubocop -A`
結構修正してくれましたね。
残されたエラーを見ていきましょう。
まずは上記の文をお馴染みのdeepで翻訳してみます。
(訳)2つのファイルを検査し、69の違反が検出され、59の違反が修正され、さらに3つの違反が rubocop -A で修正可能です。
言われた通り実行してみます。
% rubocop -A
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.
Please also note that you can opt-in to new cops by default by adding this to your config:
AllCops:
NewCops: enable
・
・
・
2 files inspected, 11 offenses detected, 4 offenses corrected
(訳)2ファイル検査、11件の違反が検出、4件の違反が修正
残りエラーが7つあるみたいですね。
あと先ほどから出ている下記のエラー文なんですが、
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.
Please also note that you can opt-in to new cops by default by adding this to your config:
AllCops:
NewCops: enable
これはRuboCopが最近追加されたいくつかのコード検査ルールを検出し、それらがRuboCopの設定ファイルで有効または無効になっていないことを示しています。
これらのコード検査ルールは、.rubocop.ymlファイルで明示的に有効または無効にする必要があります。
RuboCopの設定ファイルにEnabledという設定を追加し、それをtrueまたはfalseの値に設定することで、これらのコード検査ルールを有効または無効にできます。また、新しいコード検査ルールをデフォルトで有効にすることもできます。その場合、AllCopsセクションを追加し、NewCopsをenableに設定します。
まあ文章だけだとわかりづらいので.実際にrubocop.ymlファイルを作ってみます。
AllCops:
NewCops: enable
設定についてさらに噛み砕いて説明していきます。
NewCops: enable
とはNewCops: rubocop のバージョンをアップグレードしたときに新しいCopが自動的に有効になる設定です。
これにより改善点が見つかりやすいので、enableにしておきます。
そしたら実行してみましょう。
% rubocop -a
Inspecting 2 files
.C
Offenses:
blackjack.rb:3:1: C: Style/Documentation: Missing top-level documentation comment for class Card.
class Card
^^^^^^^^^^
blackjack.rb:27:1: C: Style/Documentation: Missing top-level documentation comment for class Deck.
class Deck
^^^^^^^^^^
blackjack.rb:41:1: C: Style/Documentation: Missing top-level documentation comment for class Player.
class Player
^^^^^^^^^^^^
・
・
・
2 files inspected, 7 offenses detected
先ほど挙げていたエラー分が表示されなくなり、エラーの内容が変わりました。
エラー文に飽きてきたかもしれませんが根気良く頑張るます(𓁹‿𓁹)フッ・・・
上記のエラーを調べてみると、RuboCopがクラス Card、Deck、Playerの上部にドキュメンテーションコメントがないことを検出したことを示してくれているみたいです。 つまり、クラスの説明が不足しているということです。
どういうことかというとそれぞれのクラスが何をするかわからないから説明を書かないと後で見返した時に困りますよってことを伝えてくれてるってこと。
では説明文を追加してみましょう。
# Represents a single playing card in a deck.
class Card
# ...
end
# Represents a deck of playing cards.
class Deck
# ...
end
# Represents a player in the game of blackjack.
class Player
# ...
end
それではこの状態で再度rubocop -a
を実行してみます。
% rubocop -a
Inspecting 2 files
.C
Offenses:
blackjack.rb:75:1: C: Metrics/AbcSize: Assignment Branch Condition size for play_blackjack is too high. [<6, 51, 16> 53.79/17]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for play_blackjack is too high. [8/7]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/MethodLength: Method has too many lines. [41/10]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/PerceivedComplexity: Perceived complexity for play_blackjack is too high. [10/8]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
2 files inspected, 4 offenses detected
エラーが減りましたね。残り4つです。
残りの4つは、よくみて見ると全部play_blackjackメソッドについて言及していますね。
確かにplay_blackjackメソッドは誰がどうみても長いしごちゃごちゃしてるし、文句は言われる気がします。笑
上記のエラー文を訳して調べてみましょうか。
RuboCop公式によると
- Metrics/AbcSizeは、代入ブランチ条件のサイズが大きすぎるということを示しています。
- Metrics/CyclomaticComplexityは、コードの分岐が複雑すぎるということを示しています。
- Metrics/MethodLengthは、メソッドが長すぎるということを示しています。
- Metrics/PerceivedComplexityは、コードの読み取りやすさが低いことを示しています。
ということみたいです。
案の定めちゃくちゃ怒られてますね。笑
ここも要約すると
play_blackjackメソッド長すぎてごちゃごちゃしてるからどうにかならない?
ってことですね。
要望に応えるのが真のエンジニア・・・
play_blackjackメソッドのところを修正していきます。
def player_turn(deck, player)
loop do
puts "あなたの現在の得点は#{player.score}です。カードを引きますか?(Y/N)"
answer = gets.chomp.upcase
if answer == "Y"
player.hit(deck.draw)
puts "あなたの引いたカードは#{player.hand.last}です。"
else
break
end
end
end
def dealer_turn(deck, dealer)
while dealer.score < 17
dealer.hit(deck.draw)
end
end
def determine_winner(player, dealer)
if player.score > 21
:dealer
elsif dealer.score > 21
:player
else
player.score > dealer.score ? :player : :dealer
end
end
def display_winner(winner)
if winner == :player
puts "あなたの勝ちです!"
else
puts "ディーラーの勝ちです。"
end
end
def play_blackjack
deck = Deck.new
player = Player.new
dealer = Player.new
# Initial draw
2.times { player.hit(deck.draw) }
2.times { dealer.hit(deck.draw) }
puts "ブラックジャックを開始します。"
puts "あなたの引いたカードは#{player.hand[0]}です。"
puts "あなたの引いたカードは#{player.hand[1]}です。"
puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
puts "ディーラーの引いた2枚目のカードはわかりません。"
player_turn(deck, player)
dealer_turn(deck, dealer)
winner = determine_winner(player, dealer)
display_winner(winner)
puts "ブラックジャックを終了します。"
end
play_blackjack
このリファクタリングでは、play_blackjackメソッドをいくつかの小さなメソッドに分割しました。
- player_turn: プレイヤーのターンを処理。
- dealer_turn: ディーラーのターンを処理。
- determine_winner: プレイヤーとディーラーの勝者を決定。
- display_winner: 勝者を表示。
これにより、RuboCopが検出した違反を修正し、コードの可読性が向上するはずです。
再度実行してみると、
% rubocop -a
Inspecting 2 files
.C
Offenses:
blackjack.rb:108:1: C: Metrics/AbcSize: Assignment Branch Condition size for play_blackjack is too high. [<4, 25, 0> 25.32/17]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:108:1: C: Metrics/MethodLength: Method has too many lines. [15/10]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
2 files inspected, 2 offenses detected
asone0420@MacBook-Air submission_quest %
狙い通りエラー文が減りましたね。
Metrics/AbcSize
とMetrics/MethodLength
なのでメソッドをより小さくして行数も減らせということなので最後の要望に答えます。
def initial_draw(player, dealer, deck)
2.times { player.hit(deck.draw) }
2.times { dealer.hit(deck.draw) }
end
def display_initial_cards(player, dealer)
puts "ブラックジャックを開始します。"
puts "あなたの引いたカードは#{player.hand[0]}です。"
puts "あなたの引いたカードは#{player.hand[1]}です。"
puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
puts "ディーラーの引いた2枚目のカードはわかりません。"
end
def play_blackjack
deck = Deck.new
player = Player.new
dealer = Player.new
initial_draw(player, dealer, deck)
display_initial_cards(player, dealer)
player_turn(deck, player)
dealer_turn(deck, dealer)
winner = determine_winner(player, dealer)
display_winner(winner)
puts "ブラックジャックを終了します。"
end
play_blackjack
上記では、play_blackjackメソッドをさらに短くするために、初期カードの配布と表示を別のメソッドに分割しました。
- initial_draw: プレイヤーとディーラーに初期カードを配る。
- display_initial_cards: プレイヤーとディーラーの初期カードを表示。
これにより、play_blackjackメソッドの行数が短くなり、RuboCopの指摘に対処できます。
それでは最後に試してみます。
% rubocop -a
Inspecting 2 files
..
2 files inspected, no offenses detected
なんとかクリアできました。
ここまで根気よく付き合わないとエラーが消えないとは・・・
恐るべしRuboCop。
まとめ
RuboCopは、Rubyプログラムの静的コード解析ツールです。コードの品質をチェックし、一貫性のあるコーディングスタイルを維持することを目的としています。
また具体的には以下のような機能を提供しています。
- 複雑さの低減 :RuboCopは、メソッドの長さや複雑さをチェックし、コードの可読性と保守性を向上させるための指摘を行う。
- パフォーマンスの最適化 :RuboCopは、より効率的な代替手段が存在する場合、コードのパフォーマンスを向上させるための提案を行う。
- コーディングスタイルの統一 :RuboCopは、インデントや空白の使用、命名規則など、一貫したコーディングスタイルを維持するためのルールを適用する。
- セキュリティの向上 :RuboCopは、セキュリティのリスクがあるコードを検出し、より安全なコーディング方法を提案する。
最後に
今回、RuboCopを使って導入から実際にプログラムを修正してみて、いかに自分のコードがいい加減か痛感させられました。
日頃からプログラムの機能的なところだけじゃなくて、読み手のことまで意識してコードを書いていければいいなと深く感じました。
参考文献